Skip to content

Commit 08deef5

Browse files
committed
Merge pull request #253 from tseaver/174-verify_entity_repr
Fix #174: verify Entity.__repr__ logic
2 parents be5edde + 961e695 commit 08deef5

File tree

2 files changed

+77
-35
lines changed

2 files changed

+77
-35
lines changed

gcloud/datastore/entity.py

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
from gcloud.datastore.key import Key
2020

2121

22+
class NoKey(RuntimeError):
23+
"""Exception raised by Entity methods which require a key."""
24+
25+
2226
class Entity(dict): # pylint: disable=too-many-public-methods
2327
""":type dataset: :class:`gcloud.datastore.dataset.Dataset`
2428
:param dataset: The dataset in which this entity belongs.
@@ -82,8 +86,8 @@ def dataset(self):
8286
be `None`. It also means that if you change the key on the entity,
8387
this will refer to that key's dataset.
8488
"""
85-
if self.key():
86-
return self.key().dataset()
89+
if self._key:
90+
return self._key.dataset()
8791

8892
def key(self, key=None):
8993
"""Get or set the :class:`.datastore.key.Key` on the current entity.
@@ -117,8 +121,8 @@ def kind(self):
117121
which knows its Kind.
118122
"""
119123

120-
if self.key():
121-
return self.key().kind()
124+
if self._key:
125+
return self._key.kind()
122126

123127
@classmethod
124128
def from_key(cls, key):
@@ -162,6 +166,18 @@ def from_protobuf(cls, pb, dataset=None): # pylint: disable=invalid-name
162166

163167
return entity
164168

169+
@property
170+
def _must_key(self):
171+
"""Return our key, or raise NoKey if not set.
172+
173+
:rtype: :class:`gcloud.datastore.key.Key`.
174+
:returns: our key
175+
:raises: NoKey if key is None
176+
"""
177+
if self._key is None:
178+
raise NoKey('no key')
179+
return self._key
180+
165181
def reload(self):
166182
"""Reloads the contents of this entity from the datastore.
167183
@@ -174,11 +190,8 @@ def reload(self):
174190
exists remotely, however it will *not* override any properties that
175191
exist only locally.
176192
"""
177-
178-
# Note that you must have a valid key, otherwise this makes no sense.
179-
# pylint: disable=maybe-no-member
180-
entity = self.dataset().get_entity(self.key().to_protobuf())
181-
# pylint: enable=maybe-no-member
193+
key = self._must_key
194+
entity = key.dataset().get_entity(key.to_protobuf())
182195

183196
if entity:
184197
self.update(entity)
@@ -196,27 +209,24 @@ def save(self):
196209
:rtype: :class:`gcloud.datastore.entity.Entity`
197210
:returns: The entity with a possibly updated Key.
198211
"""
199-
# pylint: disable=maybe-no-member
200-
key_pb = self.dataset().connection().save_entity(
201-
dataset_id=self.dataset().id(),
202-
key_pb=self.key().to_protobuf(), properties=dict(self))
203-
# pylint: enable=maybe-no-member
212+
key = self._must_key
213+
dataset = key.dataset()
214+
connection = dataset.connection()
215+
key_pb = connection.save_entity(
216+
dataset_id=dataset.id(),
217+
key_pb=key.to_protobuf(),
218+
properties=dict(self))
204219

205220
# If we are in a transaction and the current entity needs an
206221
# automatically assigned ID, tell the transaction where to put that.
207-
transaction = self.dataset().connection().transaction()
208-
# pylint: disable=maybe-no-member
209-
if transaction and self.key().is_partial():
222+
transaction = connection.transaction()
223+
if transaction and key.is_partial():
210224
transaction.add_auto_id_entity(self)
211-
# pylint: enable=maybe-no-member
212225

213226
if isinstance(key_pb, datastore_pb.Key):
214227
updated_key = Key.from_protobuf(key_pb)
215228
# Update the path (which may have been altered).
216-
# pylint: disable=maybe-no-member
217-
key = self.key().path(updated_key.path())
218-
# pylint: enable=maybe-no-member
219-
self.key(key)
229+
self._key = key.path(updated_key.path())
220230

221231
return self
222232

@@ -228,20 +238,14 @@ def delete(self):
228238
set on the entity. Whatever is stored remotely using the key on the
229239
entity will be deleted.
230240
"""
231-
# NOTE: pylint thinks key() is an Entity, hence key().to_protobuf()
232-
# is not defined. This is because one branch of the return
233-
# in the key() definition returns self.
234-
# pylint: disable=maybe-no-member
235-
self.dataset().connection().delete_entity(
236-
dataset_id=self.dataset().id(), key_pb=self.key().to_protobuf())
237-
# pylint: enable=maybe-no-member
241+
key = self._must_key
242+
dataset = key.dataset()
243+
dataset.connection().delete_entity(
244+
dataset_id=dataset.id(), key_pb=key.to_protobuf())
238245

