Skip to content

Commit fa00f9a

Browse files
author
Chris Rossi
authored
Fix Structured Properties (#102)
Fixes #101.
1 parent 7118d9f commit fa00f9a

File tree

5 files changed

+194
-72
lines changed

5 files changed

+194
-72
lines changed

packages/google-cloud-ndb/src/google/cloud/ndb/model.py

Lines changed: 107 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -340,17 +340,15 @@ def _entity_from_protobuf(protobuf):
340340
return _entity_from_ds_entity(ds_entity)
341341

342342

343-
def _entity_to_protobuf(entity, set_key=True):
344-
"""Serialize an entity to a protobuffer.
343+
def _entity_to_ds_entity(entity, set_key=True):
344+
"""Convert an NDB entity to Datastore entity.
345345
346346
Args:
347-
entity (Model): The entity to be serialized.
347+
entity (Model): The entity to be converted.
348348
349349
Returns:
350-
google.cloud.datastore_v1.types.Entity: The protocol buffer
351-
representation.
350+
google.cloud.datastore.entity.Entity: The converted entity.
352351
"""
353-
# First, make a datastore entity
354352
data = {}
355353
for cls in type(entity).mro():
356354
for prop in cls.__dict__.values():
@@ -376,7 +374,20 @@ def _entity_to_protobuf(entity, set_key=True):
376374
ds_entity = entity_module.Entity()
377375
ds_entity.update(data)
378376

379-
# Then, use datatore to get the protocol buffer
377+
return ds_entity
378+
379+
380+
def _entity_to_protobuf(entity, set_key=True):
381+
"""Serialize an entity to a protocol buffer.
382+
383+
Args:
384+
entity (Model): The entity to be serialized.
385+
386+
Returns:
387+
google.cloud.datastore_v1.types.Entity: The protocol buffer
388+
representation.
389+
"""
390+
ds_entity = _entity_to_ds_entity(entity, set_key=set_key)
380391
return helpers.entity_to_protobuf(ds_entity)
381392

382393

@@ -3362,29 +3373,29 @@ class StructuredProperty(Property):
33623373
The values of the sub-entity are indexed and can be queried.
33633374
"""
33643375

3365-
_modelclass = None
3376+
_model_class = None
33663377
_kwargs = None
33673378

3368-
def __init__(self, modelclass, name=None, **kwargs):
3379+
def __init__(self, model_class, name=None, **kwargs):
33693380
super(StructuredProperty, self).__init__(name=name, **kwargs)
33703381
if self._repeated:
3371-
if modelclass._has_repeated:
3382+
if model_class._has_repeated:
33723383
raise TypeError(
33733384
"This StructuredProperty cannot use repeated=True "
33743385
"because its model class (%s) contains repeated "
33753386
"properties (directly or indirectly)."
3376-
% modelclass.__name__
3387+
% model_class.__name__
33773388
)
3378-
self._modelclass = modelclass
3389+
self._model_class = model_class
33793390

33803391
def _get_value(self, entity):
33813392
"""Override _get_value() to *not* raise UnprojectedPropertyError.
33823393
3383-
This is necessary because the projection must include both the sub-entity and
3384-
the property name that is projected (e.g. 'foo.bar' instead of only 'foo'). In
3385-
that case the original code would fail, because it only looks for the property
3386-
name ('foo'). Here we check for a value, and only call the original code if the
3387-
value is None.
3394+
This is necessary because the projection must include both the
3395+
sub-entity and the property name that is projected (e.g. 'foo.bar'
3396+
instead of only 'foo'). In that case the original code would fail,
3397+
because it only looks for the property name ('foo'). Here we check for
3398+
a value, and only call the original code if the value is None.
33883399
"""
33893400
value = self._get_user_value(entity)
33903401
if value is None and entity._projection:
@@ -3403,11 +3414,11 @@ def _get_for_dict(self, entity):
34033414
def __getattr__(self, attrname):
34043415
"""Dynamically get a subproperty."""
34053416
# Optimistically try to use the dict key.
3406-
prop = self._modelclass._properties.get(attrname)
3417+
prop = self._model_class._properties.get(attrname)
34073418
if prop is None:
34083419
raise AttributeError(
34093420
"Model subclass %s has no attribute %s"
3410-
% (self._modelclass.__name__, attrname)
3421+
% (self._model_class.__name__, attrname)
34113422
)
34123423
prop_copy = copy.copy(prop)
34133424
prop_copy._name = self._name + "." + prop_copy._name
@@ -3436,37 +3447,41 @@ def _comparison(self, op, value):
34363447
) # Import late to avoid circular imports.
34373448

