diff --git a/mopidy/models.py b/mopidy/models.py index 52af300d..817b5ae3 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -128,12 +128,15 @@ class ImmutableObject(object): """ Superclass for immutable objects whose fields can only be modified via the - constructor. + constructor. Fields should be :class:`Field` instances to ensure type + safety in our models. :param kwargs: kwargs to set as fields on the object :type kwargs: any """ + __metaclass__ = FieldOwner + def __init__(self, *args, **kwargs): for key, value in kwargs.items(): if not hasattr(self, key) or callable(getattr(self, key)): @@ -142,7 +145,7 @@ class ImmutableObject(object): key) if value == getattr(self, key): continue # Don't explicitly set default values - self.__dict__[key] = value + super(ImmutableObject, self).__setattr__(key, value) def __setattr__(self, name, value): if name.startswith('_'): @@ -192,7 +195,7 @@ class ImmutableObject(object): :type values: dict :rtype: new instance of the model being copied """ - data = {} + data = {} # TODO: do we need public key handling now? for key in self.__dict__.keys(): public_key = key.lstrip('_') value = values.pop(public_key, self.__dict__[key]) @@ -207,7 +210,7 @@ class ImmutableObject(object): return self.__class__(**data) def serialize(self): - data = {} + data = {} # TODO: do we need public key handling now? data['__model__'] = self.__class__.__name__ for key in self.__dict__.keys(): public_key = key.lstrip('_') @@ -282,14 +285,10 @@ class Ref(ImmutableObject): """ #: The object URI. Read-only. - uri = None + uri = String() #: The object name. Read-only. - name = None - - #: The object type, e.g. "artist", "album", "track", "playlist", - #: "directory". Read-only. - type = None + name = String() #: Constant used for comparison with the :attr:`type` field. ALBUM = 'album' @@ -306,6 +305,10 @@ class Ref(ImmutableObject): #: Constant used for comparison with the :attr:`type` field. TRACK = 'track' + #: The object type, e.g. "artist", "album", "track", "playlist", + #: "directory". Read-only. + type = Field(choices=(ALBUM, ARTIST, DIRECTORY, PLAYLIST, TRACK)) + @classmethod def album(cls, **kwargs): """Create a :class:`Ref` with ``type`` :attr:`ALBUM`.""" @@ -346,13 +349,13 @@ class Image(ImmutableObject): """ #: The image URI. Read-only. - uri = None + uri = String() #: Optional width of the image or :class:`None`. Read-only. - width = None + width = Integer(min=0) #: Optional height of the image or :class:`None`. Read-only. - height = None + height = Integer(min=0) class Artist(ImmutableObject): @@ -367,13 +370,13 @@ class Artist(ImmutableObject): """ #: The artist URI. Read-only. - uri = None + uri = String() #: The artist name. Read-only. - name = None + name = String() #: The MusicBrainz ID of the artist. Read-only. - musicbrainz_id = None + musicbrainz_id = String() class Album(ImmutableObject): @@ -398,37 +401,32 @@ class Album(ImmutableObject): """ #: The album URI. Read-only. - uri = None + uri = String() #: The album name. Read-only. - name = None + name = String() #: A set of album artists. Read-only. - artists = frozenset() + artists = Collection(type=Artist, container=frozenset) #: The number of tracks in the album. Read-only. - num_tracks = None + num_tracks = Integer(min=0) #: The number of discs in the album. Read-only. - num_discs = None + num_discs = Integer(min=0) #: The album release date. Read-only. - date = None + date = String() # TODO: add date type #: The MusicBrainz ID of the album. Read-only. - musicbrainz_id = None + musicbrainz_id = String() #: The album image URIs. Read-only. - images = frozenset() + images = Collection(type=basestring, container=frozenset) # XXX If we want to keep the order of images we shouldn't use frozenset() # as it doesn't preserve order. I'm deferring this issue until we got # actual usage of this field with more than one image. - def __init__(self, *args, **kwargs): - self.__dict__['artists'] = frozenset(kwargs.pop('artists', None) or []) - self.__dict__['images'] = frozenset(kwargs.pop('images', None) or []) - super(Album, self).__init__(*args, **kwargs) - class Track(ImmutableObject): @@ -466,61 +464,52 @@ class Track(ImmutableObject): """ #: The track URI. Read-only. - uri = None + uri = String() #: The track name. Read-only. - name = None + name = String() #: A set of track artists. Read-only. - artists = frozenset() + artists = Collection(type=Artist, container=frozenset) #: The track :class:`Album`. Read-only. - album = None + album = Field(type=Album) #: A set of track composers. Read-only. - composers = frozenset() + composers = Collection(type=Artist, container=frozenset) #: A set of track performers`. Read-only. - performers = frozenset() + performers = Collection(type=Artist, container=frozenset) #: The track genre. Read-only. - genre = None + genre = String() #: The track number in the album. Read-only. - track_no = None + track_no = Integer(min=0) #: The disc number in the album. Read-only. - disc_no = None + disc_no = Integer(min=0) #: The track release date. Read-only. - date = None + date = String() # TODO: add date type #: The track length in milliseconds. Read-only. - length = None + length = Integer(min=0) #: The track's bitrate in kbit/s. Read-only. - bitrate = None + bitrate = Integer(min=0) #: The track comment. Read-only. - comment = None + comment = String() #: The MusicBrainz ID of the track. Read-only. - musicbrainz_id = None + musicbrainz_id = String() #: 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 = None - - def __init__(self, *args, **kwargs): - def get(key): - return frozenset(kwargs.pop(key, None) or []) - - self.__dict__['artists'] = get('artists') - self.__dict__['composers'] = get('composers') - self.__dict__['performers'] = get('performers') - super(Track, self).__init__(*args, **kwargs) + last_modified = Integer(min=0) class TlTrack(ImmutableObject): @@ -546,10 +535,10 @@ class TlTrack(ImmutableObject): """ #: The tracklist ID. Read-only. - tlid = None + tlid = Integer(min=0) #: The track. Read-only. - track = None + track = Field(type=Track) def __init__(self, *args, **kwargs): if len(args) == 2 and len(kwargs) == 0: @@ -577,23 +566,19 @@ class Playlist(ImmutableObject): """ #: The playlist URI. Read-only. - uri = None + uri = String() #: The playlist name. Read-only. - name = None + name = String() #: The playlist's tracks. Read-only. - tracks = tuple() + tracks = Collection(type=Track, container=tuple) #: The playlist modification time in milliseconds since Unix epoch. #: Read-only. #: #: Integer, or :class:`None` if unknown. - last_modified = None - - def __init__(self, *args, **kwargs): - self.__dict__['tracks'] = tuple(kwargs.pop('tracks', None) or []) - super(Playlist, self).__init__(*args, **kwargs) + last_modified = Integer(min=0) # TODO: def insert(self, pos, track): ... ? @@ -617,19 +602,13 @@ class SearchResult(ImmutableObject): """ # The search result URI. Read-only. - uri = None + uri = String() # The tracks matching the search query. Read-only. - tracks = tuple() + tracks = Collection(type=Track, container=tuple) # The artists matching the search query. Read-only. - artists = tuple() + artists = Collection(type=Artist, container=tuple) # The albums matching the search query. Read-only. - albums = tuple() - - def __init__(self, *args, **kwargs): - self.__dict__['tracks'] = tuple(kwargs.pop('tracks', None) or []) - self.__dict__['artists'] = tuple(kwargs.pop('artists', None) or []) - self.__dict__['albums'] = tuple(kwargs.pop('albums', None) or []) - super(SearchResult, self).__init__(*args, **kwargs) + albums = Collection(type=Album, container=tuple)