From 71677cc962218a0001ef9d9b4404179995cc21ef Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Mon, 30 Oct 2017 02:32:04 +0000 Subject: Refactor sub-model building These used to be done with formatters but that was somewhat an abuse of the formatter model in the first place. This changeset adds a model attribute that will cause the underlying model builder to dynamically construct a model or list of models depending on the incoming data type. --- pandora/models/__init__.py | 20 +++++++++++++------- pandora/models/pandora.py | 12 +++++------- tests/test_pandora/test_models.py | 25 +++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/pandora/models/__init__.py b/pandora/models/__init__.py index 5b25de2..141a64b 100644 --- a/pandora/models/__init__.py +++ b/pandora/models/__init__.py @@ -3,7 +3,7 @@ from collections import namedtuple from ..py2compat import with_metaclass -class Field(namedtuple("Field", ["field", "default", "formatter"])): +class Field(namedtuple("Field", ["field", "default", "formatter", "model"])): """Model Field Model fields represent JSON key/value pairs. When added to a PandoraModel @@ -26,8 +26,8 @@ class Field(namedtuple("Field", ["field", "default", "formatter"])): model based on the type of data in the JSON """ - def __new__(cls, field, default=None, formatter=None): - return super(Field, cls).__new__(cls, field, default, formatter) + def __new__(cls, field, default=None, formatter=None, model=None): + return super(Field, cls).__new__(cls, field, default, formatter, model) class SyntheticField(namedtuple("SyntheticField", ["field"])): @@ -39,8 +39,6 @@ class SyntheticField(namedtuple("SyntheticField", ["field"])): payload. """ - default = None - @staticmethod def formatter(api_client, field, data): # pragma: no cover """Format Value for Model @@ -100,7 +98,7 @@ class PandoraModel(with_metaclass(ModelMetaClass, object)): safe_types = (type(None), str, bytes, int, bool) for key, value in self._fields.items(): - default = value.default + default = getattr(value, "default", None) if not isinstance(default, safe_types): default = type(default)() @@ -117,13 +115,21 @@ class PandoraModel(with_metaclass(ModelMetaClass, object)): this function runs even if they are missing from the incoming JSON. """ for key, value in instance.__class__._fields.items(): - newval = data.get(value.field, value.default) + default = getattr(value, "default", None) + newval = data.get(value.field, default) if isinstance(value, SyntheticField): newval = value.formatter(api_client, value.field, data, newval) setattr(instance, key, newval) continue + model_class = getattr(value, "model", None) + if newval and model_class: + if isinstance(newval, list): + newval = model_class.from_json_list(api_client, newval) + else: + newval = model_class.from_json(api_client, newval) + if newval and value.formatter: newval = value.formatter(api_client, newval) diff --git a/pandora/models/pandora.py b/pandora/models/pandora.py index d7d2bf1..71a1d51 100644 --- a/pandora/models/pandora.py +++ b/pandora/models/pandora.py @@ -253,8 +253,8 @@ class Bookmark(PandoraModel): class BookmarkList(PandoraModel): - songs = Field("songs", formatter=Bookmark.from_json_list) - artists = Field("artists", formatter=Bookmark.from_json_list) + songs = Field("songs", model=Bookmark) + artists = Field("artists", model=Bookmark) class SearchResultItem(PandoraModel): @@ -344,11 +344,9 @@ class SearchResult(PandoraModel): nearest_matches_available = Field("nearMatchesAvailable") explanation = Field("explanation") - songs = Field("songs", formatter=SongSearchResultItem.from_json_list) - artists = Field("artists", formatter=ArtistSearchResultItem.from_json_list) - genre_stations = Field( - "genreStations", - formatter=GenreStationSearchResultItem.from_json_list) + songs = Field("songs", model=SongSearchResultItem) + artists = Field("artists", model=ArtistSearchResultItem) + genre_stations = Field("genreStations", model=GenreStationSearchResultItem) class GenreStationList(PandoraDictListModel): diff --git a/tests/test_pandora/test_models.py b/tests/test_pandora/test_models.py index 914f09e..1310d71 100644 --- a/tests/test_pandora/test_models.py +++ b/tests/test_pandora/test_models.py @@ -37,15 +37,26 @@ class TestModelMetaClass(TestCase): class TestPandoraModel(TestCase): - JSON_DATA = {"field2": ["test2"], "field3": 41} + JSON_DATA = { + "field2": ["test2"], + "field3": 41, + "field4": {"field1": "foo"}, + "field5": [{"field1": "foo"}, {"field1": "bar"}], + } class TestModel(m.PandoraModel): + class SubModel(m.PandoraModel): + + field1 = m.Field("field1") + THE_LIST = [] field1 = m.Field("field1", default="a string") field2 = m.Field("field2", default=THE_LIST) field3 = m.Field("field3", formatter=lambda c, x: x + 1) + field4 = m.Field("field4", model=SubModel) + field5 = m.Field("field5", model=SubModel) class NoFieldsModel(m.PandoraModel): pass @@ -76,6 +87,14 @@ class TestPandoraModel(TestCase): self.assertEqual("a string", result.field1) self.assertEqual(["test2"], result.field2) + def test_it_creates_sub_models(self): + result = self.TestModel.from_json(None, self.JSON_DATA) + self.assertIsInstance(result.field4, self.TestModel.SubModel) + self.assertEqual("foo", result.field4.field1) + self.assertEqual(2, len(result.field5)) + self.assertEqual("foo", result.field5[0].field1) + self.assertEqual("bar", result.field5[1].field1) + def test_populate_fields_calls_formatter(self): result = self.TestModel.from_json(None, self.JSON_DATA) self.assertEqual(42, result.field3) @@ -87,7 +106,9 @@ class TestPandoraModel(TestCase): self.assertEqual("a string", result[1].field1) def test_repr(self): - expected = "TestModel(field1='a string', field2=['test2'], field3=42)" + expected = ("TestModel(field1='a string', field2=['test2'], field3=42," + " field4=SubModel(field1='foo'), " + "field5=[SubModel(field1='foo'), SubModel(field1='bar')])") result = self.TestModel.from_json(None, self.JSON_DATA) self.assertEqual(expected, repr(result)) -- cgit v1.2.3