34383449
return FilterNode(self._name, op, value)
3450+
34393451
value = self._do_validate(value)
3440-
value = self._call_to_base_type(value)
34413452
filters = []
34423453
match_keys = []
3443-
for prop in self._modelclass._properties.values():
3444-
vals = prop._get_base_value_unwrapped_as_list(value)
3454+
for prop in self._model_class._properties.values():
3455+
subvalue = prop._get_value(value)
34453456
if prop._repeated:
3446-
if vals: # pragma: no branch
3457+
if subvalue: # pragma: no branch
34473458
raise exceptions.BadFilterError(
34483459
"Cannot query for non-empty repeated property %s"
34493460
% prop._name
34503461
)
34513462
continue # pragma: NO COVER
3452-
val = vals[0]
3453-
if val is not None: # pragma: no branch
3463+
3464+
if subvalue is not None: # pragma: no branch
34543465
altprop = getattr(self, prop._code_name)
3455-
filt = altprop._comparison(op, val)
3466+
filt = altprop._comparison(op, subvalue)
34563467
filters.append(filt)
34573468
match_keys.append(altprop._name)
3469+
34583470
if not filters:
34593471
raise exceptions.BadFilterError(
34603472
"StructuredProperty filter without any values"
34613473
)
3474+
34623475
if len(filters) == 1:
34633476
return filters[0]
3477+
34643478
if self._repeated:
34653479
raise NotImplementedError("This depends on code not yet ported.")
34663480
# pb = value._to_pb(allow_partial=True)
34673481
# pred = RepeatedStructuredPropertyPredicate(match_keys, pb,
34683482
# self._name + '.')
34693483
# filters.append(PostFilterNode(pred))
3484+
34703485
return ConjunctionNode(*filters)
34713486

34723487
def _IN(self, value):
@@ -3491,11 +3506,11 @@ def _IN(self, value):
34913506
def _validate(self, value):
34923507
if isinstance(value, dict):
34933508
# A dict is assumed to be the result of a _to_dict() call.
3494-
return self._modelclass(**value)
3495-
if not isinstance(value, self._modelclass):
3509+
return self._model_class(**value)
3510+
if not isinstance(value, self._model_class):
34963511
raise exceptions.BadValueError(
34973512
"Expected %s instance, got %s"
3498-
% (self._modelclass.__name__, value.__class__)
3513+
% (self._model_class.__name__, value.__class__)
34993514
)
35003515

