From 31c5925a5347dc31a228007bcbbbffe8e222ba6b Mon Sep 17 00:00:00 2001 From: Joakim Soderlund Date: Sun, 18 Dec 2016 18:40:06 +0100 Subject: [PATCH] Add stories module --- fimfarchive/stories.py | 90 +++++++++++++++++++ tests/test_stories.py | 190 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 280 insertions(+) create mode 100644 fimfarchive/stories.py create mode 100644 tests/test_stories.py diff --git a/fimfarchive/stories.py b/fimfarchive/stories.py new file mode 100644 index 0000000..ad49fe9 --- /dev/null +++ b/fimfarchive/stories.py @@ -0,0 +1,90 @@ +""" +Stories for Fimfarchive. +""" + + +# +# Fimfarchive, preserves stories from Fimfiction. +# Copyright (C) 2015 Joakim Soderlund +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + + +class Story: + """ + Represents a story. + """ + + def __init__(self, key, fetcher, meta=None, data=None): + if fetcher is None and (data is None or meta is None): + raise ValueError("Story must contain fetcher if lazy.") + + self.key = key + self.fetcher = fetcher + self._meta = meta + self._data = data + + @property + def is_fetched(self): + """ + True if no more fetches are necessary. + """ + return self.has_meta and self.has_data + + @property + def has_meta(self): + """ + True if story meta has been fetched. + """ + return self._meta is not None + + @property + def meta(self): + """ + Returns the story meta. + + Meta may be fetched if this story instance is lazy. + + Raises: + InvalidStoryError: If a valid story is not found. + StorySourceError: If source does not return any data. + """ + if not self.has_meta: + self._meta = self.fetcher.fetch_meta(self.key) + + return self._meta + + @property + def has_data(self): + """ + True if story data has been fetched. + """ + return self._data is not None + + @property + def data(self): + """ + Returns the story data. + + Data may be fetched if this story instance is lazy. + + Raises: + InvalidStoryError: If a valid story is not found. + StorySourceError: If source does not return any data. + """ + if not self.has_data: + self._data = self.fetcher.fetch_data(self.key) + + return self._data diff --git a/tests/test_stories.py b/tests/test_stories.py new file mode 100644 index 0000000..f036886 --- /dev/null +++ b/tests/test_stories.py @@ -0,0 +1,190 @@ +""" +Story tests. +""" + + +# +# Fimfarchive, preserves stories from Fimfiction. +# Copyright (C) 2015 Joakim Soderlund +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + + +import pytest + +from fimfarchive.stories import Story +from fimfarchive.exceptions import FimfarchiveError + + +KEY = 1 + + +class TestStory: + """ + Story tests. + """ + + @pytest.fixture + def meta(self): + """ + Returns some fake story meta. + """ + return {'id': KEY} + + @pytest.fixture + def data(self): + """ + Returns some fake story data. + """ + return b'' + + def test_init(self, fetcher, meta, data): + """ + Tests story initialization. + """ + story = Story(KEY, fetcher, meta, data) + + assert story.key == KEY + assert story.fetcher == fetcher + assert story.has_meta and story.meta == meta + assert story.has_data and story.data == data + + def test_init_with_fetcher_only(self, fetcher): + """ + Tests lazy story can be initialized. + """ + story = Story(KEY, fetcher) + + assert story.key == KEY + assert story.fetcher == fetcher + + def test_init_with_meta_and_data_only(self, meta, data): + """ + Test story can be initialized with only meta and data. + """ + story = Story(KEY, None, meta, data) + + assert story.key == KEY + assert story.has_data and story.data == data + assert story.has_meta and story.meta == meta + + def test_init_without_fetcher_nor_meta(self, data): + """ + Tests `ValueError` is raised on init without fetcher nor meta. + """ + with pytest.raises(ValueError): + Story(KEY, None, None, data) + + def test_init_without_fetcher_nor_data(self, meta): + """ + Tests `ValueError` is raised on init without fetcher nor data. + """ + with pytest.raises(ValueError): + Story(KEY, None, meta, None) + + def test_init_without_fetcher_nor_both(self): + """ + Tests `ValueError` is raised on init without fetcher, meta, nor data. + """ + with pytest.raises(ValueError): + Story(KEY, None) + + def test_fetch_meta_from_fetcher(self, fetcher, meta, data): + """ + Tests lazy story fetches meta once from fetcher. + """ + fetcher.fetch_meta.return_value = meta + story = Story(KEY, fetcher, None, data) + + assert not story.has_meta + fetcher.fetch_meta.assert_not_called() + assert story.meta == meta + assert story.has_meta + assert story.meta == meta + fetcher.fetch_meta.assert_called_once_with(KEY) + + def test_fetch_data_from_fetcher(self, fetcher, meta, data): + """ + Tests lazy story fetches data once from fetcher. + """ + fetcher.fetch_data.return_value = data + story = Story(KEY, fetcher, meta, None) + + assert not story.has_data + fetcher.fetch_data.assert_not_called() + assert story.data == data + assert story.has_data + assert story.data == data + fetcher.fetch_data.assert_called_once_with(KEY) + + def test_meta_not_fetched_unless_necessary(self, fetcher, meta): + """ + Test `fetch_meta` not called unless necessary. + """ + story = Story(KEY, fetcher, meta, None) + + assert story.has_meta and story.meta == meta + fetcher.fetch_meta.assert_not_called() + + def test_data_not_fetched_unless_necessary(self, fetcher, data): + """ + Test `fetch_data` not called unless necessary. + """ + story = Story(KEY, fetcher, None, data) + + assert story.has_data and story.data == data + fetcher.fetch_data.assert_not_called() + + def test_is_fetched(self, fetcher, meta, data): + """ + Tests `is_fetched` requires both meta and data. + """ + fetcher.fetch_meta.return_value = meta + fetcher.fetch_data.return_value = data + story = Story(KEY, fetcher) + + assert not story.is_fetched + assert story.meta == meta + assert not story.is_fetched + assert story.data == data + assert story.is_fetched + + def test_raises_fetch_meta_exception(self, fetcher): + """ + Tests exception from meta fetch is raised. + """ + fetcher.fetch_meta.side_effect = FimfarchiveError() + story = Story(KEY, fetcher) + + assert not story.has_meta + + with pytest.raises(FimfarchiveError): + story.meta + + assert not story.has_meta + + def test_raises_fetch_data_exception(self, fetcher): + """ + Tests exception from data fetch is raised. + """ + fetcher.fetch_data.side_effect = FimfarchiveError() + story = Story(KEY, fetcher) + + assert not story.has_data + + with pytest.raises(FimfarchiveError): + story.data + + assert not story.has_data