Skip to content

Commit 433dcf9

Browse files
authored
Continuation of ndb Property implementation (part 5) (googleapis#6333)
This is still incomplete, it's a very large class. In particular, this implements: - `Property._get_value` - `Property._delete_value` - `Property._is_initialized` - Descriptors for `Property` (i.e. `__get__`, `__set__` and `__delete__`) - `Property._prepare_for_put` - `Property._check_property` - `Property._get_for_dict`
1 parent 65b6f85 commit 433dcf9

File tree

2 files changed

+298
-0
lines changed

2 files changed

+298
-0
lines changed

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

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,157 @@ def _apply_to_values(self, entity, function):
11901190

11911191
return value
11921192

1193+
def _get_value(self, entity):
1194+
"""Get the value for this property from an entity.
1195+
1196+
For a repeated property this initializes the value to an empty
1197+
list if it is not set.
1198+
1199+
Args:
1200+
entity (Model): An entity to get a value from.
1201+
1202+
Returns:
1203+
Any: The user value stored for the current property.
1204+
1205+
Raises:
1206+
UnprojectedPropertyError: If the ``entity`` is the result of a
1207+
projection query and the current property is not one of the
1208+
projected properties.
1209+
"""
1210+
if entity._projection:
1211+
if self._name not in entity._projection:
1212+
raise UnprojectedPropertyError(
1213+
"Property {} is not in the projection".format(self._name)
1214+
)
1215+
1216+
return self._get_user_value(entity)
1217+
1218+
def _delete_value(self, entity):
1219+
"""Delete the value for this property from an entity.
1220+
1221+
.. note::
1222+
1223+
If no value exists this is a no-op; deleted values will not be
1224+
serialized but requesting their value will return :data:`None` (or
1225+
an empty list in the case of a repeated property).
1226+
1227+
Args:
1228+
entity (Model): An entity to get a value from.
1229+
"""
1230+
if self._name in entity._values:
1231+
del entity._values[self._name]
1232+
1233+
def _is_initialized(self, entity):
1234+
"""Ask if the entity has a value for this property.
1235+
1236+
This returns :data:`False` if a value is stored but the stored value
1237+
is :data:`None`.
1238+
1239+
Args:
1240+
entity (Model): An entity to get a value from.
1241+
"""
1242+
return not self._required or (
1243+
(self._has_value(entity) or self._default is not None)
1244+
and self._get_value(entity) is not None
1245+
)
1246+
1247+
def __get__(self, entity, unused_cls=None):
1248+
"""Descriptor protocol: get the value from the entity.
1249+
1250+
Args:
1251+
entity (Model): An entity to get a value from.
1252+
unused_cls (type): The class that owns this instance.
1253+
"""
1254+
if entity is None:
1255+
# Handle the case where ``__get__`` is called on the class
1256+
# rather than an instance.
1257+
return self
1258+
return self._get_value(entity)
1259+
1260+
def __set__(self, entity, value):
1261+
"""Descriptor protocol: set the value on the entity.
1262+
1263+
Args:
1264+
entity (Model): An entity to set a value on.
1265+
value (Any): The value to set.
1266+
"""
1267+
self._set_value(entity, value)
1268+
1269+
def __delete__(self, entity):
1270+
"""Descriptor protocol: delete the value from the entity.
1271+
1272+
Args:
1273+
entity (Model): An entity to delete a value from.
1274+
"""
1275+
self._delete_value(entity)
1276+
1277+
def _prepare_for_put(self, entity):
1278+
"""Allow this property to define a pre-put hook.
1279+
1280+
This base class implementation does nothing, but subclasses may
1281+
provide hooks.
1282+
1283+
Args:
1284+
entity (Model): An entity with values.
1285+
"""
1286+
pass
1287+
1288+
def _check_property(self, rest=None, require_indexed=True):
1289+
"""Check this property for specific requirements.
1290+
1291+
Called by ``Model._check_properties()``.
1292+
1293+
Args:
1294+
rest: Optional subproperty to check, of the form
1295+
``name1.name2...nameN``.
1296+
required_indexed (bool): Indicates if the current property must
1297+
be indexed.
1298+
1299+
Raises:
1300+
InvalidPropertyError: If ``require_indexed`` is :data:`True`
1301+
but the current property is not indexed.
1302+
InvalidPropertyError: If a subproperty is specified via ``rest``
1303+
(:class:`StructuredProperty` overrides this method to handle
1304+
subproperties).
1305+
"""
1306+
if require_indexed and not self._indexed:
1307+
raise InvalidPropertyError(
1308+
"Property is unindexed {}".format(self._name)
1309+
)
1310+
1311+
if rest:
1312+
raise InvalidPropertyError(
1313+
"Referencing subproperty {}.{} but {} is not a structured "
1314+
"property".format(self._name, rest, self._name)
1315+
)
1316+
1317+
def _get_for_dict(self, entity):
1318+
"""Retrieve the value like ``_get_value()``.
1319+
1320+
This is intended to be processed for ``_to_dict()``.
1321+
1322+
Property subclasses can override this if they want the dictionary
1323+
returned by ``entity._to_dict()`` to contain a different value. The
1324+
main use case is allowing :class:`StructuredProperty` and
1325+
:class:`LocalStructuredProperty` to allow the default ``_get_value()``
1326+
behavior.
1327+
1328+
* If you override ``_get_for_dict()`` to return a different type, you
1329+
must override ``_validate()`` to accept values of that type and
1330+
convert them back to the original type.
1331+
1332+
* If you override ``_get_for_dict()``, you must handle repeated values
1333+
and :data:`None` correctly. However, ``_validate()`` does not need to
1334+
handle these.
1335+
1336+
Args:
1337+
entity (Model): An entity to get a value from.
1338+
1339+
Returns:
1340+
Any: The user value stored for the current property.
1341+
"""
1342+
return self._get_value(entity)
1343+
11931344

11941345
class ModelKey(Property):
11951346
__slots__ = ()

ndb/tests/unit/test_model.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,153 @@ def test__apply_to_values_repeated_when_none():
11511151
# Check mocks.
11521152
function.assert_not_called()
11531153

1154+
@staticmethod
1155+
def test__get_value():
1156+
prop = model.Property(name="prop")
1157+
value = b"\x00\x01"
1158+
values = {prop._name: value}
1159+
entity = unittest.mock.Mock(
1160+
_projection=None, _values=values, spec=("_projection", "_values")
1161+
)
1162+
assert value is prop._get_value(entity)
1163+
# Cache is untouched.
1164+
assert model.Property._FIND_METHODS_CACHE == {}
1165+
1166+
@staticmethod
1167+
def test__get_value_projected_present():
1168+
prop = model.Property(name="prop")
1169+
value = 92.5
1170+
values = {prop._name: value}
1171+
entity = unittest.mock.Mock(
1172+
_projection=(prop._name,),
1173+
_values=values,
1174+
spec=("_projection", "_values"),
1175+
)
1176+
assert value is prop._get_value(entity)
1177+
# Cache is untouched.
1178+
assert model.Property._FIND_METHODS_CACHE == {}
1179+
1180+
@staticmethod
1181+
def test__get_value_projected_absent():
1182+
prop = model.Property(name="prop")
1183+
entity = unittest.mock.Mock(
1184+
_projection=("nope",), spec=("_projection",)
1185+
)
1186+
with pytest.raises(model.UnprojectedPropertyError):
1187+
prop._get_value(entity)
1188+
# Cache is untouched.
1189+
assert model.Property._FIND_METHODS_CACHE == {}
1190+
1191+
@staticmethod
1192+
def test__delete_value():
1193+
prop = model.Property(name="prop")
1194+
value = b"\x00\x01"
1195+
values = {prop._name: value}
1196+
entity = unittest.mock.Mock(_values=values, spec=("_values",))
1197+
prop._delete_value(entity)
1198+
assert values == {}
1199+
1200+
@staticmethod
1201+
def test__delete_value_no_op():
1202+
prop = model.Property(name="prop")
1203+
values = {}
1204+
entity = unittest.mock.Mock(_values=values, spec=("_values",))
1205+
prop._delete_value(entity)
1206+
assert values == {}
1207+
1208+
@staticmethod
1209+
def test__is_initialized_not_required():
1210+
prop = model.Property(name="prop", required=False)
1211+
entity = unittest.mock.sentinel.entity
1212+
assert prop._is_initialized(entity)
1213+
# Cache is untouched.
1214+
assert model.Property._FIND_METHODS_CACHE == {}
1215+
1216+
@staticmethod
1217+
def test__is_initialized_default_fallback():
1218+
prop = model.Property(name="prop", required=True, default=11111)
1219+
values = {}
1220+
entity = unittest.mock.Mock(
1221+
_projection=None, _values=values, spec=("_projection", "_values")
1222+
)
1223+
assert prop._is_initialized(entity)
1224+
# Cache is untouched.
1225+
assert model.Property._FIND_METHODS_CACHE == {}
1226+
1227+
@staticmethod
1228+
def test__is_initialized_set_to_none():
1229+
prop = model.Property(name="prop", required=True)
1230+
values = {prop._name: None}
1231+
entity = unittest.mock.Mock(
1232+
_projection=None, _values=values, spec=("_projection", "_values")
1233+
)
1234+
assert not prop._is_initialized(entity)
1235+
# Cache is untouched.
1236+
assert model.Property._FIND_METHODS_CACHE == {}
1237+
1238+
@staticmethod
1239+
def test_instance_descriptors(property_clean_cache):
1240+
class Model:
1241+
prop = model.Property(name="prop", required=True)
1242+
1243+
def __init__(self):
1244+
self._projection = None
1245+
self._values = {}
1246+
1247+
m = Model()
1248+
value = 1234.5
1249+
# __set__
1250+
m.prop = value
1251+
assert m._values == {b"prop": value}
1252+
# __get__
1253+
assert m.prop == value
1254+
# __delete__
1255+
del m.prop
1256+
assert m._values == {}
1257+
1258+
@staticmethod
1259+
def test_class_descriptors():
1260+
prop = model.Property(name="prop", required=True)
1261+
1262+
class Model:
1263+
prop2 = prop
1264+
1265+
assert Model.prop2 is prop
1266+
1267+
@staticmethod
1268+
def test__prepare_for_put():
1269+
prop = model.Property(name="prop")
1270+
assert prop._prepare_for_put(None) is None
1271+
1272+
@staticmethod
1273+
def test__check_property():
1274+
prop = model.Property(name="prop")
1275+
assert prop._check_property() is None
1276+
1277+
@staticmethod
1278+
def test__check_property_not_indexed():
1279+
prop = model.Property(name="prop", indexed=False)
1280+
with pytest.raises(model.InvalidPropertyError):
1281+
prop._check_property(require_indexed=True)
1282+
1283+
@staticmethod
1284+
def test__check_property_with_subproperty():
1285+
prop = model.Property(name="prop", indexed=True)
1286+
with pytest.raises(model.InvalidPropertyError):
1287+
prop._check_property(rest="a.b.c")
1288+
1289+
@staticmethod
1290+
def test__get_for_dict():
1291+
prop = model.Property(name="prop")
1292+
value = b"\x00\x01"
1293+
values = {prop._name: value}
1294+
entity = unittest.mock.Mock(
1295+
_projection=None, _values=values, spec=("_projection", "_values")
1296+
)
1297+
assert value is prop._get_for_dict(entity)
1298+
# Cache is untouched.
1299+
assert model.Property._FIND_METHODS_CACHE == {}
1300+
11541301

11551302
class TestModelKey:
11561303
@staticmethod

0 commit comments

Comments
 (0)