35013516
def _has_value(self, entity, rest=None):
@@ -3507,27 +3522,34 @@ def _has_value(self, entity, rest=None):
35073522
35083523
Args:
35093524
entity (ndb.Model): An instance of a model.
3510-
rest (list[str]): optional list of attribute names to check in addition.
3525+
rest (list[str]): optional list of attribute names to check in
3526+
addition.
35113527
35123528
Returns:
35133529
bool: True if the entity has a value for that property.
35143530
"""
35153531
ok = super(StructuredProperty, self)._has_value(entity)
35163532
if ok and rest:
3517-
lst = self._get_base_value_unwrapped_as_list(entity)
3518-
if len(lst) != 1:
3519-
raise RuntimeError(
3520-
"Failed to retrieve sub-entity of StructuredProperty"
3521-
" %s" % self._name
3522-
)
3523-
subent = lst[0]
3533+
value = self._get_value(entity)
3534+
if self._repeated:
3535+
if len(value) != 1:
3536+
raise RuntimeError(
3537+
"Failed to retrieve sub-entity of StructuredProperty"
3538+
" %s" % self._name
3539+
)
3540+
subent = value[0]
3541+
else:
3542+
subent = value
3543+
35243544
if subent is None:
35253545
return True
3546+
35263547
subprop = subent._properties.get(rest[0])
35273548
if subprop is None:
35283549
ok = False
35293550
else:
35303551
ok = subprop._has_value(subent, rest[1:])
3552+
35313553
return ok
35323554

35333555
def _check_property(self, rest=None, require_indexed=True):
@@ -3541,15 +3563,42 @@ def _check_property(self, rest=None, require_indexed=True):
35413563
raise InvalidPropertyError(
35423564
"Structured property %s requires a subproperty" % self._name
35433565
)
3544-
self._modelclass._check_properties(
3566+
self._model_class._check_properties(
35453567
[rest], require_indexed=require_indexed
35463568
)
35473569

3548-
def _get_base_value_at_index(self, entity, index):
3549-
assert self._repeated
3550-
value = self._retrieve_value(entity, self._default)
3551-
value[index] = self._opt_call_to_base_type(value[index])
3552-
return value[index].b_val
3570+
def _to_base_type(self, value):
3571+
"""Convert a value to the "base" value type for this property.
3572+
3573+
Args:
3574+
value: The given class value to be converted.
3575+
3576+
Returns:
3577+
bytes
3578+
3579+
Raises:
3580+
TypeError: If ``value`` is not the correct ``Model`` type.
3581+
"""
3582+
if not isinstance(value, self._model_class):
3583+
raise TypeError(
3584+
"Cannot convert to protocol buffer. Expected {} value; "
3585+
"received {}".format(self._model_class.__name__, value)
3586+
)
3587+
return _entity_to_ds_entity(value)
3588+
3589+
def _from_base_type(self, value):
3590+
"""Convert a value from the "base" value type for this property.
3591+
Args:
3592+
value(~google.cloud.datastore.Entity or bytes): The value to be
3593+
converted.
3594+
Returns:
3595+
The converted value with given class.
3596+
"""
3597+
if isinstance(value, entity_module.Entity):
3598+
value = _entity_from_ds_entity(
3599+
value, model_class=self._model_class
3600+
)
3601+
return value
35533602

35543603
def _get_value_size(self, entity):
35553604
values = self._retrieve_value(entity, self._default)
@@ -3569,7 +3618,8 @@ class LocalStructuredProperty(BlobProperty):
35693618
.. automethod:: _from_base_type
35703619
.. automethod:: _validate
35713620
Args:
3572-
kls (ndb.Model): The class of the property.
3621+
model_class (type): The class of the property. (Must be subclass of
3622+
``ndb.Model``.)
35733623
name (str): The name of the property.
35743624
compressed (bool): Indicates if the value should be compressed (via
35753625
``zlib``).
@@ -3585,19 +3635,19 @@ class LocalStructuredProperty(BlobProperty):
35853635
to the datastore.
35863636
"""
35873637

3588-
_kls = None
3638+
_model_class = None
35893639
_keep_keys = False
35903640
_kwargs = None
35913641

3592-
def __init__(self, kls, **kwargs):
3642+
def __init__(self, model_class, **kwargs):
35933643
indexed = kwargs.pop("indexed", False)
35943644
if indexed:
35953645
raise NotImplementedError(
35963646
"Cannot index LocalStructuredProperty {}.".format(self._name)
35973647
)
35983648
keep_keys = kwargs.pop("keep_keys", False)
35993649
super(LocalStructuredProperty, self).__init__(**kwargs)
3600-
self._kls = kls
3650+
self._model_class = model_class
36013651
self._keep_keys = keep_keys
36023652

