diff options
author | Mike Crute <mike@crute.us> | 2017-10-30 02:32:04 +0000 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2017-10-30 02:32:04 +0000 |
commit | 71677cc962218a0001ef9d9b4404179995cc21ef (patch) | |
tree | 743853f3671a1149343ca7f8e7b2a9323dc6c662 | |
parent | 2740e2c88f952b7b6ca29ef0dc6216c5ac19c7b1 (diff) | |
download | pydora-71677cc962218a0001ef9d9b4404179995cc21ef.tar.bz2 pydora-71677cc962218a0001ef9d9b4404179995cc21ef.tar.xz pydora-71677cc962218a0001ef9d9b4404179995cc21ef.zip |
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.
-rw-r--r-- | pandora/models/__init__.py | 20 | ||||
-rw-r--r-- | pandora/models/pandora.py | 12 | ||||
-rw-r--r-- | 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 | |||
3 | from ..py2compat import with_metaclass | 3 | from ..py2compat import with_metaclass |
4 | 4 | ||
5 | 5 | ||
6 | class Field(namedtuple("Field", ["field", "default", "formatter"])): | 6 | class Field(namedtuple("Field", ["field", "default", "formatter", "model"])): |
7 | """Model Field | 7 | """Model Field |
8 | 8 | ||
9 | Model fields represent JSON key/value pairs. When added to a PandoraModel | 9 | Model fields represent JSON key/value pairs. When added to a PandoraModel |
@@ -26,8 +26,8 @@ class Field(namedtuple("Field", ["field", "default", "formatter"])): | |||
26 | model based on the type of data in the JSON | 26 | model based on the type of data in the JSON |
27 | """ | 27 | """ |
28 | 28 | ||
29 | def __new__(cls, field, default=None, formatter=None): | 29 | def __new__(cls, field, default=None, formatter=None, model=None): |
30 | return super(Field, cls).__new__(cls, field, default, formatter) | 30 | return super(Field, cls).__new__(cls, field, default, formatter, model) |
31 | 31 | ||
32 | 32 | ||
33 | class SyntheticField(namedtuple("SyntheticField", ["field"])): | 33 | class SyntheticField(namedtuple("SyntheticField", ["field"])): |
@@ -39,8 +39,6 @@ class SyntheticField(namedtuple("SyntheticField", ["field"])): | |||
39 | payload. | 39 | payload. |
40 | """ | 40 | """ |
41 | 41 | ||
42 | default = None | ||
43 | |||
44 | @staticmethod | 42 | @staticmethod |
45 | def formatter(api_client, field, data): # pragma: no cover | 43 | def formatter(api_client, field, data): # pragma: no cover |
46 | """Format Value for Model | 44 | """Format Value for Model |
@@ -100,7 +98,7 @@ class PandoraModel(with_metaclass(ModelMetaClass, object)): | |||
100 | safe_types = (type(None), str, bytes, int, bool) | 98 | safe_types = (type(None), str, bytes, int, bool) |
101 | 99 | ||
102 | for key, value in self._fields.items(): | 100 | for key, value in self._fields.items(): |
103 | default = value.default | 101 | default = getattr(value, "default", None) |
104 | 102 | ||
105 | if not isinstance(default, safe_types): | 103 | if not isinstance(default, safe_types): |
106 | default = type(default)() | 104 | default = type(default)() |
@@ -117,13 +115,21 @@ class PandoraModel(with_metaclass(ModelMetaClass, object)): | |||
117 | this function runs even if they are missing from the incoming JSON. | 115 | this function runs even if they are missing from the incoming JSON. |
118 | """ | 116 | """ |
119 | for key, value in instance.__class__._fields.items(): | 117 | for key, value in instance.__class__._fields.items(): |
120 | newval = data.get(value.field, value.default) | 118 | default = getattr(value, "default", None) |
119 | newval = data.get(value.field, default) | ||
121 | 120 | ||
122 | if isinstance(value, SyntheticField): | 121 | if isinstance(value, SyntheticField): |
123 | newval = value.formatter(api_client, value.field, data, newval) | 122 | newval = value.formatter(api_client, value.field, data, newval) |
124 | setattr(instance, key, newval) | 123 | setattr(instance, key, newval) |
125 | continue | 124 | continue |
126 | 125 | ||
126 | model_class = getattr(value, "model", None) | ||
127 | if newval and model_class: | ||
128 | if isinstance(newval, list): | ||
129 | newval = model_class.from_json_list(api_client, newval) | ||
130 | else: | ||
131 | newval = model_class.from_json(api_client, newval) | ||
132 | |||
127 | if newval and value.formatter: | 133 | if newval and value.formatter: |
128 | newval = value.formatter(api_client, newval) | 134 | newval = value.formatter(api_client, newval) |
129 | 135 | ||
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): | |||
253 | 253 | ||
254 | class BookmarkList(PandoraModel): | 254 | class BookmarkList(PandoraModel): |
255 | 255 | ||
256 | songs = Field("songs", formatter=Bookmark.from_json_list) | 256 | songs = Field("songs", model=Bookmark) |
257 | artists = Field("artists", formatter=Bookmark.from_json_list) | 257 | artists = Field("artists", model=Bookmark) |
258 | 258 | ||
259 | 259 | ||
260 | class SearchResultItem(PandoraModel): | 260 | class SearchResultItem(PandoraModel): |
@@ -344,11 +344,9 @@ class SearchResult(PandoraModel): | |||
344 | 344 | ||
345 | nearest_matches_available = Field("nearMatchesAvailable") | 345 | nearest_matches_available = Field("nearMatchesAvailable") |
346 | explanation = Field("explanation") | 346 | explanation = Field("explanation") |
347 | songs = Field("songs", formatter=SongSearchResultItem.from_json_list) | 347 | songs = Field("songs", model=SongSearchResultItem) |
348 | artists = Field("artists", formatter=ArtistSearchResultItem.from_json_list) | 348 | artists = Field("artists", model=ArtistSearchResultItem) |
349 | genre_stations = Field( | 349 | genre_stations = Field("genreStations", model=GenreStationSearchResultItem) |
350 | "genreStations", | ||
351 | formatter=GenreStationSearchResultItem.from_json_list) | ||
352 | 350 | ||
353 | 351 | ||
354 | class GenreStationList(PandoraDictListModel): | 352 | 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): | |||
37 | 37 | ||
38 | class TestPandoraModel(TestCase): | 38 | class TestPandoraModel(TestCase): |
39 | 39 | ||
40 | JSON_DATA = {"field2": ["test2"], "field3": 41} | 40 | JSON_DATA = { |
41 | "field2": ["test2"], | ||
42 | "field3": 41, | ||
43 | "field4": {"field1": "foo"}, | ||
44 | "field5": [{"field1": "foo"}, {"field1": "bar"}], | ||
45 | } | ||
41 | 46 | ||
42 | class TestModel(m.PandoraModel): | 47 | class TestModel(m.PandoraModel): |
43 | 48 | ||
49 | class SubModel(m.PandoraModel): | ||
50 | |||
51 | field1 = m.Field("field1") | ||
52 | |||
44 | THE_LIST = [] | 53 | THE_LIST = [] |
45 | 54 | ||
46 | field1 = m.Field("field1", default="a string") | 55 | field1 = m.Field("field1", default="a string") |
47 | field2 = m.Field("field2", default=THE_LIST) | 56 | field2 = m.Field("field2", default=THE_LIST) |
48 | field3 = m.Field("field3", formatter=lambda c, x: x + 1) | 57 | field3 = m.Field("field3", formatter=lambda c, x: x + 1) |
58 | field4 = m.Field("field4", model=SubModel) | ||
59 | field5 = m.Field("field5", model=SubModel) | ||
49 | 60 | ||
50 | class NoFieldsModel(m.PandoraModel): | 61 | class NoFieldsModel(m.PandoraModel): |
51 | pass | 62 | pass |
@@ -76,6 +87,14 @@ class TestPandoraModel(TestCase): | |||
76 | self.assertEqual("a string", result.field1) | 87 | self.assertEqual("a string", result.field1) |
77 | self.assertEqual(["test2"], result.field2) | 88 | self.assertEqual(["test2"], result.field2) |
78 | 89 | ||
90 | def test_it_creates_sub_models(self): | ||
91 | result = self.TestModel.from_json(None, self.JSON_DATA) | ||
92 | self.assertIsInstance(result.field4, self.TestModel.SubModel) | ||
93 | self.assertEqual("foo", result.field4.field1) | ||
94 | self.assertEqual(2, len(result.field5)) | ||
95 | self.assertEqual("foo", result.field5[0].field1) | ||
96 | self.assertEqual("bar", result.field5[1].field1) | ||
97 | |||
79 | def test_populate_fields_calls_formatter(self): | 98 | def test_populate_fields_calls_formatter(self): |
80 | result = self.TestModel.from_json(None, self.JSON_DATA) | 99 | result = self.TestModel.from_json(None, self.JSON_DATA) |
81 | self.assertEqual(42, result.field3) | 100 | self.assertEqual(42, result.field3) |
@@ -87,7 +106,9 @@ class TestPandoraModel(TestCase): | |||
87 | self.assertEqual("a string", result[1].field1) | 106 | self.assertEqual("a string", result[1].field1) |
88 | 107 | ||
89 | def test_repr(self): | 108 | def test_repr(self): |
90 | expected = "TestModel(field1='a string', field2=['test2'], field3=42)" | 109 | expected = ("TestModel(field1='a string', field2=['test2'], field3=42," |
110 | " field4=SubModel(field1='foo'), " | ||
111 | "field5=[SubModel(field1='foo'), SubModel(field1='bar')])") | ||
91 | result = self.TestModel.from_json(None, self.JSON_DATA) | 112 | result = self.TestModel.from_json(None, self.JSON_DATA) |
92 | self.assertEqual(expected, repr(result)) | 113 | self.assertEqual(expected, repr(result)) |
93 | 114 | ||