Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

serde: add deserializer and remove useless code #869

Merged
merged 5 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions feeluown/library/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@
import time
from typing import List, Optional, Tuple, Any, Union

from pydantic import ConfigDict, BaseModel as _BaseModel, PrivateAttr
from pydantic import (
ConfigDict, BaseModel as _BaseModel, PrivateAttr,
model_validator, model_serializer,
)

try:
# pydantic>=2.0
from pydantic import field_validator
Expand Down Expand Up @@ -215,6 +219,29 @@ def __getattr__(self, attr):
return getattr(self, attr[:-8])
raise

@model_validator(mode='before')
def _deserialize(cls, value):
if isinstance(value, dict):
js = value
if 'provider' in js:
js['source'] = js.pop('provider', None)
js.pop('uri', None)
js.pop('__type__', None)
return js
return value

@model_serializer(mode='wrap')
def _serialize(self, f):
from feeluown.library import reverse

js = f(self)
js.pop('meta')
js.pop('state')
js['provider'] = js['source']
js['uri'] = reverse(self)
js['__type__'] = f'feeluown.library.{self.__class__.__name__}'
return js


class BaseBriefModel(BaseModel):
"""
Expand Down Expand Up @@ -279,7 +306,7 @@ class SongModel(BriefSongModel, BaseNormalModel):
meta: Any = ModelMeta.create(ModelType.song, is_normal=True)
title: str
album: Optional[TAlbum] = None
artists: List[BriefArtistModel]
artists: List[TArtist]
duration: int # milliseconds
# A playlist can consist of multiple songs and a song can have many children.
# The differences between playlist's songs and song' children is that
Expand Down
44 changes: 31 additions & 13 deletions feeluown/serializers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .base import SerializerError
from .base import SerializerError, DeserializerError

# format Serializer mapping, like::
#
Expand All @@ -7,27 +7,40 @@
# 'plain': PlainSerializer
# }
_MAPPING = {}
_DE_MAPPING = {}


def register_serializer(type_, serializer_cls):
_MAPPING[type_] = serializer_cls


def _load_serializers():
register_serializer('plain', PlainSerializer)
register_serializer('json', JsonSerializer)
register_serializer('python', PythonSerializer)
def register_deserializer(type_, deserializer_cls):
_DE_MAPPING[type_] = deserializer_cls


def get_serializer(format):
def get_serializer(format_):
global _MAPPING

if not _MAPPING:
_load_serializers()
if format not in _MAPPING:
raise SerializerError("Serializer for format:{} not found".format(format))
return _MAPPING.get(format)
register_serializer('plain', PlainSerializer)
register_serializer('json', JsonSerializer)
register_serializer('python', PythonSerializer)
if format_ not in _MAPPING:
raise SerializerError(f"Serializer for format:{format_} not found")
return _MAPPING.get(format_)


def get_deserializer(format_: str):
global _DE_MAPPING

if not _DE_MAPPING:
register_deserializer('python', PythonDeserializer)
if format_ not in _DE_MAPPING:
raise DeserializerError(f"Deserializer for format:{format_} not found")
return _DE_MAPPING[format_]

def serialize(format, obj, **options):

def serialize(format_, obj, **options):
"""serialize python object defined in feeluown package

:raises SerializerError:
Expand All @@ -40,12 +53,17 @@ def serialize(format, obj, **options):
serialize('json', songs, indent=4, fetch=True)
serialize('json', providers)
"""
serializer = get_serializer(format)(**options)
serializer = get_serializer(format_)(**options)
return serializer.serialize(obj)


def deserialize(format_, obj, **options):
deserializer = get_deserializer(format_)(**options)
return deserializer.deserialize(obj)


from .base import SerializerMeta, SimpleSerializerMixin # noqa
from .plain import PlainSerializer # noqa
from .json_ import JsonSerializer # noqa
from .python import PythonSerializer # noqa
from .python import PythonSerializer, PythonDeserializer # noqa
from .objs import * # noqa
49 changes: 48 additions & 1 deletion feeluown/serializers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ class SerializerError(Exception):
pass


class DeserializerError(Exception):
"""
this error will be raised when

* Deserializer initialization failed
* Deserializer not found
* Deserializer serialization failed
"""
pass


class Serializer:
"""Serializer abstract base class

Expand Down Expand Up @@ -62,7 +73,43 @@ def get_serializer_cls(cls, model):
# FIXME: remove me when model v2 has its own serializer
if isinstance(model, model_cls):
return serialize_cls
raise SerializerError("no serializer for {}".format(model))
raise SerializerError(f"no serializer for {type(model)}")


class Deserializer:
def __init__(self, **options):
"""
Subclass should validate and parse options by themselves, here,
we list three commonly used options.

as_line is a *format* option:

- as_line: line format of a object (mainly designed for PlainSerializer)

brief and fetch are *representation* options:

- brief: a minimum human readable representation of the object.
we hope that people can *identify which object it is* through
this representation.
For example, if an object has ten attributes, this representation
may only contain three attributes.

- fetch: if this option is specified, the attribute value
should be authoritative.
"""
self.options = copy.deepcopy(options)

def deserialize(self, obj):
deserializer_cls = self.get_deserializer_cls(obj)
return deserializer_cls(**self.options).deserialize(obj)

@classmethod
def get_deserializer_cls(cls, model):
for model_cls, serialize_cls in cls._mapping.items():
# FIXME: remove me when model v2 has its own serializer
if isinstance(model, model_cls):
return serialize_cls
raise SerializerError(f"no serializer for {type(model)}")


class SerializerMeta(type):
Expand Down
120 changes: 0 additions & 120 deletions feeluown/serializers/model_helpers.py

This file was deleted.

Loading
Loading