From f237736f877031a36b1126425b5e937749b1f0fa Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 18 Nov 2012 16:27:04 +0100 Subject: [PATCH 1/7] models: Add '__type__' attribute to serialized models --- mopidy/models.py | 1 + tests/models_test.py | 25 +++++++++++++++---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index 17616f9d..6a2938ad 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -78,6 +78,7 @@ class ImmutableObject(object): def serialize(self): data = {} + data['__type__'] = self.__class__.__name__ for key in self.__dict__.keys(): public_key = key.lstrip('_') value = self.__dict__[key] diff --git a/tests/models_test.py b/tests/models_test.py index 6c1520cb..5fbd4dd3 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -83,7 +83,7 @@ class ArtistTest(unittest.TestCase): def test_serialize(self): self.assertDictEqual( - {'uri': 'uri', 'name': 'name'}, + {'__type__': 'Artist', 'uri': 'uri', 'name': 'name'}, Artist(uri='uri', name='name').serialize()) def test_eq_name(self): @@ -195,13 +195,14 @@ class AlbumTest(unittest.TestCase): def test_serialize_without_artists(self): self.assertDictEqual( - {'uri': 'uri', 'name': 'name'}, + {'__type__': 'Album', 'uri': 'uri', 'name': 'name'}, Album(uri='uri', name='name').serialize()) def test_serialize_with_artists(self): artist = Artist(name='foo') self.assertDictEqual( - {'uri': 'uri', 'name': 'name', 'artists': [artist.serialize()]}, + {'__type__': 'Album', 'uri': 'uri', 'name': 'name', 'artists': + [artist.serialize()]}, Album(uri='uri', name='name', artists=[artist]).serialize()) def test_eq_name(self): @@ -386,19 +387,21 @@ class TrackTest(unittest.TestCase): def test_serialize_without_artists(self): self.assertDictEqual( - {'uri': 'uri', 'name': 'name'}, + {'__type__': 'Track', 'uri': 'uri', 'name': 'name'}, Track(uri='uri', name='name').serialize()) def test_serialize_with_artists(self): artist = Artist(name='foo') self.assertDictEqual( - {'uri': 'uri', 'name': 'name', 'artists': [artist.serialize()]}, + {'__type__': 'Track', 'uri': 'uri', 'name': 'name', + 'artists': [artist.serialize()]}, Track(uri='uri', name='name', artists=[artist]).serialize()) def test_serialize_with_album(self): album = Album(name='foo') self.assertDictEqual( - {'uri': 'uri', 'name': 'name', 'album': album.serialize()}, + {'__type__': 'Track', 'uri': 'uri', 'name': 'name', + 'album': album.serialize()}, Track(uri='uri', name='name', album=album).serialize()) def test_eq_uri(self): @@ -590,9 +593,10 @@ class TlTrackTest(unittest.TestCase): repr(TlTrack(tlid=123, track=Track(uri='uri')))) def test_serialize(self): + track = Track(uri='uri', name='name') self.assertDictEqual( - {'tlid': 123, 'track': {'uri': 'uri', 'name': 'name'}}, - TlTrack(tlid=123, track=Track(uri='uri', name='name')).serialize()) + {'__type__': 'TlTrack', 'tlid': 123, 'track': track.serialize()}, + TlTrack(tlid=123, track=track).serialize()) def test_eq(self): tlid = 123 @@ -719,13 +723,14 @@ class PlaylistTest(unittest.TestCase): def test_serialize_without_tracks(self): self.assertDictEqual( - {'uri': 'uri', 'name': 'name'}, + {'__type__': 'Playlist', 'uri': 'uri', 'name': 'name'}, Playlist(uri='uri', name='name').serialize()) def test_serialize_with_tracks(self): track = Track(name='foo') self.assertDictEqual( - {'uri': 'uri', 'name': 'name', 'tracks': [track.serialize()]}, + {'__type__': 'Playlist', 'uri': 'uri', 'name': 'name', + 'tracks': [track.serialize()]}, Playlist(uri='uri', name='name', tracks=[track]).serialize()) def test_eq_name(self): From 68e4b207cb99be65df7d096691ce2f07b5aba8b3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 18 Nov 2012 16:50:17 +0100 Subject: [PATCH 2/7] models: Support automatic serialization to and deserialization from JSON --- mopidy/models.py | 45 ++++++++++++++++++++++++++++++++++++++++++++ tests/models_test.py | 37 +++++++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/mopidy/models.py b/mopidy/models.py index 6a2938ad..9eadb314 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import json + class ImmutableObject(object): """ @@ -91,6 +93,49 @@ class ImmutableObject(object): return data +class ModelJSONEncoder(json.JSONEncoder): + """ + Automatically serialize Mopidy models to JSON. + + Usage:: + + >>> import json + >>> json.dumps({'a_track': Track(name='name')}, cls=ModelJSONEncoder) + '{"a_track": {"__type__": "Track", "name": "name"}}' + + """ + def default(self, obj): + if isinstance(obj, 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": {"__type__": "Track", "name": "name"}}', + ... object_hook=model_json_decoder) + {u'a_track': Track(artists=[], name=u'name')} + + """ + if '__type__' in dct: + obj_type = dct.pop('__type__') + if obj_type == 'Album': + return Album(**dct) + if obj_type == 'Artist': + return Artist(**dct) + if obj_type == 'Playlist': + return Playlist(**dct) + if obj_type == 'Track': + return Track(**dct) + return dct + + class Artist(ImmutableObject): """ :param uri: artist URI diff --git a/tests/models_test.py b/tests/models_test.py index 5fbd4dd3..c49142a8 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -1,8 +1,11 @@ from __future__ import unicode_literals import datetime +import json -from mopidy.models import Artist, Album, TlTrack, Track, Playlist +from mopidy.models import ( + Artist, Album, TlTrack, Track, Playlist, + ModelJSONEncoder, model_json_decoder) from tests import unittest @@ -86,6 +89,12 @@ class ArtistTest(unittest.TestCase): {'__type__': 'Artist', 'uri': 'uri', 'name': 'name'}, Artist(uri='uri', name='name').serialize()) + def test_to_json_and_Back(self): + artist1 = Artist(uri='uri', name='name') + serialized = json.dumps(artist1, cls=ModelJSONEncoder) + artist2 = json.loads(serialized, object_hook=model_json_decoder) + self.assertEqual(artist1, artist2) + def test_eq_name(self): artist1 = Artist(name='name') artist2 = Artist(name='name') @@ -205,6 +214,12 @@ class AlbumTest(unittest.TestCase): [artist.serialize()]}, Album(uri='uri', name='name', artists=[artist]).serialize()) + def test_to_json_and_back(self): + album1 = Album(uri='uri', name='name', artists=[Artist(name='foo')]) + serialized = json.dumps(album1, cls=ModelJSONEncoder) + album2 = json.loads(serialized, object_hook=model_json_decoder) + self.assertEqual(album1, album2) + def test_eq_name(self): album1 = Album(name='name') album2 = Album(name='name') @@ -404,6 +419,14 @@ class TrackTest(unittest.TestCase): 'album': album.serialize()}, Track(uri='uri', name='name', album=album).serialize()) + def test_to_json_and_back(self): + track1 = Track( + uri='uri', name='name', album=Album(name='foo'), + artists=[Artist(name='foo')]) + serialized = json.dumps(track1, cls=ModelJSONEncoder) + track2 = json.loads(serialized, object_hook=model_json_decoder) + self.assertEqual(track1, track2) + def test_eq_uri(self): track1 = Track(uri='uri1') track2 = Track(uri='uri1') @@ -598,6 +621,12 @@ class TlTrackTest(unittest.TestCase): {'__type__': 'TlTrack', 'tlid': 123, 'track': track.serialize()}, TlTrack(tlid=123, track=track).serialize()) + def test_to_json_and_back(self): + track1 = Track(uri='uri', name='name') + serialized = json.dumps(track1, cls=ModelJSONEncoder) + track2 = json.loads(serialized, object_hook=model_json_decoder) + self.assertEqual(track1, track2) + def test_eq(self): tlid = 123 track = Track() @@ -733,6 +762,12 @@ class PlaylistTest(unittest.TestCase): 'tracks': [track.serialize()]}, Playlist(uri='uri', name='name', tracks=[track]).serialize()) + def test_to_json_and_back(self): + playlist1 = Playlist(uri='uri', name='name') + serialized = json.dumps(playlist1, cls=ModelJSONEncoder) + playlist2 = json.loads(serialized, object_hook=model_json_decoder) + self.assertEqual(playlist1, playlist2) + def test_eq_name(self): playlist1 = Playlist(name='name') playlist2 = Playlist(name='name') From 3bc4126b45f952c254de82263fbc33b906679ea5 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Sun, 18 Nov 2012 17:28:37 +0100 Subject: [PATCH 3/7] models: Fix TlTrack deserialization --- mopidy/models.py | 2 ++ tests/models_test.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index 9eadb314..4861ef0d 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -131,6 +131,8 @@ def model_json_decoder(dct): return Artist(**dct) if obj_type == 'Playlist': return Playlist(**dct) + if obj_type == 'TlTrack': + return TlTrack(**dct) if obj_type == 'Track': return Track(**dct) return dct diff --git a/tests/models_test.py b/tests/models_test.py index c49142a8..21ad7ead 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -622,10 +622,10 @@ class TlTrackTest(unittest.TestCase): TlTrack(tlid=123, track=track).serialize()) def test_to_json_and_back(self): - track1 = Track(uri='uri', name='name') - serialized = json.dumps(track1, cls=ModelJSONEncoder) - track2 = json.loads(serialized, object_hook=model_json_decoder) - self.assertEqual(track1, track2) + tl_track1 = TlTrack(tlid=123, track=Track(uri='uri', name='name')) + serialized = json.dumps(tl_track1, cls=ModelJSONEncoder) + tl_track2 = json.loads(serialized, object_hook=model_json_decoder) + self.assertEqual(tl_track1, tl_track2) def test_eq(self): tlid = 123 From f83c595e3aa5bdeb71723ddc662132edd0cc9062 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 19 Nov 2012 23:02:08 +0100 Subject: [PATCH 4/7] models: Support deserialization of any ImmutableObject --- mopidy/models.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index 4861ef0d..b8f8b8b2 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -125,16 +125,9 @@ def model_json_decoder(dct): """ if '__type__' in dct: obj_type = dct.pop('__type__') - if obj_type == 'Album': - return Album(**dct) - if obj_type == 'Artist': - return Artist(**dct) - if obj_type == 'Playlist': - return Playlist(**dct) - if obj_type == 'TlTrack': - return TlTrack(**dct) - if obj_type == 'Track': - return Track(**dct) + cls = globals().get(obj_type, None) + if issubclass(cls, ImmutableObject): + return cls(**dct) return dct From 34d444e56372e0231a99504397408aeb77d73fd2 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 19 Nov 2012 23:35:05 +0100 Subject: [PATCH 5/7] models: Don't allow model deserialization to override methods --- mopidy/models.py | 2 +- tests/models_test.py | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index b8f8b8b2..901d637b 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -14,7 +14,7 @@ class ImmutableObject(object): def __init__(self, *args, **kwargs): for key, value in kwargs.items(): - if not hasattr(self, key): + if not hasattr(self, key) or callable(getattr(self, key)): raise TypeError( '__init__() got an unexpected keyword argument "%s"' % key) diff --git a/tests/models_test.py b/tests/models_test.py index 21ad7ead..ed17cef3 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -89,12 +89,33 @@ class ArtistTest(unittest.TestCase): {'__type__': 'Artist', 'uri': 'uri', 'name': 'name'}, Artist(uri='uri', name='name').serialize()) - def test_to_json_and_Back(self): + def test_to_json_and_back(self): artist1 = Artist(uri='uri', name='name') serialized = json.dumps(artist1, cls=ModelJSONEncoder) artist2 = json.loads(serialized, object_hook=model_json_decoder) self.assertEqual(artist1, artist2) + def test_to_json_and_back_with_unknown_field(self): + artist = Artist(uri='uri', name='name').serialize() + artist['foo'] = 'foo' + serialized = json.dumps(artist) + test = lambda: json.loads(serialized, object_hook=model_json_decoder) + self.assertRaises(TypeError, test) + + def test_to_json_and_back_with_field_matching_method(self): + artist = Artist(uri='uri', name='name').serialize() + artist['copy'] = 'foo' + serialized = json.dumps(artist) + test = lambda: json.loads(serialized, object_hook=model_json_decoder) + self.assertRaises(TypeError, test) + + def test_to_json_and_back_with_field_matching_internal_field(self): + artist = Artist(uri='uri', name='name').serialize() + artist['__mro__'] = 'foo' + serialized = json.dumps(artist) + test = lambda: json.loads(serialized, object_hook=model_json_decoder) + self.assertRaises(TypeError, test) + def test_eq_name(self): artist1 = Artist(name='name') artist2 = Artist(name='name') From 693a3d3ec63bcf3c0dc4831e68c9800e75019376 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Mon, 19 Nov 2012 23:58:09 +0100 Subject: [PATCH 6/7] models: Model creation with kwarg matching method name should fail --- tests/models_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/models_test.py b/tests/models_test.py index ed17cef3..218804e7 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -79,6 +79,13 @@ class ArtistTest(unittest.TestCase): test = lambda: Artist(foo='baz') self.assertRaises(TypeError, test) + def test_invalid_kwarg_with_name_matching_method(self): + test = lambda: Artist(copy='baz') + self.assertRaises(TypeError, test) + + test = lambda: Artist(serialize='baz') + self.assertRaises(TypeError, test) + def test_repr(self): self.assertEquals( "Artist(name=u'name', uri=u'uri')", From 8c6f04a408739957c8c818302fb67886208b24d3 Mon Sep 17 00:00:00 2001 From: Stein Magnus Jodal Date: Tue, 20 Nov 2012 00:21:26 +0100 Subject: [PATCH 7/7] models: Change serialized type marker from '__type__' to '__model__' --- mopidy/models.py | 12 ++++++------ tests/models_test.py | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index 901d637b..a4ed1b4f 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -80,7 +80,7 @@ class ImmutableObject(object): def serialize(self): data = {} - data['__type__'] = self.__class__.__name__ + data['__model__'] = self.__class__.__name__ for key in self.__dict__.keys(): public_key = key.lstrip('_') value = self.__dict__[key] @@ -101,7 +101,7 @@ class ModelJSONEncoder(json.JSONEncoder): >>> import json >>> json.dumps({'a_track': Track(name='name')}, cls=ModelJSONEncoder) - '{"a_track": {"__type__": "Track", "name": "name"}}' + '{"a_track": {"__model__": "Track", "name": "name"}}' """ def default(self, obj): @@ -118,14 +118,14 @@ def model_json_decoder(dct): >>> import json >>> json.loads( - ... '{"a_track": {"__type__": "Track", "name": "name"}}', + ... '{"a_track": {"__model__": "Track", "name": "name"}}', ... object_hook=model_json_decoder) {u'a_track': Track(artists=[], name=u'name')} """ - if '__type__' in dct: - obj_type = dct.pop('__type__') - cls = globals().get(obj_type, None) + if '__model__' in dct: + model_name = dct.pop('__model__') + cls = globals().get(model_name, None) if issubclass(cls, ImmutableObject): return cls(**dct) return dct diff --git a/tests/models_test.py b/tests/models_test.py index 218804e7..9a3062fc 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -93,7 +93,7 @@ class ArtistTest(unittest.TestCase): def test_serialize(self): self.assertDictEqual( - {'__type__': 'Artist', 'uri': 'uri', 'name': 'name'}, + {'__model__': 'Artist', 'uri': 'uri', 'name': 'name'}, Artist(uri='uri', name='name').serialize()) def test_to_json_and_back(self): @@ -232,14 +232,14 @@ class AlbumTest(unittest.TestCase): def test_serialize_without_artists(self): self.assertDictEqual( - {'__type__': 'Album', 'uri': 'uri', 'name': 'name'}, + {'__model__': 'Album', 'uri': 'uri', 'name': 'name'}, Album(uri='uri', name='name').serialize()) def test_serialize_with_artists(self): artist = Artist(name='foo') self.assertDictEqual( - {'__type__': 'Album', 'uri': 'uri', 'name': 'name', 'artists': - [artist.serialize()]}, + {'__model__': 'Album', 'uri': 'uri', 'name': 'name', + 'artists': [artist.serialize()]}, Album(uri='uri', name='name', artists=[artist]).serialize()) def test_to_json_and_back(self): @@ -430,20 +430,20 @@ class TrackTest(unittest.TestCase): def test_serialize_without_artists(self): self.assertDictEqual( - {'__type__': 'Track', 'uri': 'uri', 'name': 'name'}, + {'__model__': 'Track', 'uri': 'uri', 'name': 'name'}, Track(uri='uri', name='name').serialize()) def test_serialize_with_artists(self): artist = Artist(name='foo') self.assertDictEqual( - {'__type__': 'Track', 'uri': 'uri', 'name': 'name', + {'__model__': 'Track', 'uri': 'uri', 'name': 'name', 'artists': [artist.serialize()]}, Track(uri='uri', name='name', artists=[artist]).serialize()) def test_serialize_with_album(self): album = Album(name='foo') self.assertDictEqual( - {'__type__': 'Track', 'uri': 'uri', 'name': 'name', + {'__model__': 'Track', 'uri': 'uri', 'name': 'name', 'album': album.serialize()}, Track(uri='uri', name='name', album=album).serialize()) @@ -646,7 +646,7 @@ class TlTrackTest(unittest.TestCase): def test_serialize(self): track = Track(uri='uri', name='name') self.assertDictEqual( - {'__type__': 'TlTrack', 'tlid': 123, 'track': track.serialize()}, + {'__model__': 'TlTrack', 'tlid': 123, 'track': track.serialize()}, TlTrack(tlid=123, track=track).serialize()) def test_to_json_and_back(self): @@ -780,13 +780,13 @@ class PlaylistTest(unittest.TestCase): def test_serialize_without_tracks(self): self.assertDictEqual( - {'__type__': 'Playlist', 'uri': 'uri', 'name': 'name'}, + {'__model__': 'Playlist', 'uri': 'uri', 'name': 'name'}, Playlist(uri='uri', name='name').serialize()) def test_serialize_with_tracks(self): track = Track(name='foo') self.assertDictEqual( - {'__type__': 'Playlist', 'uri': 'uri', 'name': 'name', + {'__model__': 'Playlist', 'uri': 'uri', 'name': 'name', 'tracks': [track.serialize()]}, Playlist(uri='uri', name='name', tracks=[track]).serialize())