Skip to content

Commit

Permalink
Continuation of ndb Property implementation (part 3) (googleapis#6318)
Browse files Browse the repository at this point in the history
This is still incomplete, it's a very large class.

In particular, this implements:

- Adds `Property._code_name` class and instance attribute
- `Property._fix_up`
- `Property._store_value`
- `Property._set_value`
- `Property._has_value`
- `Property._retrieve_value`
- Modifies `property_clean_cache` fixture to make sure the cache
  is empty on fixture entry and make sure the cache is
  non-empty before clearing
  • Loading branch information
dhermes authored Oct 26, 2018
1 parent 78a1362 commit 5cc9d0d
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 0 deletions.
106 changes: 106 additions & 0 deletions ndb/src/google/cloud/ndb/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ def __hash__(self):

class Property(ModelAttribute):
# Instance default fallbacks provided by class.
_code_name = None
_name = None
_indexed = True
_repeated = False
Expand Down Expand Up @@ -746,6 +747,111 @@ def _do_validate(self, value):

return value

def _fix_up(self, cls, code_name):
"""Internal helper called to tell the property its name.
This is called by :meth:`_fix_up_properties`, which is called by
:class:`MetaModel` when finishing the construction of a :class:`Model`
subclass. The name passed in is the name of the class attribute to
which the current property is assigned (a.k.a. the code name). Note
that this means that each property instance must be assigned to (at
most) one class attribute. E.g. to declare three strings, you must
call create three :class`StringProperty` instances:
.. code-block:: python
class MyModel(ndb.Model):
foo = ndb.StringProperty()
bar = ndb.StringProperty()
baz = ndb.StringProperty()
you cannot write:
.. code-block:: python
class MyModel(ndb.Model):
foo = bar = baz = ndb.StringProperty()
Args:
cls (type): The class that the property is stored on. This argument
is unused by this method, but may be used by subclasses.
code_name (str): The name (on the class) that refers to this
property.
"""
self._code_name = code_name
if self._name is None:
self._name = code_name

def _store_value(self, entity, value):
"""Store a value in an entity for this property.
This assumes validation has already taken place. For a repeated
property the value should be a list.
Args:
entity (Model): An entity to set a value on.
value (Any): The value to be stored for this property.
"""
entity._values[self._name] = value

def _set_value(self, entity, value):
"""Set a value in an entity for a property.
This performs validation first. For a repeated property the value
should be a list (or similar container).
Args:
entity (Model): An entity to set a value on.
value (Any): The value to be stored for this property.
Raises:
ReadonlyPropertyError: If the ``entity`` is the result of a
projection query.
.BadValueError: If the current property is repeated but the
``value`` is not a basic container (:class:`list`,
:class:`tuple`, :class:`set` or :class:`frozenset`).
"""
if entity._projection:
raise ReadonlyPropertyError(
"You cannot set property values of a projection entity"
)

if self._repeated:
if not isinstance(value, (list, tuple, set, frozenset)):
raise exceptions.BadValueError(
"Expected list or tuple, got {!r}".format(value)
)
value = [self._do_validate(v) for v in value]
else:
if value is not None:
value = self._do_validate(value)

self._store_value(entity, value)

def _has_value(self, entity, unused_rest=None):
"""Determine if the entity has a value for this property.
Args:
entity (Model): An entity to check if the current property has
a value set.
unused_rest (None): An always unused keyword.
"""
return self._name in entity._values

def _retrieve_value(self, entity, default=None):
"""Retrieve the value for this property from an entity.
This returns :data:`None` if no value is set, or the ``default``
argument if given. For a repeated property this returns a list if a
value is set, otherwise :data:`None`. No additional transformations
are applied.
Args:
entity (Model): An entity to get a value from.
default (Optional[Any]): The default value to use as fallback.
"""
return entity._values.get(self._name, default)

def _call_to_base_type(self, value):
"""Call all ``_validate()`` and ``_to_base_type()`` methods on value.
Expand Down
2 changes: 2 additions & 0 deletions ndb/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ def property_clean_cache():
This property is set at runtime (with calls to ``_find_methods()``), so
this fixture allows resetting the class to its original state.
"""
assert model.Property._FIND_METHODS_CACHE == {}
try:
yield
finally:
assert model.Property._FIND_METHODS_CACHE != {}
model.Property._FIND_METHODS_CACHE.clear()
105 changes: 105 additions & 0 deletions ndb/tests/unit/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,111 @@ def _validate(self, value):
assert result is value
assert value == ["SimpleProperty._validate"]

@staticmethod
def test__fix_up():
prop = model.Property(name="foo")
assert prop._code_name is None
prop._fix_up(None, "bar")
assert prop._code_name == "bar"

@staticmethod
def test__fix_up_no_name():
prop = model.Property()
assert prop._name is None
assert prop._code_name is None

prop._fix_up(None, "both")
assert prop._code_name == "both"
assert prop._name == "both"

@staticmethod
def test__store_value():
entity = unittest.mock.Mock(_values={}, spec=("_values",))
prop = model.Property(name="foo")
prop._store_value(entity, unittest.mock.sentinel.value)
assert entity._values == {prop._name: unittest.mock.sentinel.value}

@staticmethod
def test__set_value(property_clean_cache):
entity = unittest.mock.Mock(
_projection=False,
_values={},
spec=("_projection", "_values"),
)
prop = model.Property(name="foo", repeated=False)
prop._set_value(entity, 19)
assert entity._values == {prop._name: 19}

@staticmethod
def test__set_value_none():
entity = unittest.mock.Mock(
_projection=False,
_values={},
spec=("_projection", "_values"),
)
prop = model.Property(name="foo", repeated=False)
prop._set_value(entity, None)
assert entity._values == {prop._name: None}
# Cache is untouched.
assert model.Property._FIND_METHODS_CACHE == {}

@staticmethod
def test__set_value_repeated(property_clean_cache):
entity = unittest.mock.Mock(
_projection=False,
_values={},
spec=("_projection", "_values"),
)
prop = model.Property(name="foo", repeated=True)
prop._set_value(entity, (11, 12, 13))
assert entity._values == {prop._name: [11, 12, 13]}

@staticmethod
def test__set_value_repeated_bad_container():
entity = unittest.mock.Mock(
_projection=False,
_values={},
spec=("_projection", "_values"),
)
prop = model.Property(name="foo", repeated=True)
with pytest.raises(exceptions.BadValueError):
prop._set_value(entity, None)
# Cache is untouched.
assert model.Property._FIND_METHODS_CACHE == {}

@staticmethod
def test__set_value_projection():
entity = unittest.mock.Mock(
_projection=True,
spec=("_projection",),
)
prop = model.Property(name="foo", repeated=True)
with pytest.raises(model.ReadonlyPropertyError):
prop._set_value(entity, None)
# Cache is untouched.
assert model.Property._FIND_METHODS_CACHE == {}

@staticmethod
def test__has_value():
prop = model.Property(name="foo")
values = {prop._name: 88}
entity1 = unittest.mock.Mock(_values=values, spec=("_values",))
entity2 = unittest.mock.Mock(_values={}, spec=("_values",))

assert prop._has_value(entity1)
assert not prop._has_value(entity2)

@staticmethod
def test__retrieve_value():
prop = model.Property(name="foo")
values = {prop._name: b"\x00\x01"}
entity1 = unittest.mock.Mock(_values=values, spec=("_values",))
entity2 = unittest.mock.Mock(_values={}, spec=("_values",))

assert prop._retrieve_value(entity1) == b"\x00\x01"
assert prop._retrieve_value(entity2) is None
assert prop._retrieve_value(entity2, default=b"zip") == b"zip"

@staticmethod
def _property_subtype_chain():
class A(model.Property):
Expand Down

0 comments on commit 5cc9d0d

Please sign in to comment.