Skip to content

Commit bed3917

Browse files
author
Mike Hearing
committed
Cleanup of recursive serialization to return better object deltas
1 parent 0467557 commit bed3917

File tree

6 files changed

+68
-21
lines changed

6 files changed

+68
-21
lines changed

pyrestorm/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.1.3'
1+
__version__ = '0.1.4'

pyrestorm/client.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import json
22
import requests
3-
import urllib
43

54
from exceptions.http import (
65
ServerErrorException,

pyrestorm/models.py

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
primitives = [int, str, unicode, bool, type(None)]
99
non_object_types = [dict, list, tuple]
1010

11+
SENTINEL = '__SENTINEL__'
12+
1113

1214
class RestModelBase(type):
1315
# Called when class is imported got guarantee proper setup of child classes to parent
@@ -104,36 +106,75 @@ def _bind_data(obj, data):
104106
else:
105107
setattr(obj, key, copy.deepcopy(val))
106108

107-
# Converts object structure into JSON
108-
def _serialize_data(self, obj):
109+
@staticmethod
110+
def _get_reference_data(ref, idx):
111+
ref_type = type(ref)
112+
ret = ref
113+
if ref_type == list:
114+
try:
115+
ret = ref[idx]
116+
except ValueError:
117+
ret = SENTINEL
118+
elif ref_type == dict:
119+
ret = ref.get(idx, SENTINEL)
120+
121+
return ret
122+
123+
def _serialize_data(self, obj, ref):
124+
'''Converts the local Python objects into a JSON structure.
125+
126+
There are 3 cases we need to handle in this recursive function:
127+
1. Primitives: Should be serialized to JSON as-is
128+
2. List: Iterate over all elements and call _serialize_data on each
129+
3. Dictionary/Object: Recursively call _serialize_data
130+
131+
Arguments:
132+
obj - The current attribute on the model which we are comparing
133+
against the previous state for changes.
134+
ref - Reference data stored on the `RestModel` for the attribute
135+
layer currently being looked at. Used for saving since a
136+
HTTP PATCH is used for updates, where we only send the delta.
137+
'''
109138
local_diff = {}
110139

111-
# Convert to dictionary
140+
# Get the `dict` representation of the `object` to combine cases
112141
if type(obj) not in (primitives + non_object_types):
113142
obj = obj.__dict__
114143

115-
# For all of the top level keys
144+
# Check each value in the `dict` for changes
116145
for key, value in obj.iteritems():
117-
# Do not worry about private
118-
if key.startswith('_'):
146+
# 0. Private variables: SKIP
147+
# Escape early if nothing has changed
148+
if key.startswith('_') or self._get_reference_data(ref, key) == value:
119149
continue
120150

151+
# Determine what type the current `value` is to process one of the three cases
121152
value_type = type(value)
122-
if value_type not in primitives:
123-
# Nested structure
153+
154+
# 1. Primitives
155+
if value_type in primitives:
156+
# If the value of the field is not what we've seen before, add it to the diff
157+
if ref != value:
158+
local_diff[key] = value
159+
160+
# 2/3. Objects
161+
else:
162+
# 2. Lists
124163
if value_type == list:
125-
local_diff[key] = copy.deepcopy(value)
164+
# Process each idex of the `list`
165+
local_diff[key] = []
126166
for idx, inner_value in enumerate(value):
127-
if type(inner_value) not in primitives:
128-
local_diff[key][idx] = self._serialize_data(inner_value)
167+
if type(inner_value) in primitives:
168+
local_diff[key].append(inner_value)
129169
else:
130-
local_diff[key][idx] = inner_value
131-
# Object/Dictionary
170+
local_diff[key].append(self._serialize_data(inner_value, self._get_reference_data(ref, idx)))
171+
# 3. Object/Dictionary
132172
else:
133-
local_diff[key] = self._serialize_data(value)
134-
# Primitive type
135-
elif self._data.get(key, '__SENTINEL__') != value:
136-
local_diff[key] = value
173+
new_ref = self._get_reference_data(ref, key)
174+
_data = self._serialize_data(value, new_ref)
175+
if not _data and type(value) not in (primitives + non_object_types):
176+
continue
177+
local_diff[key] = _data
137178

138179
return local_diff
139180

@@ -157,7 +198,7 @@ def get_absolute_url(self):
157198
# Does not save nested keys
158199
def save(self, raise_exception=False, **kwargs):
159200
# Difference between the original model and new one
160-
diff = self._serialize_data(self)
201+
diff = self._serialize_data(self, self._data)
161202

162203
# Perform a PATCH only if there are difference
163204
if diff:

pyrestorm/query.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def filter(self, **kwargs):
197197
def none(self, *args, **kwargs):
198198
'''Imitate an empty `RestQueryset` with no results
199199
'''
200-
return iter([])
200+
return
201201

202202
def all(self, *args, **kwargs):
203203
'''Unmodified query to return all results in the `RestQueryset`

tests/test_models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,11 @@ def test_restmodel_get_multipleobjectreturned(self):
4848

4949
def test_restmodel_save(self):
5050
post = Post.objects.get(id=1)
51+
post.body = [Comment(body='Are we having fun yet?'), Comment(body='Hoe about now?')]
5152
post.title = 'Testing'
5253
post.save()
54+
# Redundant save for list serializing
55+
post.save()
5356
self.assertEqual(post._data['title'], post.title)
5457

5558
def test_restmodel_serializable_value(self):

tests/test_query.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ def test_iter(self):
3131
for item in queryset:
3232
self.assertTrue(True)
3333

34+
def test_none(self):
35+
queryset = RestQueryset(Post).none()
36+
self.assertIsNone(queryset)
37+
3438

3539
class RestPaginatedQuerysetTestCase(TestCase):
3640
def test_init(self):

0 commit comments

Comments
 (0)