Skip to content

Commit c1137f6

Browse files
committed
Re-writing Entity to subclass object instead of dict.
1 parent 21e5f45 commit c1137f6

File tree

6 files changed

+127
-34
lines changed

6 files changed

+127
-34
lines changed

gcloud/datastore/entity.py

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class NoDataset(RuntimeError):
2626
"""Exception raised by Entity methods which require a dataset."""
2727

2828

29-
class Entity(dict):
29+
class Entity(object):
3030
"""Entities are akin to rows in a relational database
3131
3232
An entity storing the actual instance of data.
@@ -41,9 +41,9 @@ class Entity(dict):
4141
Entities in this API act like dictionaries with extras built in that
4242
allow you to delete or persist the data stored on the entity.
4343
44-
Entities are mutable and act like a subclass of a dictionary.
45-
This means you could take an existing entity and change the key
46-
to duplicate the object.
44+
Entities are mutable and properties can be set, updated and deleted
45+
like keys in a dictionary. This means you could take an existing entity
46+
and change the key to duplicate the object.
4747
4848
Use :func:`gcloud.datastore.dataset.Dataset.get_entity`
4949
to retrieve an existing entity.
@@ -59,10 +59,9 @@ class Entity(dict):
5959
>>> entity
6060
<Entity[{'kind': 'EntityKind', id: 1234}] {'age': 20, 'name': 'JJ'}>
6161
62-
And you can convert an entity to a regular Python dictionary with the
63-
`dict` builtin:
62+
And you can convert an entity to a regular Python dictionary
6463
65-
>>> dict(entity)
64+
>>> entity.to_dict()
6665
{'age': 20, 'name': 'JJ'}
6766
6867
.. note::
@@ -94,14 +93,57 @@ class Entity(dict):
9493
"""
9594

9695
def __init__(self, dataset=None, kind=None, exclude_from_indexes=()):
97-
super(Entity, self).__init__()
9896
self._dataset = dataset
97+
self._data = {}
9998
if kind:
10099
self._key = Key().kind(kind)
101100
else:
102101
self._key = None
103102
self._exclude_from_indexes = set(exclude_from_indexes)
104103

