Python3 Migrate
This commit is contained in:
361
venv/lib/python3.7/site-packages/mopidy/models/__init__.py
Normal file
361
venv/lib/python3.7/site-packages/mopidy/models/__init__.py
Normal file
@@ -0,0 +1,361 @@
|
||||
from mopidy.models import fields
|
||||
from mopidy.models.immutable import ImmutableObject, ValidatedImmutableObject
|
||||
from mopidy.models.serialize import ModelJSONEncoder, model_json_decoder
|
||||
|
||||
__all__ = [
|
||||
"ImmutableObject",
|
||||
"Ref",
|
||||
"Image",
|
||||
"Artist",
|
||||
"Album",
|
||||
"Track",
|
||||
"TlTrack",
|
||||
"Playlist",
|
||||
"SearchResult",
|
||||
"model_json_decoder",
|
||||
"ModelJSONEncoder",
|
||||
"ValidatedImmutableObject",
|
||||
]
|
||||
|
||||
|
||||
class Ref(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
Model to represent URI references with a human friendly name and type
|
||||
attached. This is intended for use a lightweight object "free" of metadata
|
||||
that can be passed around instead of using full blown models.
|
||||
|
||||
:param uri: object URI
|
||||
:type uri: string
|
||||
:param name: object name
|
||||
:type name: string
|
||||
:param type: object type
|
||||
:type type: string
|
||||
"""
|
||||
|
||||
#: The object URI. Read-only.
|
||||
uri = fields.URI()
|
||||
|
||||
#: The object name. Read-only.
|
||||
name = fields.String()
|
||||
|
||||
#: The object type, e.g. "artist", "album", "track", "playlist",
|
||||
#: "directory". Read-only.
|
||||
type = fields.Identifier() # TODO: consider locking this down.
|
||||
# type = fields.Field(choices=(ALBUM, ARTIST, DIRECTORY, PLAYLIST, TRACK))
|
||||
|
||||
#: Constant used for comparison with the :attr:`type` field.
|
||||
ALBUM = "album"
|
||||
|
||||
#: Constant used for comparison with the :attr:`type` field.
|
||||
ARTIST = "artist"
|
||||
|
||||
#: Constant used for comparison with the :attr:`type` field.
|
||||
DIRECTORY = "directory"
|
||||
|
||||
#: Constant used for comparison with the :attr:`type` field.
|
||||
PLAYLIST = "playlist"
|
||||
|
||||
#: Constant used for comparison with the :attr:`type` field.
|
||||
TRACK = "track"
|
||||
|
||||
@classmethod
|
||||
def album(cls, **kwargs):
|
||||
"""Create a :class:`Ref` with ``type`` :attr:`ALBUM`."""
|
||||
kwargs["type"] = Ref.ALBUM
|
||||
return cls(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def artist(cls, **kwargs):
|
||||
"""Create a :class:`Ref` with ``type`` :attr:`ARTIST`."""
|
||||
kwargs["type"] = Ref.ARTIST
|
||||
return cls(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def directory(cls, **kwargs):
|
||||
"""Create a :class:`Ref` with ``type`` :attr:`DIRECTORY`."""
|
||||
kwargs["type"] = Ref.DIRECTORY
|
||||
return cls(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def playlist(cls, **kwargs):
|
||||
"""Create a :class:`Ref` with ``type`` :attr:`PLAYLIST`."""
|
||||
kwargs["type"] = Ref.PLAYLIST
|
||||
return cls(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def track(cls, **kwargs):
|
||||
"""Create a :class:`Ref` with ``type`` :attr:`TRACK`."""
|
||||
kwargs["type"] = Ref.TRACK
|
||||
return cls(**kwargs)
|
||||
|
||||
|
||||
class Image(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
:param string uri: URI of the image
|
||||
:param int width: Optional width of image or :class:`None`
|
||||
:param int height: Optional height of image or :class:`None`
|
||||
"""
|
||||
|
||||
#: The image URI. Read-only.
|
||||
uri = fields.URI()
|
||||
|
||||
#: Optional width of the image or :class:`None`. Read-only.
|
||||
width = fields.Integer(min=0)
|
||||
|
||||
#: Optional height of the image or :class:`None`. Read-only.
|
||||
height = fields.Integer(min=0)
|
||||
|
||||
|
||||
class Artist(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
:param uri: artist URI
|
||||
:type uri: string
|
||||
:param name: artist name
|
||||
:type name: string
|
||||
:param sortname: artist name for sorting
|
||||
:type sortname: string
|
||||
:param musicbrainz_id: MusicBrainz ID
|
||||
:type musicbrainz_id: string
|
||||
"""
|
||||
|
||||
#: The artist URI. Read-only.
|
||||
uri = fields.URI()
|
||||
|
||||
#: The artist name. Read-only.
|
||||
name = fields.String()
|
||||
|
||||
#: Artist name for better sorting, e.g. with articles stripped
|
||||
sortname = fields.String()
|
||||
|
||||
#: The MusicBrainz ID of the artist. Read-only.
|
||||
musicbrainz_id = fields.Identifier()
|
||||
|
||||
|
||||
class Album(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
:param uri: album URI
|
||||
:type uri: string
|
||||
:param name: album name
|
||||
:type name: string
|
||||
:param artists: album artists
|
||||
:type artists: list of :class:`Artist`
|
||||
:param num_tracks: number of tracks in album
|
||||
:type num_tracks: integer or :class:`None` if unknown
|
||||
:param num_discs: number of discs in album
|
||||
:type num_discs: integer or :class:`None` if unknown
|
||||
:param date: album release date (YYYY or YYYY-MM-DD)
|
||||
:type date: string
|
||||
:param musicbrainz_id: MusicBrainz ID
|
||||
:type musicbrainz_id: string
|
||||
"""
|
||||
|
||||
#: The album URI. Read-only.
|
||||
uri = fields.URI()
|
||||
|
||||
#: The album name. Read-only.
|
||||
name = fields.String()
|
||||
|
||||
#: A set of album artists. Read-only.
|
||||
artists = fields.Collection(type=Artist, container=frozenset)
|
||||
|
||||
#: The number of tracks in the album. Read-only.
|
||||
num_tracks = fields.Integer(min=0)
|
||||
|
||||
#: The number of discs in the album. Read-only.
|
||||
num_discs = fields.Integer(min=0)
|
||||
|
||||
#: The album release date. Read-only.
|
||||
date = fields.Date()
|
||||
|
||||
#: The MusicBrainz ID of the album. Read-only.
|
||||
musicbrainz_id = fields.Identifier()
|
||||
|
||||
|
||||
class Track(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
:param uri: track URI
|
||||
:type uri: string
|
||||
:param name: track name
|
||||
:type name: string
|
||||
:param artists: track artists
|
||||
:type artists: list of :class:`Artist`
|
||||
:param album: track album
|
||||
:type album: :class:`Album`
|
||||
:param composers: track composers
|
||||
:type composers: list of :class:`Artist`
|
||||
:param performers: track performers
|
||||
:type performers: list of :class:`Artist`
|
||||
:param genre: track genre
|
||||
:type genre: string
|
||||
:param track_no: track number in album
|
||||
:type track_no: integer or :class:`None` if unknown
|
||||
:param disc_no: disc number in album
|
||||
:type disc_no: integer or :class:`None` if unknown
|
||||
:param date: track release date (YYYY or YYYY-MM-DD)
|
||||
:type date: string
|
||||
:param length: track length in milliseconds
|
||||
:type length: integer or :class:`None` if there is no duration
|
||||
:param bitrate: bitrate in kbit/s
|
||||
:type bitrate: integer
|
||||
:param comment: track comment
|
||||
:type comment: string
|
||||
:param musicbrainz_id: MusicBrainz ID
|
||||
:type musicbrainz_id: string
|
||||
:param last_modified: Represents last modification time
|
||||
:type last_modified: integer or :class:`None` if unknown
|
||||
"""
|
||||
|
||||
#: The track URI. Read-only.
|
||||
uri = fields.URI()
|
||||
|
||||
#: The track name. Read-only.
|
||||
name = fields.String()
|
||||
|
||||
#: A set of track artists. Read-only.
|
||||
artists = fields.Collection(type=Artist, container=frozenset)
|
||||
|
||||
#: The track :class:`Album`. Read-only.
|
||||
album = fields.Field(type=Album)
|
||||
|
||||
#: A set of track composers. Read-only.
|
||||
composers = fields.Collection(type=Artist, container=frozenset)
|
||||
|
||||
#: A set of track performers`. Read-only.
|
||||
performers = fields.Collection(type=Artist, container=frozenset)
|
||||
|
||||
#: The track genre. Read-only.
|
||||
genre = fields.String()
|
||||
|
||||
#: The track number in the album. Read-only.
|
||||
track_no = fields.Integer(min=0)
|
||||
|
||||
#: The disc number in the album. Read-only.
|
||||
disc_no = fields.Integer(min=0)
|
||||
|
||||
#: The track release date. Read-only.
|
||||
date = fields.Date()
|
||||
|
||||
#: The track length in milliseconds. Read-only.
|
||||
length = fields.Integer(min=0)
|
||||
|
||||
#: The track's bitrate in kbit/s. Read-only.
|
||||
bitrate = fields.Integer(min=0)
|
||||
|
||||
#: The track comment. Read-only.
|
||||
comment = fields.String()
|
||||
|
||||
#: The MusicBrainz ID of the track. Read-only.
|
||||
musicbrainz_id = fields.Identifier()
|
||||
|
||||
#: Integer representing when the track was last modified. Exact meaning
|
||||
#: depends on source of track. For local files this is the modification
|
||||
#: time in milliseconds since Unix epoch. For other backends it could be an
|
||||
#: equivalent timestamp or simply a version counter.
|
||||
last_modified = fields.Integer(min=0)
|
||||
|
||||
|
||||
class TlTrack(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
A tracklist track. Wraps a regular track and it's tracklist ID.
|
||||
|
||||
The use of :class:`TlTrack` allows the same track to appear multiple times
|
||||
in the tracklist.
|
||||
|
||||
This class also accepts it's parameters as positional arguments. Both
|
||||
arguments must be provided, and they must appear in the order they are
|
||||
listed here.
|
||||
|
||||
This class also supports iteration, so your extract its values like this::
|
||||
|
||||
(tlid, track) = tl_track
|
||||
|
||||
:param tlid: tracklist ID
|
||||
:type tlid: int
|
||||
:param track: the track
|
||||
:type track: :class:`Track`
|
||||
"""
|
||||
|
||||
#: The tracklist ID. Read-only.
|
||||
tlid = fields.Integer(min=0)
|
||||
|
||||
#: The track. Read-only.
|
||||
track = fields.Field(type=Track)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if len(args) == 2 and len(kwargs) == 0:
|
||||
kwargs["tlid"] = args[0]
|
||||
kwargs["track"] = args[1]
|
||||
args = []
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __iter__(self):
|
||||
return iter([self.tlid, self.track])
|
||||
|
||||
|
||||
class Playlist(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
:param uri: playlist URI
|
||||
:type uri: string
|
||||
:param name: playlist name
|
||||
:type name: string
|
||||
:param tracks: playlist's tracks
|
||||
:type tracks: list of :class:`Track` elements
|
||||
:param last_modified:
|
||||
playlist's modification time in milliseconds since Unix epoch
|
||||
:type last_modified: int
|
||||
"""
|
||||
|
||||
#: The playlist URI. Read-only.
|
||||
uri = fields.URI()
|
||||
|
||||
#: The playlist name. Read-only.
|
||||
name = fields.String()
|
||||
|
||||
#: The playlist's tracks. Read-only.
|
||||
tracks = fields.Collection(type=Track, container=tuple)
|
||||
|
||||
#: The playlist modification time in milliseconds since Unix epoch.
|
||||
#: Read-only.
|
||||
#:
|
||||
#: Integer, or :class:`None` if unknown.
|
||||
last_modified = fields.Integer(min=0)
|
||||
|
||||
# TODO: def insert(self, pos, track): ... ?
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
"""The number of tracks in the playlist. Read-only."""
|
||||
return len(self.tracks)
|
||||
|
||||
|
||||
class SearchResult(ValidatedImmutableObject):
|
||||
|
||||
"""
|
||||
:param uri: search result URI
|
||||
:type uri: string
|
||||
:param tracks: matching tracks
|
||||
:type tracks: list of :class:`Track` elements
|
||||
:param artists: matching artists
|
||||
:type artists: list of :class:`Artist` elements
|
||||
:param albums: matching albums
|
||||
:type albums: list of :class:`Album` elements
|
||||
"""
|
||||
|
||||
#: The search result URI. Read-only.
|
||||
uri = fields.URI()
|
||||
|
||||
#: The tracks matching the search query. Read-only.
|
||||
tracks = fields.Collection(type=Track, container=tuple)
|
||||
|
||||
#: The artists matching the search query. Read-only.
|
||||
artists = fields.Collection(type=Artist, container=tuple)
|
||||
|
||||
#: The albums matching the search query. Read-only.
|
||||
albums = fields.Collection(type=Album, container=tuple)
|
||||
179
venv/lib/python3.7/site-packages/mopidy/models/fields.py
Normal file
179
venv/lib/python3.7/site-packages/mopidy/models/fields.py
Normal file
@@ -0,0 +1,179 @@
|
||||
import sys
|
||||
|
||||
|
||||
class Field:
|
||||
|
||||
"""
|
||||
Base field for use in
|
||||
:class:`~mopidy.models.immutable.ValidatedImmutableObject`. These fields
|
||||
are responsible for type checking and other data sanitation in our models.
|
||||
|
||||
For simplicity fields use the Python descriptor protocol to store the
|
||||
values in the instance dictionary. Also note that fields are mutable if
|
||||
the object they are attached to allow it.
|
||||
|
||||
Default values will be validated with the exception of :class:`None`.
|
||||
|
||||
:param default: default value for field
|
||||
:param type: if set the field value must be of this type
|
||||
:param choices: if set the field value must be one of these
|
||||
"""
|
||||
|
||||
def __init__(self, default=None, type=None, choices=None):
|
||||
self._name = None # Set by ValidatedImmutableObjectMeta
|
||||
self._choices = choices
|
||||
self._default = default
|
||||
self._type = type
|
||||
|
||||
if self._default is not None:
|
||||
self.validate(self._default)
|
||||
|
||||
def validate(self, value):
|
||||
"""Validate and possibly modify the field value before assignment"""
|
||||
if self._type and not isinstance(value, self._type):
|
||||
raise TypeError(
|
||||
f"Expected {self._name} to be a {self._type}, not {value!r}"
|
||||
)
|
||||
if self._choices and value not in self._choices:
|
||||
raise TypeError(
|
||||
f"Expected {self._name} to be a one of {self._choices}, not {value!r}"
|
||||
)
|
||||
return value
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if not instance:
|
||||
return self
|
||||
return getattr(instance, "_" + self._name, self._default)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
if value is not None:
|
||||
value = self.validate(value)
|
||||
|
||||
if value is None or value == self._default:
|
||||
self.__delete__(instance)
|
||||
else:
|
||||
setattr(instance, "_" + self._name, value)
|
||||
|
||||
def __delete__(self, instance):
|
||||
if hasattr(instance, "_" + self._name):
|
||||
delattr(instance, "_" + self._name)
|
||||
|
||||
|
||||
class String(Field):
|
||||
|
||||
"""
|
||||
Specialized :class:`Field` which is wired up for bytes and unicode.
|
||||
|
||||
:param default: default value for field
|
||||
"""
|
||||
|
||||
def __init__(self, default=None):
|
||||
# TODO: normalize to unicode?
|
||||
# TODO: only allow unicode?
|
||||
# TODO: disallow empty strings?
|
||||
super().__init__(type=str, default=default)
|
||||
|
||||
|
||||
class Date(String):
|
||||
"""
|
||||
:class:`Field` for storing ISO 8601 dates as a string.
|
||||
|
||||
Supported formats are ``YYYY-MM-DD``, ``YYYY-MM`` and ``YYYY``, currently
|
||||
not validated.
|
||||
|
||||
:param default: default value for field
|
||||
"""
|
||||
|
||||
pass # TODO: make this check for YYYY-MM-DD, YYYY-MM, YYYY using strptime.
|
||||
|
||||
|
||||
class Identifier(String):
|
||||
"""
|
||||
:class:`Field` for storing values such as GUIDs or other identifiers.
|
||||
|
||||
Values will be interned.
|
||||
|
||||
:param default: default value for field
|
||||
"""
|
||||
|
||||
def validate(self, value):
|
||||
value = super().validate(value)
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode()
|
||||
return sys.intern(value)
|
||||
|
||||
|
||||
class URI(Identifier):
|
||||
"""
|
||||
:class:`Field` for storing URIs
|
||||
|
||||
Values will be interned, currently not validated.
|
||||
|
||||
:param default: default value for field
|
||||
"""
|
||||
|
||||
pass # TODO: validate URIs?
|
||||
|
||||
|
||||
class Integer(Field):
|
||||
"""
|
||||
:class:`Field` for storing integer numbers.
|
||||
|
||||
:param default: default value for field
|
||||
:param min: field value must be larger or equal to this value when set
|
||||
:param max: field value must be smaller or equal to this value when set
|
||||
"""
|
||||
|
||||
def __init__(self, default=None, min=None, max=None):
|
||||
self._min = min
|
||||
self._max = max
|
||||
super().__init__(type=int, default=default)
|
||||
|
||||
def validate(self, value):
|
||||
value = super().validate(value)
|
||||
if self._min is not None and value < self._min:
|
||||
raise ValueError(
|
||||
f"Expected {self._name} to be at least {self._min}, not {value:d}"
|
||||
)
|
||||
if self._max is not None and value > self._max:
|
||||
raise ValueError(
|
||||
f"Expected {self._name} to be at most {self._max}, not {value:d}"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class Boolean(Field):
|
||||
"""
|
||||
:class:`Field` for storing boolean values
|
||||
|
||||
:param default: default value for field
|
||||
"""
|
||||
|
||||
def __init__(self, default=None):
|
||||
super().__init__(type=bool, default=default)
|
||||
|
||||
|
||||
class Collection(Field):
|
||||
"""
|
||||
:class:`Field` for storing collections of a given type.
|
||||
|
||||
:param type: all items stored in the collection must be of this type
|
||||
:param container: the type to store the items in
|
||||
"""
|
||||
|
||||
def __init__(self, type, container=tuple):
|
||||
super().__init__(type=type, default=container())
|
||||
|
||||
def validate(self, value):
|
||||
if isinstance(value, str):
|
||||
raise TypeError(
|
||||
f"Expected {self._name} to be a collection of "
|
||||
f"{self._type.__name__}, not {value!r}"
|
||||
)
|
||||
for v in value:
|
||||
if not isinstance(v, self._type):
|
||||
raise TypeError(
|
||||
f"Expected {self._name} to be a collection of "
|
||||
f"{self._type.__name__}, not {value!r}"
|
||||
)
|
||||
return self._default.__class__(value) or None
|
||||
219
venv/lib/python3.7/site-packages/mopidy/models/immutable.py
Normal file
219
venv/lib/python3.7/site-packages/mopidy/models/immutable.py
Normal file
@@ -0,0 +1,219 @@
|
||||
import copy
|
||||
import itertools
|
||||
import weakref
|
||||
|
||||
from mopidy.models.fields import Field
|
||||
|
||||
# Registered models for automatic deserialization
|
||||
_models = {}
|
||||
|
||||
|
||||
class ImmutableObject:
|
||||
"""
|
||||
Superclass for immutable objects whose fields can only be modified via the
|
||||
constructor.
|
||||
|
||||
This version of this class has been retained to avoid breaking any clients
|
||||
relying on it's behavior. Internally in Mopidy we now use
|
||||
:class:`ValidatedImmutableObject` for type safety and it's much smaller
|
||||
memory footprint.
|
||||
|
||||
:param kwargs: kwargs to set as fields on the object
|
||||
:type kwargs: any
|
||||
"""
|
||||
|
||||
# Any sub-classes that don't set slots won't be effected by the base using
|
||||
# slots as they will still get an instance dict.
|
||||
__slots__ = ["__weakref__"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
if not self._is_valid_field(key):
|
||||
raise TypeError(
|
||||
f"__init__() got an unexpected keyword argument {key!r}"
|
||||
)
|
||||
self._set_field(key, value)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name.startswith("_"):
|
||||
object.__setattr__(self, name, value)
|
||||
else:
|
||||
raise AttributeError("Object is immutable.")
|
||||
|
||||
def __delattr__(self, name):
|
||||
if name.startswith("_"):
|
||||
object.__delattr__(self, name)
|
||||
else:
|
||||
raise AttributeError("Object is immutable.")
|
||||
|
||||
def _is_valid_field(self, name):
|
||||
return hasattr(self, name) and not callable(getattr(self, name))
|
||||
|
||||
def _set_field(self, name, value):
|
||||
if value == getattr(self.__class__, name):
|
||||
self.__dict__.pop(name, None)
|
||||
else:
|
||||
self.__dict__[name] = value
|
||||
|
||||
def _items(self):
|
||||
return self.__dict__.items()
|
||||
|
||||
def __repr__(self):
|
||||
kwarg_pairs = []
|
||||
for key, value in sorted(self._items()):
|
||||
if isinstance(value, (frozenset, tuple)):
|
||||
if not value:
|
||||
continue
|
||||
value = list(value)
|
||||
kwarg_pairs.append(f"{key}={value!r}")
|
||||
return f"{self.__class__.__name__}({', '.join(kwarg_pairs)})"
|
||||
|
||||
def __hash__(self):
|
||||
hash_sum = 0
|
||||
for key, value in self._items():
|
||||
hash_sum += hash(key) + hash(value)
|
||||
return hash_sum
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
return all(
|
||||
a == b
|
||||
for a, b in itertools.zip_longest(
|
||||
self._items(), other._items(), fillvalue=object()
|
||||
)
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def replace(self, **kwargs):
|
||||
"""
|
||||
Replace the fields in the model and return a new instance
|
||||
|
||||
Examples::
|
||||
|
||||
# Returns a track with a new name
|
||||
Track(name='foo').replace(name='bar')
|
||||
# Return an album with a new number of tracks
|
||||
Album(num_tracks=2).replace(num_tracks=5)
|
||||
|
||||
:param kwargs: kwargs to set as fields on the object
|
||||
:type kwargs: any
|
||||
:rtype: instance of the model with replaced fields
|
||||
"""
|
||||
other = copy.copy(self)
|
||||
for key, value in kwargs.items():
|
||||
if not self._is_valid_field(key):
|
||||
raise TypeError(
|
||||
f"replace() got an unexpected keyword argument {key!r}"
|
||||
)
|
||||
other._set_field(key, value)
|
||||
return other
|
||||
|
||||
def serialize(self):
|
||||
data = {}
|
||||
data["__model__"] = self.__class__.__name__
|
||||
for key, value in self._items():
|
||||
if isinstance(value, (set, frozenset, list, tuple)):
|
||||
value = [
|
||||
v.serialize() if isinstance(v, ImmutableObject) else v
|
||||
for v in value
|
||||
]
|
||||
elif isinstance(value, ImmutableObject):
|
||||
value = value.serialize()
|
||||
if not (isinstance(value, list) and len(value) == 0):
|
||||
data[key] = value
|
||||
return data
|
||||
|
||||
|
||||
class _ValidatedImmutableObjectMeta(type):
|
||||
|
||||
"""Helper that initializes fields, slots and memoizes instance creation."""
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
fields = {}
|
||||
|
||||
for base in bases: # Copy parent fields over to our state
|
||||
fields.update(getattr(base, "_fields", {}))
|
||||
|
||||
for key, value in attrs.items(): # Add our own fields
|
||||
if isinstance(value, Field):
|
||||
fields[key] = "_" + key
|
||||
value._name = key
|
||||
|
||||
attrs["_fields"] = fields
|
||||
attrs["_instances"] = weakref.WeakValueDictionary()
|
||||
attrs["__slots__"] = list(attrs.get("__slots__", [])) + list(
|
||||
fields.values()
|
||||
)
|
||||
|
||||
clsc = super().__new__(cls, name, bases, attrs)
|
||||
|
||||
if clsc.__name__ != "ValidatedImmutableObject":
|
||||
_models[clsc.__name__] = clsc
|
||||
|
||||
return clsc
|
||||
|
||||
def __call__(cls, *args, **kwargs): # noqa: N805
|
||||
instance = super().__call__(*args, **kwargs)
|
||||
return cls._instances.setdefault(weakref.ref(instance), instance)
|
||||
|
||||
|
||||
class ValidatedImmutableObject(
|
||||
ImmutableObject, metaclass=_ValidatedImmutableObjectMeta
|
||||
):
|
||||
"""
|
||||
Superclass for immutable objects whose fields can only be modified via the
|
||||
constructor. Fields should be :class:`Field` instances to ensure type
|
||||
safety in our models.
|
||||
|
||||
Note that since these models can not be changed, we heavily memoize them
|
||||
to save memory. So constructing a class with the same arguments twice will
|
||||
give you the same instance twice.
|
||||
"""
|
||||
|
||||
__slots__ = ["_hash"]
|
||||
|
||||
def __hash__(self):
|
||||
if not hasattr(self, "_hash"):
|
||||
hash_sum = super().__hash__()
|
||||
object.__setattr__(self, "_hash", hash_sum)
|
||||
return self._hash
|
||||
|
||||
def _is_valid_field(self, name):
|
||||
return name in self._fields
|
||||
|
||||
def _set_field(self, name, value):
|
||||
object.__setattr__(self, name, value)
|
||||
|
||||
def _items(self):
|
||||
for field, key in self._fields.items():
|
||||
if hasattr(self, key):
|
||||
yield field, getattr(self, key)
|
||||
|
||||
def replace(self, **kwargs):
|
||||
"""
|
||||
Replace the fields in the model and return a new instance
|
||||
|
||||
Examples::
|
||||
|
||||
# Returns a track with a new name
|
||||
Track(name='foo').replace(name='bar')
|
||||
# Return an album with a new number of tracks
|
||||
Album(num_tracks=2).replace(num_tracks=5)
|
||||
|
||||
Note that internally we memoize heavily to keep memory usage down given
|
||||
our overly repetitive data structures. So you might get an existing
|
||||
instance if it contains the same values.
|
||||
|
||||
:param kwargs: kwargs to set as fields on the object
|
||||
:type kwargs: any
|
||||
:rtype: instance of the model with replaced fields
|
||||
"""
|
||||
if not kwargs:
|
||||
return self
|
||||
other = super().replace(**kwargs)
|
||||
if hasattr(self, "_hash"):
|
||||
object.__delattr__(other, "_hash")
|
||||
return self._instances.setdefault(weakref.ref(other), other)
|
||||
43
venv/lib/python3.7/site-packages/mopidy/models/serialize.py
Normal file
43
venv/lib/python3.7/site-packages/mopidy/models/serialize.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import json
|
||||
|
||||
from mopidy.models import immutable
|
||||
|
||||
|
||||
class ModelJSONEncoder(json.JSONEncoder):
|
||||
|
||||
"""
|
||||
Automatically serialize Mopidy models to JSON.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> import json
|
||||
>>> json.dumps({'a_track': Track(name='name')}, cls=ModelJSONEncoder)
|
||||
'{"a_track": {"__model__": "Track", "name": "name"}}'
|
||||
|
||||
"""
|
||||
|
||||
def default(self, obj):
|
||||
if isinstance(obj, immutable.ImmutableObject):
|
||||
return obj.serialize()
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
def model_json_decoder(dct):
|
||||
"""
|
||||
Automatically deserialize Mopidy models from JSON.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> import json
|
||||
>>> json.loads(
|
||||
... '{"a_track": {"__model__": "Track", "name": "name"}}',
|
||||
... object_hook=model_json_decoder)
|
||||
{u'a_track': Track(artists=[], name=u'name')}
|
||||
|
||||
"""
|
||||
if "__model__" in dct:
|
||||
model_name = dct.pop("__model__")
|
||||
if model_name in immutable._models:
|
||||
cls = immutable._models[model_name]
|
||||
return cls(**dct)
|
||||
return dct
|
||||
Reference in New Issue
Block a user