36033653
def _validate(self, value):
@@ -3609,11 +3659,13 @@ def _validate(self, value):
36093659
"""
36103660
if isinstance(value, dict):
36113661
# A dict is assumed to be the result of a _to_dict() call.
3612-
value = self._kls(**value)
3662+
value = self._model_class(**value)
36133663

3614-
if not isinstance(value, self._kls):
3664+
if not isinstance(value, self._model_class):
36153665
raise exceptions.BadValueError(
3616-
"Expected {}, got {!r}".format(self._kls.__name__, value)
3666+
"Expected {}, got {!r}".format(
3667+
self._model_class.__name__, value
3668+
)
36173669
)
36183670

36193671
def _to_base_type(self, value):
@@ -3623,12 +3675,12 @@ def _to_base_type(self, value):
36233675
Returns:
36243676
bytes
36253677
Raises:
3626-
TypeError: If ``value`` is not a given class.
3678+
TypeError: If ``value`` is not the correct ``Model`` type.
36273679
"""
3628-
if not isinstance(value, self._kls):
3680+
if not isinstance(value, self._model_class):
36293681
raise TypeError(
36303682
"Cannot convert to bytes expected {} value; "
3631-
"received {}".format(self._kls.__name__, value)
3683+
"received {}".format(self._model_class.__name__, value)
36323684
)
36333685
pb = _entity_to_protobuf(value, set_key=self._keep_keys)
36343686
return pb.SerializePartialToString()
@@ -3647,7 +3699,7 @@ def _from_base_type(self, value):
36473699
value = helpers.entity_from_protobuf(pb)
36483700
if not self._keep_keys and value.key:
36493701
value.key = None
3650-
return _entity_from_ds_entity(value, model_class=self._kls)
3702+
return _entity_from_ds_entity(value, model_class=self._model_class)
36513703

36523704

36533705
class GenericProperty(Property):
@@ -4328,7 +4380,7 @@ def _fix_up_properties(cls):
43284380
if isinstance(attr, Property):
43294381
if attr._repeated or (
43304382
isinstance(attr, StructuredProperty)
4331-
and attr._modelclass._has_repeated
4383+
and attr._model_class._has_repeated
43324384
):
43334385
cls._has_repeated = True
43344386
cls._properties[attr._name] = attr

packages/google-cloud-ndb/tests/system/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import time
1616

1717
KIND = "SomeKind"
18+
OTHER_KIND = "OtherKind"
1819
OTHER_NAMESPACE = "other-namespace"
1920

2021

packages/google-cloud-ndb/tests/system/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
from google.cloud import datastore
77
from google.cloud import ndb
88

9-
from . import KIND, OTHER_NAMESPACE
9+
from . import KIND, OTHER_KIND, OTHER_NAMESPACE
1010

1111

1212
def all_entities(client):
1313
return itertools.chain(
1414
client.query(kind=KIND).fetch(),
15+
client.query(kind=OTHER_KIND).fetch(),
1516
client.query(namespace=OTHER_NAMESPACE).fetch(),
1617
)
1718

packages/google-cloud-ndb/tests/system/test_crud.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,3 +329,26 @@ def do_the_thing():
329329

330330
entity = ndb.transaction(do_the_thing)
331331
assert entity.foo == 42
332+
333+
334+
@pytest.mark.usefixtures("client_context")
335+
def test_insert_entity_with_structured_property(dispose_of):
336+
class OtherKind(ndb.Model):
337+
one = ndb.StringProperty()
338+
two = ndb.StringProperty()
339+
340+
class SomeKind(ndb.Model):
341+
foo = ndb.IntegerProperty()
342+
bar = ndb.StructuredProperty(OtherKind)
343+
344+
entity = SomeKind(foo=42, bar=OtherKind(one="hi", two="mom"))
345+
key = entity.put()
346+
347+
retrieved = key.get()
348+
assert retrieved.foo == 42
349+
assert retrieved.bar.one == "hi"
350+
assert retrieved.bar.two == "mom"
351+
352+
assert isinstance(retrieved.bar, OtherKind)
353+
354+
dispose_of(key._key)

0 commit comments

Comments
 (0)