239246
def __repr__(self):
240-
# An entity should have a key all the time (even if it's partial).
241-
if self.key():
242-
# pylint: disable=maybe-no-member
243-
return '<Entity%s %s>' % (self.key().path(),
247+
if self._key:
248+
return '<Entity%s %s>' % (self._key.path(),
244249
super(Entity, self).__repr__())
245-
# pylint: enable=maybe-no-member
246250
else:
247251
return '<Entity %s>' % (super(Entity, self).__repr__())

gcloud/datastore/test_entity.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ def test_from_protobuf(self):
8585
self.assertEqual(key.kind(), _KIND)
8686
self.assertEqual(key.id(), _ID)
8787

88+
def test_reload_no_key(self):
89+
from gcloud.datastore.entity import NoKey
90+
91+
entity = self._makeOne(None, None)
92+
entity['foo'] = 'Foo'
93+
self.assertRaises(NoKey, entity.reload)
94+
8895
def test_reload_miss(self):
8996
dataset = _Dataset()
9097
key = _Key(dataset)
@@ -105,6 +112,13 @@ def test_reload_hit(self):
105112
self.assertTrue(entity.reload() is entity)
106113
self.assertEqual(entity['foo'], 'Bar')
107114

115+
def test_save_no_key(self):
116+
from gcloud.datastore.entity import NoKey
117+
118+
entity = self._makeOne(None, None)
119+
entity['foo'] = 'Foo'
120+
self.assertRaises(NoKey, entity.save)
121+
108122
def test_save_wo_transaction_wo_auto_id_wo_returned_key(self):
109123
connection = _Connection()
110124
dataset = _Dataset(connection)
@@ -167,6 +181,13 @@ def test_save_w_returned_key(self):
167181
(_DATASET_ID, 'KEY', {'foo': 'Foo'}))
168182
self.assertEqual(key._path, [{'kind': _KIND, 'id': _ID}])
169183

184+
def test_delete_no_key(self):
185+
from gcloud.datastore.entity import NoKey
186+
187+
entity = self._makeOne(None, None)
188+
entity['foo'] = 'Foo'
189+
self.assertRaises(NoKey, entity.delete)
190+
170191
def test_delete(self):
171192
connection = _Connection()
172193
dataset = _Dataset(connection)
@@ -177,8 +198,23 @@ def test_delete(self):
177198
self.assertTrue(entity.delete() is None)
178199
self.assertEqual(connection._deleted, (_DATASET_ID, 'KEY'))
179200

201+
def test___repr___no_key_empty(self):
202+
entity = self._makeOne(None, None)
203+
self.assertEqual(repr(entity), '<Entity {}>')
204+
205+
def test___repr___w_key_non_empty(self):
206+
connection = _Connection()
207+
dataset = _Dataset(connection)
208+
key = _Key(dataset)
209+
key.path('/bar/baz')
210+
entity = self._makeOne()
211+
entity.key(key)
212+
entity['foo'] = 'Foo'
213+
self.assertEqual(repr(entity), "<Entity/bar/baz {'foo': 'Foo'}>")
214+
180215

181216
class _Key(object):
217+
_MARKER = object()
182218
_key = 'KEY'
183219
_partial = False
184220
_path = None
@@ -195,7 +231,9 @@ def to_protobuf(self):
195231
def is_partial(self):
196232
return self._partial
197233

198-
def path(self, path):
234+
def path(self, path=_MARKER):
235+
if path is self._MARKER:
236+
return self._path
199237
self._path = path
200238

201239

0 commit comments

Comments
 (0)