From 72d65b9195f25e37f7d3c65df00e03c6c22df853 Mon Sep 17 00:00:00 2001 From: cosven Date: Thu, 22 Aug 2024 01:34:18 +0800 Subject: [PATCH] clean code: python serializer --- feeluown/serializers/model_helpers.py | 121 -------------------- feeluown/serializers/objs.py | 127 ++++++++++++++++++-- feeluown/serializers/plain.py | 159 ++++++++++++++++---------- feeluown/serializers/python.py | 65 +++++------ tests/serializers/test_serializers.py | 1 + 5 files changed, 242 insertions(+), 231 deletions(-) delete mode 100644 feeluown/serializers/model_helpers.py diff --git a/feeluown/serializers/model_helpers.py b/feeluown/serializers/model_helpers.py deleted file mode 100644 index e1c2a40020..0000000000 --- a/feeluown/serializers/model_helpers.py +++ /dev/null @@ -1,121 +0,0 @@ -from feeluown.library import AbstractProvider -from feeluown.library import ( - BaseModel, - SongModel, - ArtistModel, - AlbumModel, - PlaylistModel, - UserModel, - BriefSongModel, - BriefArtistModel, - BriefAlbumModel, - BriefPlaylistModel, - BriefUserModel, - SimpleSearchResult, -) -from feeluown.library import reverse - - -class ModelSerializerMixin: - - def _get_items(self, model): - # initialize fields that need to be serialized - # if as_line option is set, we always use fields_display - if self.opt_as_line or self.opt_brief: - modelcls = type(model) - fields = [field for field in model.__fields__ - if field not in BaseModel.__fields__] - # Include properties. - pydantic_fields = ("__values__", "fields", "__fields_set__", - "model_computed_fields", "model_extra", - "model_fields_set") - fields += [prop for prop in dir(modelcls) - if isinstance(getattr(modelcls, prop), property) - and prop not in pydantic_fields] - else: - assert False, 'unreachable' - fields = self._declared_fields - items = [("provider", model.source), - ("identifier", str(model.identifier)), - ("uri", reverse(model))] - if self.opt_fetch: - for field in fields: - items.append((field, getattr(model, field))) - else: - for field in fields: - items.append((field, getattr(model, field + '_display'))) - return items - - -class SongSerializerMixin: - class Meta: - types = (SongModel, BriefSongModel) - # since url can be too long, we put it at last - fields = ('title', 'duration', 'album', 'artists') - line_fmt = '{uri:{uri_length}}\t# {title:_18} - {artists_name:_20}' - - -class ArtistSerializerMixin: - class Meta: - types = (ArtistModel, BriefArtistModel) - fields = ('name', 'songs') - line_fmt = '{uri:{uri_length}}\t# {name:_40}' - - -class AlbumSerializerMixin: - class Meta: - types = (AlbumModel, BriefAlbumModel) - fields = ('name', 'artists', 'songs') - line_fmt = '{uri:{uri_length}}\t# {name:_18} - {artists_name:_20}' - - -class PlaylistSerializerMixin: - class Meta: - types = (PlaylistModel, BriefPlaylistModel) - fields = ('name', ) - line_fmt = '{uri:{uri_length}}\t# {name:_40}' - - -class UserSerializerMixin: - class Meta: - types = (UserModel, BriefUserModel) - fields = ('name', 'playlists') - line_fmt = '{uri:{uri_length}}\t# {name:_40}' - - -class SearchSerializerMixin: - """ - - .. note:: - - SearchModel isn't a standard model, it does not have identifier, - the uri of SearchModel instance is also not so graceful, so we handle - it as a normal object temporarily. - """ - - class Meta: - types = (SimpleSearchResult, ) - - def _get_items(self, result): - fields = ('songs', 'albums', 'artists', 'playlists',) - items = [] - for field in fields: - value = getattr(result, field) - if value: # only append if it is not empty - items.append((field, value)) - return items - - -class ProviderSerializerMixin: - class Meta: - types = (AbstractProvider, ) - - def _get_items(self, provider): - """ - :type provider: AbstractProvider - """ - return [ - ('identifier', provider.identifier), - ('uri', 'fuo://{}'.format(provider.identifier)), - ('name', provider.name), - ] diff --git a/feeluown/serializers/objs.py b/feeluown/serializers/objs.py index 864d9a879e..69ae564756 100644 --- a/feeluown/serializers/objs.py +++ b/feeluown/serializers/objs.py @@ -3,21 +3,57 @@ TODO: too much code to define serializers for an object. """ + from feeluown.app import App +from feeluown.library import AbstractProvider, SimpleSearchResult, reverse from feeluown.player import PlaybackMode, State, Metadata from . import PlainSerializer, PythonSerializer, \ SerializerMeta, SimpleSerializerMixin +from .python import ListSerializer as PythonListSerializer +from .plain import ListSerializer as PlainListSerializer + + +class SearchSerializerMixin: + """ + + .. note:: + + SearchModel isn't a standard model, it does not have identifier, + the uri of SearchModel instance is also not so graceful, so we handle + it as a normal object temporarily. + """ + + class Meta: + types = (SimpleSearchResult,) + + def _get_items(self, result): + fields = ('songs', 'albums', 'artists', 'playlists',) + items = [] + for field in fields: + value = getattr(result, field) + if value: # only append if it is not empty + items.append((field, value)) + return items -__all__ = ( - 'AppPythonSerializer', - 'AppPlainSerializer', -) +class ProviderSerializerMixin: + class Meta: + types = (AbstractProvider,) + + def _get_items(self, provider): + """ + :type provider: AbstractProvider + """ + return [ + ('identifier', provider.identifier), + ('uri', 'fuo://{}'.format(provider.identifier)), + ('name', provider.name), + ] class AppSerializerMixin: class Meta: - types = (App, ) + types = (App,) def _get_items(self, app): player = app.player @@ -33,7 +69,7 @@ def _get_items(self, app): ('state', player.state.name), ] if player.state in (State.playing, State.paused) and \ - player.current_song is not None: + player.current_song is not None: items.extend([ ('duration', player.duration), ('position', player.position), @@ -45,7 +81,7 @@ def _get_items(self, app): class DictLikeSerializerMixin: class Meta: - types = (Metadata, ) + types = (Metadata,) def _get_items(self, metadata): return [(key.value, value) for key, value in metadata.items()] @@ -67,10 +103,85 @@ class DictLikePythonSerializer(PythonSerializer, pass +class ProviderPythonSerializer(PythonSerializer, + SimpleSerializerMixin, + ProviderSerializerMixin, + metaclass=SerializerMeta): + pass + + +class SearchPythonSerializer(PythonSerializer, SearchSerializerMixin, + metaclass=SerializerMeta): + + def serialize(self, result): + list_serializer = PythonListSerializer() + json_ = {} + for field, value in self._get_items(result): + json_[field] = list_serializer.serialize(value) + return json_ + + # Plain serializers. # class AppPlainSerializer(PlainSerializer, AppSerializerMixin, SimpleSerializerMixin, metaclass=SerializerMeta): - pass + ... + + +class ProviderPlainSerializer(PlainSerializer, ProviderSerializerMixin, + metaclass=SerializerMeta): + + def __init__(self, **options): + super().__init__(**options) + self.opt_as_line = options.get('as_line', False) + self.opt_uri_length = options.get('uri_length', '') + + def serialize(self, provider): + """ + :type provider: AbstractProvider + """ + items = self._get_items(provider) + dict_ = dict(items) + uri = dict_['uri'] + name = dict_['name'] + if self.opt_as_line or self.opt_level > 0: + return '{uri:{uri_length}}\t# {name}'.format( + uri=uri, + name=name, + uri_length=self.opt_uri_length + ) + return self.serialize_items(items) + + +class SearchPlainSerializer(PlainSerializer, SearchSerializerMixin, + metaclass=SerializerMeta): + + def __init__(self, **options): + super().__init__(**options) + self.opt_uri_length = options.get('uri_length', '') + + def serialize(self, result): + items = self._get_items(result) + # when serialize SearchModel, we formatt it as one line when level > 1 + if self.opt_level >= 2: + return str(result) # I think we will never use this line format + text_list = [] + for field, value in items: + serializer = PlainListSerializer( + level=self.opt_level - 1, + fetch=False, + uri_length=self.opt_uri_length + ) + value_text = serializer.serialize(value) + text_list.append(value_text) + return '\n'.join(text_list) + + def calc_max_uri_length(self, result): + items = self._get_items(result) + uri_length = 0 + for field, value in items: + for each in value: + uri_length = max(uri_length, len(reverse(each))) + return uri_length diff --git a/feeluown/serializers/plain.py b/feeluown/serializers/plain.py index f6ecebea5c..1be015bc0f 100644 --- a/feeluown/serializers/plain.py +++ b/feeluown/serializers/plain.py @@ -1,16 +1,94 @@ from textwrap import indent # FIXME: maybe we should move `reverse` into serializers package -from feeluown.library import reverse from .base import Serializer, SerializerMeta, SerializerError -from .model_helpers import ModelSerializerMixin, SongSerializerMixin, \ - ArtistSerializerMixin, AlbumSerializerMixin, PlaylistSerializerMixin, \ - UserSerializerMixin, SearchSerializerMixin, ProviderSerializerMixin from ._plain_formatter import WideFormatter +from feeluown.library import ( + reverse, + BaseModel, + SongModel, + ArtistModel, + AlbumModel, + PlaylistModel, + UserModel, + BriefSongModel, + BriefArtistModel, + BriefAlbumModel, + BriefPlaylistModel, + BriefUserModel, +) + formatter = WideFormatter() fmt = formatter.format +class ModelSerializerMixin: + + def _get_items(self, model): + # initialize fields that need to be serialized + # if as_line option is set, we always use fields_display + if self.opt_as_line or self.opt_brief: + modelcls = type(model) + fields = [field for field in model.__fields__ + if field not in BaseModel.__fields__] + # Include properties. + pydantic_fields = ("__values__", "fields", "__fields_set__", + "model_computed_fields", "model_extra", + "model_fields_set") + fields += [prop for prop in dir(modelcls) + if isinstance(getattr(modelcls, prop), property) + and prop not in pydantic_fields] + else: + assert False, 'unreachable' + fields = self._declared_fields + items = [("provider", model.source), + ("identifier", str(model.identifier)), + ("uri", reverse(model))] + if self.opt_fetch: + for field in fields: + items.append((field, getattr(model, field))) + else: + for field in fields: + items.append((field, getattr(model, field + '_display'))) + return items + + +class SongSerializerMixin: + class Meta: + types = (SongModel, BriefSongModel) + # since url can be too long, we put it at last + fields = ('title', 'duration', 'album', 'artists') + line_fmt = '{uri:{uri_length}}\t# {title:_18} - {artists_name:_20}' + + +class ArtistSerializerMixin: + class Meta: + types = (ArtistModel, BriefArtistModel) + fields = ('name', 'songs') + line_fmt = '{uri:{uri_length}}\t# {name:_40}' + + +class AlbumSerializerMixin: + class Meta: + types = (AlbumModel, BriefAlbumModel) + fields = ('name', 'artists', 'songs') + line_fmt = '{uri:{uri_length}}\t# {name:_18} - {artists_name:_20}' + + +class PlaylistSerializerMixin: + class Meta: + types = (PlaylistModel, BriefPlaylistModel) + fields = ('name',) + line_fmt = '{uri:{uri_length}}\t# {name:_40}' + + +class UserSerializerMixin: + class Meta: + types = (UserModel, BriefUserModel) + fields = ('name', 'playlists') + line_fmt = '{uri:{uri_length}}\t# {name:_40}' + + class PlainSerializer(Serializer): """PlainSerializer base class""" _mapping = {} @@ -54,7 +132,7 @@ def __init__(self, **options): options.get('brief') is False, options.get('fetch') is False]): raise SerializerError( - "as_line, brief, fetch can't be false at same time") + "as_line, brief, fetch can't be false at same time") if options.get('as_line') is True and options.get('brief') is False: raise SerializerError( "brief can't be False when as_line is True") @@ -85,17 +163,20 @@ class ListSerializer(PlainSerializer, metaclass=SerializerMeta): SearchModel is an exception. """ + class Meta: - types = (list, ) + types = (list,) def serialize(self, list_): + from .objs import SearchPlainSerializer + if not list_: return '' item0 = list_[0] serializer_cls = PlainSerializer.get_serializer_cls(item0) level = self.opt_level + 1 if issubclass(serializer_cls, ModelSerializer): - if issubclass(serializer_cls, SearchSerializer): + if issubclass(serializer_cls, SearchPlainSerializer): return self.serialize_search_result_list(list_) uri_length = max(len(reverse(item)) for item in list_) serializer = serializer_cls(fetch=False, level=level, @@ -112,8 +193,14 @@ def serialize(self, list_): return '\n'.join(text_list) def serialize_search_result_list(self, list_): - serializer = SearchSerializer(level=self.opt_level + 1, - fetch=False, brief=True, as_line=False) + from .objs import SearchPlainSerializer + + serializer = SearchPlainSerializer( + level=self.opt_level + 1, + fetch=False, + brief=True, + as_line=False + ) # calculate max uri_length max_uri_length = 0 for model in list_: @@ -166,57 +253,3 @@ def serialize(self, object): elif object is False: return 'false' return str(object) - - -class ProviderSerializer(PlainSerializer, ProviderSerializerMixin, - metaclass=SerializerMeta): - - def __init__(self, **options): - super().__init__(**options) - self.opt_as_line = options.get('as_line', False) - self.opt_uri_length = options.get('uri_length', '') - - def serialize(self, provider): - """ - :type provider: AbstractProvider - """ - items = self._get_items(provider) - dict_ = dict(items) - uri = dict_['uri'] - name = dict_['name'] - if self.opt_as_line or self.opt_level > 0: - return '{uri:{uri_length}}\t# {name}'.format( - uri=uri, - name=name, - uri_length=self.opt_uri_length - ) - return self.serialize_items(items) - - -class SearchSerializer(PlainSerializer, SearchSerializerMixin, - metaclass=SerializerMeta): - - def __init__(self, **options): - super().__init__(**options) - self.opt_uri_length = options.get('uri_length', '') - - def serialize(self, result): - items = self._get_items(result) - # when serialize SearchModel, we formatt it as one line when level > 1 - if self.opt_level >= 2: - return str(result) # I think we will never use this line format - text_list = [] - for field, value in items: - serializer = ListSerializer(level=self.opt_level - 1, fetch=False, - uri_length=self.opt_uri_length) - value_text = serializer.serialize(value) - text_list.append(value_text) - return '\n'.join(text_list) - - def calc_max_uri_length(self, result): - items = self._get_items(result) - uri_length = 0 - for field, value in items: - for each in value: - uri_length = max(uri_length, len(reverse(each))) - return uri_length diff --git a/feeluown/serializers/python.py b/feeluown/serializers/python.py index 06d7e9a840..1a1125714e 100644 --- a/feeluown/serializers/python.py +++ b/feeluown/serializers/python.py @@ -1,10 +1,7 @@ -from .typename import attach_typename -from .base import Serializer, SerializerMeta, SerializerError, \ - SimpleSerializerMixin -from .model_helpers import ModelSerializerMixin, ProviderSerializerMixin, \ - SearchSerializerMixin +from feeluown.library import BaseModel, reverse -from feeluown.library import BaseModel +from .typename import attach_typename +from .base import Serializer, SerializerMeta class PythonSerializer(Serializer): @@ -12,13 +9,12 @@ class PythonSerializer(Serializer): def __init__(self, **options): super().__init__(**options) - self.opt_level = options.get('level', 0) def serialize_items(self, items): json_ = {} for key, value in items: serializer_cls = PythonSerializer.get_serializer_cls(value) - serializer = serializer_cls(brief=True, level=self.opt_level + 1) + serializer = serializer_cls() json_[key] = serializer.serialize(value) return json_ @@ -27,29 +23,37 @@ def serialize(self, obj): return super().serialize(obj) -class ModelSerializer(PythonSerializer, ModelSerializerMixin, metaclass=SerializerMeta): +class ModelSerializer(PythonSerializer, metaclass=SerializerMeta): class Meta: types = (BaseModel, ) def __init__(self, **options): - if options.get('brief') is False and options.get('fetch') is False: - raise SerializerError( - "fetch can't be False when brief is False") - super().__init__(**options) - self.opt_as_line = options.get('as_line', False) - self.opt_brief = options.get('brief', True) - self.opt_fetch = options.get('fetch', False) - if self.opt_brief is False: - self.opt_fetch = True + def _get_items(self, model): + modelcls = type(model) + fields = [field for field in model.__fields__ + if field not in BaseModel.__fields__] + # Include properties. + pydantic_fields = ("__values__", "fields", "__fields_set__", + "model_computed_fields", "model_extra", + "model_fields_set") + fields += [prop for prop in dir(modelcls) + if isinstance(getattr(modelcls, prop), property) + and prop not in pydantic_fields] + items = [("provider", model.source), + ("identifier", str(model.identifier)), + ("uri", reverse(model))] + for field in fields: + items.append((field, getattr(model, field))) + return items @attach_typename def serialize(self, model): dict_ = {} for field, value in self._get_items(model): serializer_cls = self.get_serializer_cls(value) - value_dict = serializer_cls(brief=True, fetch=False).serialize(value) + value_dict = serializer_cls().serialize(value) dict_[field] = value_dict return dict_ @@ -69,12 +73,13 @@ def serialize(self, list_): result = [] for item in list_: serializer_cls = PythonSerializer.get_serializer_cls(item) - serializer = serializer_cls(brief=True, fetch=False) + serializer = serializer_cls() result.append(serializer.serialize(item)) return result def serialize_search_result_list(self, list_): - serializer = SearchSerializer() + from .objs import SearchPythonSerializer + serializer = SearchPythonSerializer() return [serializer.serialize(model) for model in list_] @@ -89,21 +94,3 @@ class Meta: def serialize(self, object): return object - - -class ProviderSerializer(PythonSerializer, - SimpleSerializerMixin, - ProviderSerializerMixin, - metaclass=SerializerMeta): - pass - - -class SearchSerializer(PythonSerializer, SearchSerializerMixin, - metaclass=SerializerMeta): - - def serialize(self, result): - list_serializer = ListSerializer() - json_ = {} - for field, value in self._get_items(result): - json_[field] = list_serializer.serialize(value) - return json_ diff --git a/tests/serializers/test_serializers.py b/tests/serializers/test_serializers.py index fdf26bb045..6a24b33631 100644 --- a/tests/serializers/test_serializers.py +++ b/tests/serializers/test_serializers.py @@ -44,3 +44,4 @@ def test_serialize_search_result(): result = SimpleSearchResult(q='', songs=[song]) d = serialize('python', result) assert d['songs'][0]['identifier'] == '1' + serialize('plain', result) # should not raise error