diff --git a/fimfarchive/mappers.py b/fimfarchive/mappers.py index 341afbc..0fa4172 100644 --- a/fimfarchive/mappers.py +++ b/fimfarchive/mappers.py @@ -24,11 +24,12 @@ Mappers for Fimfarchive. import os from abc import abstractmethod -from typing import Generic, Optional, TypeVar +from typing import Dict, Generic, Optional, Set, TypeVar from arrow import api as arrow, Arrow from fimfarchive.exceptions import InvalidStoryError +from fimfarchive.flavors import MetaFormat from fimfarchive.stories import Story @@ -115,3 +116,24 @@ class StoryPathMapper(Mapper[str]): key = str(story.key) return os.path.join(directory, key) + + +class MetaFormatMapper(Mapper[Optional[MetaFormat]]): + """ + Guesses the meta format of stories. + """ + spec: Dict[MetaFormat, Set[str]] = { + MetaFormat.ALPHA: {'likes', 'dislikes', 'words'}, + MetaFormat.BETA: {'num_likes', 'num_dislikes', 'num_words'}, + } + + def __call__(self, story: Story) -> Optional[MetaFormat]: + items = self.spec.items() + meta = set(story.meta.keys()) + + matches = {fmt for fmt, spec in items if spec & meta} + + if len(matches) == 1: + return next(iter(matches)) + else: + return None diff --git a/tests/test_mappers.py b/tests/test_mappers.py index 9c5db5d..82b14d5 100644 --- a/tests/test_mappers.py +++ b/tests/test_mappers.py @@ -23,12 +23,16 @@ Mapper tests. import os +from typing import no_type_check, Dict, Any from unittest.mock import patch, MagicMock, PropertyMock import pytest from fimfarchive.exceptions import InvalidStoryError -from fimfarchive.mappers import StaticMapper, StoryDateMapper, StoryPathMapper +from fimfarchive.flavors import MetaFormat +from fimfarchive.mappers import ( + MetaFormatMapper, StaticMapper, StoryDateMapper, StoryPathMapper +) from fimfarchive.stories import Story @@ -108,7 +112,7 @@ class TestStoryDateMapper: """ Tests `None` is returned when meta contains no dates. """ - meta = { + meta: Dict[str, Any] = { CHAPTERS: [ dict(), dict(), @@ -259,6 +263,7 @@ class TestStoryPathMapper: assert mapper(story) == path + @no_type_check def test_casts_values(self, tmpdir, story): """ Tests casts all values to string when joining. @@ -274,3 +279,58 @@ class TestStoryPathMapper: assert mapper(story) == os.path.join('dir', 'key') assert directory.__str__.called_once_with() assert story.key.__str__.called_once_with() + + +class TestMetaFormatMapper: + """ + MetaFormatMapper tests. + """ + + @pytest.fixture + def mapper(self): + """ + Returns a meta format mapper instance. + """ + return MetaFormatMapper() + + @pytest.fixture(params=['likes', 'dislikes', 'words']) + def alpha(self, request): + """ + Returns an alpha meta key. + """ + return request.param + + @pytest.fixture(params=['num_likes', 'num_dislikes', 'num_words']) + def beta(self, request): + """ + Returns a beta meta key. + """ + return request.param + + def merge(self, story, *keys): + """ + Returns a story containing the requested meta keys. + """ + meta = {key: i for i, key in enumerate(keys, 1)} + return story.merge(meta=meta) + + def test_alpha_format(self, mapper, story, alpha): + """ + Tests alpha meta format is detected. + """ + story = self.merge(story, alpha, 'misc') + assert mapper(story) == MetaFormat.ALPHA + + def test_beta_format(self, mapper, story, beta): + """ + Tests beta meta format is detected. + """ + story = self.merge(story, beta, 'misc') + assert mapper(story) == MetaFormat.BETA + + def test_conflict(self, mapper, story, alpha, beta): + """ + Tests None is returned for conflicting meta keys. + """ + story = self.merge(story, alpha, beta) + assert mapper(story) is None