Skip to content

Commit 5cc9d0d

Browse files
authored
Continuation of ndb Property implementation (part 3) (googleapis#6318)
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
1 parent 78a1362 commit 5cc9d0d

3 files changed

Lines changed: 213 additions & 0 deletions

File tree

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

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ def __hash__(self):
337337

338338
class Property(ModelAttribute):
339339
# Instance default fallbacks provided by class.
340+
_code_name = None
340341
_name = None
341342
_indexed = True
342343
_repeated = False
@@ -746,6 +747,111 @@ def _do_validate(self, value):
746747

747748
return value
748749

750+
def _fix_up(self, cls, code_name):
751+
"""Internal helper called to tell the property its name.
752+
753+
This is called by :meth:`_fix_up_properties`, which is called by
754+
:class:`MetaModel` when finishing the construction of a :class:`Model`
755+
subclass. The name passed in is the name of the class attribute to
756+
which the current property is assigned (a.k.a. the code name). Note
757+
that this means that each property instance must be assigned to (at
758+
most) one class attribute. E.g. to declare three strings, you must
759+
call create three :class`StringProperty` instances:
760+
761+
.. code-block:: python
762+
763+
class MyModel(ndb.Model):
764+
foo = ndb.StringProperty()
765+
bar = ndb.StringProperty()
766+
baz = ndb.StringProperty()
767+
768+
you cannot write:
769+
770+
.. code-block:: python
771+
772+
class MyModel(ndb.Model):
773+
foo = bar = baz = ndb.StringProperty()
774+
775+
Args:
776+
cls (type): The class that the property is stored on. This argument
777+
is unused by this method, but may be used by subclasses.
778+
code_name (str): The name (on the class) that refers to this
779+
property.
780+
"""
781+
self._code_name = code_name
782+
if self._name is None:
783+
self._name = code_name
784+
785+
def _store_value(self, entity, value):
786+
"""Store a value in an entity for this property.
787+
788+
This assumes validation has already taken place. For a repeated
789+
property the value should be a list.
790+
791+
Args:
792+
entity (Model): An entity to set a value on.
793+
value (Any): The value to be stored for this property.
794+
"""
795+
entity._values[self._name] = value
796+
797+
def _set_value(self, entity, value):
798+
"""Set a value in an entity for a property.
799+
800+
This performs validation first. For a repeated property the value
801+
should be a list (or similar container).
802+
803+
Args:
804+
entity (Model): An entity to set a value on.
805+
value (Any): The value to be stored for this property.
806+
807+
Raises:
808+
ReadonlyPropertyError: If the ``entity`` is the result of a
809+
projection query.
810+
.BadValueError: If the current property is repeated but the
811+
``value`` is not a basic container (:class:`list`,
812+
:class:`tuple`, :class:`set` or :class:`frozenset`).
813+
"""
814+
if entity._projection:
815+
raise ReadonlyPropertyError(
816+
"You cannot set property values of a projection entity"
817+
)
818+
819+
if self._repeated:
820+
if not isinstance(value, (list, tuple, set, frozenset)):
821+
raise exceptions.BadValueError(
822+
"Expected list or tuple, got {!r}".format(value)
823+
)
824+
value = [self._do_validate(v) for v in value]
825+
else:
826+
if value is not None:
827+
value = self._do_validate(value)
828+
829+
self._store_value(entity, value)
830+
831+
def _has_value(self, entity, unused_rest=None):
832+
"""Determine if the entity has a value for this property.
833+
834+
Args:
835+
entity (Model): An entity to check if the current property has
836+
a value set.
837+
unused_rest (None): An always unused keyword.
838+
"""
839+
return self._name in entity._values
840+
841+
def _retrieve_value(self, entity, default=None):
842+
"""Retrieve the value for this property from an entity.
843+
844+
This returns :data:`None` if no value is set, or the ``default``
845+
argument if given. For a repeated property this returns a list if a
846+
value is set, otherwise :data:`None`. No additional transformations
847+
are applied.
848+
849+
Args:
850+
entity (Model): An entity to get a value from.
851+
default (Optional[Any]): The default value to use as fallback.
852+
"""
853+
return entity._values.get(self._name, default)
854+
749855
def _call_to_base_type(self, value):
750856
"""Call all ``_validate()`` and ``_to_base_type()`` methods on value.
751857

ndb/tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ def property_clean_cache():
3030
This property is set at runtime (with calls to ``_find_methods()``), so
3131
this fixture allows resetting the class to its original state.
3232
"""
33+
assert model.Property._FIND_METHODS_CACHE == {}
3334
try:
3435
yield
3536
finally:
37+
assert model.Property._FIND_METHODS_CACHE != {}
3638
model.Property._FIND_METHODS_CACHE.clear()

ndb/tests/unit/test_model.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,111 @@ def _validate(self, value):
645645
assert result is value
646646
assert value == ["SimpleProperty._validate"]
647647

648+
@staticmethod
649+
def test__fix_up():
650+
prop = model.Property(name="foo")
651+
assert prop._code_name is None
652+
prop._fix_up(None, "bar")
653+
assert prop._code_name == "bar"
654+
655+
@staticmethod
656+
def test__fix_up_no_name():
657+
prop = model.Property()
658+
assert prop._name is None
659+
assert prop._code_name is None
660+
661+
prop._fix_up(None, "both")
662+
assert prop._code_name == "both"
663+
assert prop._name == "both"
664+
665+
@staticmethod
666+
def test__store_value():
667+
entity = unittest.mock.Mock(_values={}, spec=("_values",))
668+
prop = model.Property(name="foo")
669+
prop._store_value(entity, unittest.mock.sentinel.value)
670+
assert entity._values == {prop._name: unittest.mock.sentinel.value}
671+
672+
@staticmethod
673+
def test__set_value(property_clean_cache):
674+
entity = unittest.mock.Mock(
675+
_projection=False,
676+
_values={},
677+
spec=("_projection", "_values"),
678+
)
679+
prop = model.Property(name="foo", repeated=False)
680+
prop._set_value(entity, 19)
681+
assert entity._values == {prop._name: 19}
682+
683+
@staticmethod
684+
def test__set_value_none():
685+
entity = unittest.mock.Mock(
686+
_projection=False,
687+
_values={},
688+
spec=("_projection", "_values"),
689+
)
690+
prop = model.Property(name="foo", repeated=False)
691+
prop._set_value(entity, None)
692+
assert entity._values == {prop._name: None}
693+
# Cache is untouched.
694+
assert model.Property._FIND_METHODS_CACHE == {}
695+
696+
@staticmethod
697+
def test__set_value_repeated(property_clean_cache):
698+
entity = unittest.mock.Mock(
699+
_projection=False,
700+
_values={},
701+
spec=("_projection", "_values"),
702+
)
703+
prop = model.Property(name="foo", repeated=True)
704+
prop._set_value(entity, (11, 12, 13))
705+
assert entity._values == {prop._name: [11, 12, 13]}
706+
707+
@staticmethod
708+
def test__set_value_repeated_bad_container():
709+
entity = unittest.mock.Mock(
710+
_projection=False,
711+
_values={},
712+
spec=("_projection", "_values"),
713+
)
714+
prop = model.Property(name="foo", repeated=True)
715+
with pytest.raises(exceptions.BadValueError):
716+
prop._set_value(entity, None)
717+
# Cache is untouched.
718+
assert model.Property._FIND_METHODS_CACHE == {}
719+
720+
@staticmethod
721+
def test__set_value_projection():
722+
entity = unittest.mock.Mock(
723+
_projection=True,
724+
spec=("_projection",),
725+
)
726+
prop = model.Property(name="foo", repeated=True)
727+
with pytest.raises(model.ReadonlyPropertyError):
728+
prop._set_value(entity, None)
729+
# Cache is untouched.
730+
assert model.Property._FIND_METHODS_CACHE == {}
731+
732+
@staticmethod
733+
def test__has_value():
734+
prop = model.Property(name="foo")
735+
values = {prop._name: 88}
736+
entity1 = unittest.mock.Mock(_values=values, spec=("_values",))
737+
entity2 = unittest.mock.Mock(_values={}, spec=("_values",))
738+
739+
assert prop._has_value(entity1)
740+
assert not prop._has_value(entity2)
741+
742+
@staticmethod
743+
def test__retrieve_value():
744+
prop = model.Property(name="foo")
745+
values = {prop._name: b"\x00\x01"}
746+
entity1 = unittest.mock.Mock(_values=values, spec=("_values",))
747+
entity2 = unittest.mock.Mock(_values={}, spec=("_values",))
748+
749+
assert prop._retrieve_value(entity1) == b"\x00\x01"
750+
assert prop._retrieve_value(entity2) is None
751+
assert prop._retrieve_value(entity2, default=b"zip") == b"zip"
752+
648753
@staticmethod
649754
def _property_subtype_chain():
650755
class A(model.Property):

0 commit comments

Comments
 (0)