104+
def __getitem__(self, item_name):
105+
return self._data[item_name]
106+
107+
def __setitem__(self, item_name, value):
108+
self._data[item_name] = value
109+
110+
def __delitem__(self, item_name):
111+
del self._data[item_name]
112+
113+
def clear_properties(self):
114+
"""Clear all properties from the Entity."""
115+
self._data.clear()
116+
117+
def update_properties(self, *args, **kwargs):
118+
"""Allows entity properties to be updated in bulk.
119+
120+
Either takes a single dictionary or uses the keywords passed in.
121+
122+
>>> entity
123+
<Entity[{'kind': 'Foo', 'id': 1}] {}>
124+
>>> entity.update_properties(prop1=u'bar', prop2=u'baz')
125+
>>> entity
126+
<Entity[{'kind': 'Foo', 'id': 1}] {'prop1': u'bar', 'prop2': u'baz'}>
127+
>>> entity.update_properties({'prop1': 0, 'prop2': 1})
128+
>>> entity
129+
<Entity[{'kind': 'Foo', 'id': 1}] {'prop1': 0, 'prop2': 1}>
130+
131+
:raises: `TypeError` a mix of positional and keyword arguments are
132+
used or if more than one positional argument is used.
133+
"""
134+
if args and kwargs or len(args) > 1:
135+
raise TypeError('Only a single dictionary or keyword arguments '
136+
'may be used')
137+
if args:
138+
dict_arg, = args
139+
self._data.update(dict_arg)
140+
else:
141+
self._data.update(kwargs)
142+
143+
def to_dict(self):
144+
"""Converts the stored properties to a dictionary."""
145+
return self._data.copy()
146+
105147
def dataset(self):
106148
"""Get the :class:`.dataset.Dataset` in which this entity belongs.
107149
@@ -215,7 +257,7 @@ def reload(self):
215257
entity = dataset.get_entity(key.to_protobuf())
216258

217259
if entity:
218-
self.update(entity)
260+
self.update_properties(entity.to_dict())
219261
return self
220262

221263
def save(self):
@@ -241,7 +283,7 @@ def save(self):
241283
key_pb = connection.save_entity(
242284
dataset_id=dataset.id(),
243285
key_pb=key.to_protobuf(),
244-
properties=dict(self),
286+
properties=self._data,
245287
exclude_from_indexes=self.exclude_from_indexes())
246288

247289
# If we are in a transaction and the current entity needs an
@@ -284,7 +326,6 @@ def delete(self):
284326

285327
def __repr__(self):
286328
if self._key:
287-
return '<Entity%s %s>' % (self._key.path(),
288-
super(Entity, self).__repr__())
329+
return '<Entity%s %r>' % (self._key.path(), self._data)
289330
else:
290-
return '<Entity %s>' % (super(Entity, self).__repr__())
331+
return '<Entity %r>' % (self._data,)

gcloud/datastore/helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ def _set_protobuf_value(value_pb, val):
252252
key = val.key()
253253
if key is not None:
254254
e_pb.key.CopyFrom(key.to_protobuf())
255-
for item_key, value in val.items():
255+
for item_key, value in val.to_dict().items():
256256
p_pb = e_pb.property.add()
257257
p_pb.name = item_key
258258
_set_protobuf_value(p_pb.value, value)

gcloud/datastore/test_dataset.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def test_get_entity_hit(self):
106106
key = result.key()
107107
self.assertEqual(key._dataset_id, DATASET_ID)
108108
self.assertEqual(key.path(), PATH)
109-
self.assertEqual(list(result), ['foo'])
109+
self.assertEqual(result.to_dict().keys(), ['foo'])
110110
self.assertEqual(result['foo'], 'Foo')
111111

112112
def test_get_entity_path(self):
@@ -129,7 +129,7 @@ def test_get_entity_path(self):
129129
key = result.key()
130130
self.assertEqual(key._dataset_id, DATASET_ID)
131131
self.assertEqual(key.path(), PATH)
132-
self.assertEqual(list(result), ['foo'])
132+
self.assertEqual(result.to_dict().keys(), ['foo'])
133133
self.assertEqual(result['foo'], 'Foo')
134134

135135
def test_get_entity_odd_nonetype(self):
@@ -210,7 +210,7 @@ def test_get_entities_hit(self):
210210
key = result.key()
211211
self.assertEqual(key._dataset_id, DATASET_ID)
212212
self.assertEqual(key.path(), PATH)
213-
self.assertEqual(list(result), ['foo'])
213+
self.assertEqual(result.to_dict().keys(), ['foo'])
214214
self.assertEqual(result['foo'], 'Foo')
215215

216216
def test_allocate_ids(self):

gcloud/datastore/test_entity.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,59 @@ def test_key_setter(self):
6868
entity.key(key)
6969
self.assertTrue(entity.key() is key)
7070

71+
def test___delitem__exists(self):
72+
entity = self._makeOne()
73+
entity['foo'] = 'bar'
74+
# This will cause an error (not a failure) if it doesn't work.
75+
# Can't use a try-except because coverage.py doesn't like a branch
76+
# which never occurs.
77+
del entity['foo']
78+
79+
def test___delitem__not_exist(self):
80+
entity = self._makeOne()
81+
fail_occurred = False
82+
try:
83+
del entity['foo']
84+
except KeyError:
85+
fail_occurred = True
86+
self.assertTrue(fail_occurred)
87+
88+
def test_clear_properties(self):
89+
entity = self._makeOne()
90+
entity['foo'] = 0
91+
entity['bar'] = 1
92+
self.assertEqual(entity.to_dict(), {'foo': 0, 'bar': 1})
93+
94+
entity.clear_properties()
95+
self.assertEqual(entity.to_dict(), {})
96+
97+
def test_update_properties_dict(self):
98+
entity = self._makeOne()
99+
self.assertEqual(entity.to_dict(), {})
100+
101+
NEW_VALUES = {'prop1': 0, 'prop2': 1}
102+
entity.update_properties(NEW_VALUES)
103+
self.assertEqual(entity.to_dict(), NEW_VALUES)
104+
105+
def test_update_properties_keywords(self):
106+
entity = self._makeOne()
107+
self.assertEqual(entity.to_dict(), {})
108+
109+
NEW_VALUES = {'prop1': 0, 'prop2': 1}
110+
entity.update_properties(**NEW_VALUES)
111+
self.assertEqual(entity.to_dict(), NEW_VALUES)
112+
113+
entity.update_properties(prop1=10, prop2=11)
114+
NEW_VALUES_AGAIN = {'prop1': 10, 'prop2': 11}
115+
self.assertEqual(entity.to_dict(), NEW_VALUES_AGAIN)
116+
117+
def test_update_properties_invalid(self):
118+
entity = self._makeOne()
119+
120+
dict1 = {'foo': 'bar'}
121+
dict2 = {'baz': 'zip'}
122+
self.assertRaises(TypeError, entity.update_properties, dict1, dict2)
123+
71124
def test_from_key_wo_dataset(self):
72125
from gcloud.datastore.key import Key
73126

@@ -125,8 +178,13 @@ def test_reload_miss(self):
125178

126179
def test_reload_hit(self):
127180
dataset = _Dataset()
128-
dataset['KEY'] = {'foo': 'Bar'}
181+
182+
fake_entity = self._makeOne(dataset=dataset)
183+
fake_entity['foo'] = 'Bar'
184+
129185
key = _Key()
186+
dataset[key._key] = fake_entity
187+
130188
entity = self._makeOne(dataset)
131189
entity.key(key)
132190
entity['foo'] = 'Foo'

regression/datastore.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def _get_post(self, name=None, key_id=None, post_content=None):
6767
}
6868
# Create an entity with the given content in our dataset.
6969
entity = self.dataset.entity(kind='Post')
70-
entity.update(post_content)
70+
entity.update_properties(post_content)
7171

7272
# Update the entity key.
7373
key = None
@@ -98,9 +98,7 @@ def _generic_test_post(self, name=None, key_id=None):
9898
entity.key().namespace())
9999

100100
# Check the data is the same.
101-
retrieved_dict = dict(retrieved_entity.items())
102-
entity_dict = dict(entity.items())
103-
self.assertEqual(retrieved_dict, entity_dict)
101+
self.assertEqual(retrieved_entity.to_dict(), entity.to_dict())
104102

105103
def test_post_with_name(self):
106104
self._generic_test_post(name='post1')
@@ -249,17 +247,15 @@ def test_projection_query(self):
249247
self.assertEqual(len(entities), expected_matches)
250248

251249
arya_entity = entities[0]
252-
arya_dict = dict(arya_entity.items())
253-
self.assertEqual(arya_dict, {'name': 'Arya', 'family': 'Stark'})
250+
self.assertEqual(arya_entity.to_dict(),
251+
{'name': 'Arya', 'family': 'Stark'})
254252

255253
catelyn_stark_entity = entities[2]
256-
catelyn_stark_dict = dict(catelyn_stark_entity.items())
257-
self.assertEqual(catelyn_stark_dict,
254+
self.assertEqual(catelyn_stark_entity.to_dict(),
258255
{'name': 'Catelyn', 'family': 'Stark'})
259256

260257
catelyn_tully_entity = entities[3]
261-
catelyn_tully_dict = dict(catelyn_tully_entity.items())
262-
self.assertEqual(catelyn_tully_dict,
258+
self.assertEqual(catelyn_tully_entity.to_dict(),
263259
{'name': 'Catelyn', 'family': 'Tully'})
264260

265261
# Check both Catelyn keys are the same.
@@ -273,8 +269,8 @@ def test_projection_query(self):
273269
catelyn_tully_key._dataset_id)
274270

275271
sansa_entity = entities[8]
276-
sansa_dict = dict(sansa_entity.items())
277-
self.assertEqual(sansa_dict, {'name': 'Sansa', 'family': 'Stark'})
272+
self.assertEqual(sansa_entity.to_dict(),
273+
{'name': 'Sansa', 'family': 'Stark'})
278274

279275
def test_query_paginate_with_offset(self):
280276
query = self._base_query()
@@ -346,7 +342,5 @@ def test_transaction(self):
346342

347343
# This will always return after the transaction.
348344
retrieved_entity = self.dataset.get_entity(key)
349-
retrieved_dict = dict(retrieved_entity.items())
350-
entity_dict = dict(entity.items())
351-
self.assertEqual(retrieved_dict, entity_dict)
345+
self.assertEqual(retrieved_entity.to_dict(), entity.to_dict())
352346
retrieved_entity.delete()

regression/populate_datastore.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def add_characters():
9595
key_path, character))
9696
key = datastore.key.Key(path=key_path)
9797
entity = datastore.entity.Entity(dataset=dataset).key(key)
98-
entity.update(character)
98+
entity.update_properties(character)
9999
entity.save()
100100
print('Adding Character %s %s' % (character['name'],
101101
character['family']))

0 commit comments

Comments
 (0)