aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2017-10-30 02:32:04 +0000
committerMike Crute <mike@crute.us>2017-10-30 02:32:04 +0000
commit71677cc962218a0001ef9d9b4404179995cc21ef (patch)
tree743853f3671a1149343ca7f8e7b2a9323dc6c662
parent2740e2c88f952b7b6ca29ef0dc6216c5ac19c7b1 (diff)
downloadpydora-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__.py20
-rw-r--r--pandora/models/pandora.py12
-rw-r--r--tests/test_pandora/test_models.py25
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
3from ..py2compat import with_metaclass 3from ..py2compat import with_metaclass
4 4
5 5
6class Field(namedtuple("Field", ["field", "default", "formatter"])): 6class 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
33class SyntheticField(namedtuple("SyntheticField", ["field"])): 33class 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
254class BookmarkList(PandoraModel): 254class 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
260class SearchResultItem(PandoraModel): 260class 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
354class GenreStationList(PandoraDictListModel): 352class 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
38class TestPandoraModel(TestCase): 38class 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