Skip to content

Commit 7118d9f

Browse files
authored
implement expando model (#99)
* implement expando model.
1 parent 18f4354 commit 7118d9f

File tree

2 files changed

+176
-5
lines changed

2 files changed

+176
-5
lines changed

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

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3363,6 +3363,7 @@ class StructuredProperty(Property):
33633363
"""
33643364

33653365
_modelclass = None
3366+
_kwargs = None
33663367

33673368
def __init__(self, modelclass, name=None, **kwargs):
33683369
super(StructuredProperty, self).__init__(name=name, **kwargs)
@@ -3661,6 +3662,7 @@ class GenericProperty(Property):
36613662
"""
36623663

36633664
_compressed = False
3665+
_kwargs = None
36643666

36653667
def __init__(self, name=None, compressed=False, **kwargs):
36663668
if compressed: # Compressed implies unindexed.
@@ -3734,6 +3736,8 @@ class ComputedProperty(GenericProperty):
37343736
... hash = ndb.model.ComputedProperty(_compute_hash, name='sha1')
37353737
"""
37363738

3739+
_kwargs = None
3740+
37373741
def __init__(
37383742
self, func, name=None, indexed=None, repeated=None, verbose_name=None
37393743
):
@@ -5210,10 +5214,91 @@ def _post_put_hook(self, future):
52105214

52115215

52125216
class Expando(Model):
5213-
__slots__ = ()
5217+
"""Model subclass to support dynamic Property names and types.
5218+
5219+
Sometimes the set of properties is not known ahead of time. In such
5220+
cases you can use the Expando class. This is a Model subclass that
5221+
creates properties on the fly, both upon assignment and when loading
5222+
an entity from Cloud Datastore. For example::
5223+
5224+
>>> class SuperPerson(Expando):
5225+
name = StringProperty()
5226+
superpower = StringProperty()
5227+
5228+
>>> razorgirl = SuperPerson(name='Molly Millions',
5229+
superpower='bionic eyes, razorblade hands',
5230+
rasta_name='Steppin\' Razor',
5231+
alt_name='Sally Shears')
5232+
>>> elastigirl = SuperPerson(name='Helen Parr',
5233+
superpower='stretchable body')
5234+
>>> elastigirl.max_stretch = 30 # Meters
5235+
5236+
>>> print(razorgirl._properties.keys())
5237+
['rasta_name', 'name', 'superpower', 'alt_name']
5238+
>>> print(elastigirl._properties)
5239+
{'max_stretch': GenericProperty('max_stretch'),
5240+
'name': StringProperty('name'),
5241+
'superpower': StringProperty('superpower')}
5242+
5243+
Note: You can inspect the properties of an expando instance using the
5244+
_properties attribute, as shown above. This property exists for plain Model instances
5245+
too; it is just not as interesting for those.
5246+
"""
52145247

5215-
def __init__(self, *args, **kwargs):
5216-
raise NotImplementedError
5248+
# Set this to False (in an Expando subclass or entity) to make
5249+
# properties default to unindexed.
5250+
_default_indexed = True
5251+
5252+
# Set this to True to write [] to Cloud Datastore instead of no property
5253+
_write_empty_list_for_dynamic_properties = None
5254+
5255+
def _set_attributes(self, kwds):
5256+
for name, value in kwds.items():
5257+
setattr(self, name, value)
5258+
5259+
def __getattr__(self, name):
5260+
prop = self._properties.get(name)
5261+
if prop is None:
5262+
return super(Expando, self).__getattribute__(name)
5263+
return prop._get_value(self)
5264+
5265+
def __setattr__(self, name, value):
5266+
if name.startswith("_") or isinstance(
5267+
getattr(self.__class__, name, None), (Property, property)
5268+
):
5269+
return super(Expando, self).__setattr__(name, value)
5270+
if isinstance(value, Model):
5271+
prop = StructuredProperty(Model, name)
5272+
elif isinstance(value, dict):
5273+
prop = StructuredProperty(Expando, name)
5274+
else:
5275+
prop = GenericProperty(
5276+
name,
5277+
repeated=isinstance(value, (list, tuple)),
5278+
indexed=self._default_indexed,
5279+
write_empty_list=self._write_empty_list_for_dynamic_properties,
5280+
)
5281+
prop._code_name = name
5282+
self._properties[name] = prop
5283+
prop._set_value(self, value)
5284+
5285+
def __delattr__(self, name):
5286+
if name.startswith("_") or isinstance(
5287+
getattr(self.__class__, name, None), (Property, property)
5288+
):
5289+
return super(Expando, self).__delattr__(name)
5290+
prop = self._properties.get(name)
5291+
if not isinstance(prop, Property):
5292+
raise TypeError(
5293+
"Model properties must be Property instances; not %r" % prop
5294+
)
5295+
prop._delete_value(self)
5296+
if name in super(Expando, self)._properties:
5297+
raise RuntimeError(
5298+
"Property %s still in the list of properties for the "
5299+
"base class." % name
5300+
)
5301+
del self._properties[name]
52175302

52185303

52195304
def transactional(*args, **kwargs):

packages/google-cloud-ndb/tests/unit/test_model.py

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4310,8 +4310,94 @@ class ThisKind(ThatKind):
43104310
class TestExpando:
43114311
@staticmethod
43124312
def test_constructor():
4313-
with pytest.raises(NotImplementedError):
4314-
model.Expando()
4313+
class Expansive(model.Expando):
4314+
foo = model.StringProperty()
4315+
4316+
expansive = Expansive(foo="x", bar="y", baz="z")
4317+
assert expansive._properties == {"foo": "x", "bar": "y", "baz": "z"}
4318+
4319+
@staticmethod
4320+
def test___getattr__():
4321+
class Expansive(model.Expando):
4322+
foo = model.StringProperty()
4323+
4324+
expansive = Expansive(foo="x", bar="y", baz="z")
4325+
assert expansive.bar == "y"
4326+
4327+
@staticmethod
4328+
def test___getattr__from_model():
4329+
class Expansive(model.Expando):
4330+
foo = model.StringProperty()
4331+
4332+
expansive = Expansive(foo="x", bar="y", baz="z")
4333+
assert expansive._default_filters() == ()
4334+
4335+
@staticmethod
4336+
def test___getattr__from_model_error():
4337+
class Expansive(model.Expando):
4338+
foo = model.StringProperty()
4339+
4340+
expansive = Expansive(foo="x", bar="y", baz="z")
4341+
with pytest.raises(AttributeError):
4342+
expansive.notaproperty
4343+
4344+
@staticmethod
4345+
def test___setattr__with_model():
4346+
class Expansive(model.Expando):
4347+
foo = model.StringProperty()
4348+
4349+
expansive = Expansive(foo="x", bar=model.Model())
4350+
assert isinstance(expansive.bar, model.Model)
4351+
4352+
@staticmethod
4353+
def test___setattr__with_dict():
4354+
class Expansive(model.Expando):
4355+
foo = model.StringProperty()
4356+
4357+
expansive = Expansive(foo="x", bar={"bar": "y", "baz": "z"})
4358+
assert expansive.bar.baz == "z"
4359+
4360+
@staticmethod
4361+
def test___delattr__():
4362+
class Expansive(model.Expando):
4363+
foo = model.StringProperty()
4364+
4365+
expansive = Expansive(foo="x")
4366+
expansive.baz = "y"
4367+
assert expansive._properties == {"foo": "x", "baz": "y"}
4368+
del expansive.baz
4369+
assert expansive._properties == {"foo": "x"}
4370+
4371+
@staticmethod
4372+
def test___delattr__from_model():
4373+
class Expansive(model.Expando):
4374+
foo = model.StringProperty()
4375+
4376+
expansive = Expansive(foo="x")
4377+
with pytest.raises(AttributeError):
4378+
del expansive._nnexistent
4379+
4380+
@staticmethod
4381+
def test___delattr__non_property():
4382+
class Expansive(model.Expando):
4383+
foo = model.StringProperty()
4384+
4385+
expansive = Expansive(foo="x")
4386+
expansive.baz = "y"
4387+
expansive._properties["baz"] = "Not a Property"
4388+
with pytest.raises(TypeError):
4389+
del expansive.baz
4390+
4391+
@staticmethod
4392+
def test___delattr__runtime_error():
4393+
class Expansive(model.Expando):
4394+
foo = model.StringProperty()
4395+
4396+
expansive = Expansive(foo="x")
4397+
expansive.baz = "y"
4398+
model.Model._properties["baz"] = "baz"
4399+
with pytest.raises(RuntimeError):
4400+
del expansive.baz
43154401

43164402

43174403
def test_transactional():

0 commit comments

Comments
 (0)