Skip to content

Commit

Permalink
Use PEP-487 __set_name__ to simplify the Model and Index metaclasses. (
Browse files Browse the repository at this point in the history
  • Loading branch information
jpinner-lyft committed Nov 17, 2021
1 parent ece33c0 commit 55c06a2
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 30 deletions.
44 changes: 22 additions & 22 deletions pynamodb/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def __init__(
self.is_hash_key = hash_key
self.is_range_key = range_key

# AttributeContainerMeta._initialize_attributes will ensure this is a string
# __set_name__ will ensure this is a string
self.attr_path: List[str] = [attr_name] # type: ignore

@property
Expand Down Expand Up @@ -106,6 +106,9 @@ def __get__(self: _A, instance: Any, owner: Any) -> Union[_A, _T]:
else:
return self

def __set_name__(self, owner: Type[Any], name: str) -> None:
self.attr_name = self.attr_name or name

def _is_map_attribute_class_object(self, instance: 'Attribute') -> bool:
return isinstance(instance, MapAttribute) and not instance._is_attribute_container()

Expand Down Expand Up @@ -236,24 +239,10 @@ def _initialize_attributes(cls, discriminator_value):
cls._dynamo_to_python_attrs = {}

for name, attribute in getmembers(cls, lambda o: isinstance(o, Attribute)):
initialized = False
if isinstance(attribute, MapAttribute):
# MapAttribute instances that are class attributes of an AttributeContainer class
# should behave like an Attribute instance and not an AttributeContainer instance.
initialized = attribute._make_attribute()

cls._attributes[name] = attribute
if attribute.attr_name is None:
attribute.attr_name = name
if attribute.attr_name != name:
cls._dynamo_to_python_attrs[attribute.attr_name] = name

if initialized and isinstance(attribute, MapAttribute):
# To support creating expressions from nested attributes, MapAttribute instances
# store local copies of the attributes in cls._attributes with `attr_path` set.
# Prepend the `attr_path` lists with the dynamo attribute name.
attribute._update_attribute_paths(attribute.attr_name)

# Register the class with the discriminator if necessary.
discriminators = [name for name, attr in cls._attributes.items() if isinstance(attr, DiscriminatorAttribute)]
if len(discriminators) > 1:
Expand Down Expand Up @@ -779,7 +768,7 @@ class MyModel(Model):
does not behave as both an Attribute AND an AttributeContainer. Rather an instance of MapAttribute behaves
EITHER as an Attribute OR as an AttributeContainer, depending on where it was instantiated.
So, how do we create this dichotomous behavior? Using the AttributeContainerMeta metaclass.
So, how do we create this dichotomous behavior?
All MapAttribute instances are initialized as AttributeContainers only. During construction of
AttributeContainer classes (subclasses of MapAttribute and Model), any instances that are class attributes
are transformed from AttributeContainers to Attributes (via the `_make_attribute` method call).
Expand All @@ -793,7 +782,7 @@ def __init__(self, **attributes):
self.attribute_kwargs = {arg: attributes.pop(arg) for arg in self.attribute_args if arg in attributes}

# Assume all instances should behave like an AttributeContainer. Instances that are intended to be
# used as Attributes will be transformed by AttributeContainerMeta during creation of the containing class.
# used as Attributes will be transformed during creation of the containing class.
# Because of this do not use MRO or cooperative multiple inheritance, call the parent class directly.
AttributeContainer.__init__(self, **attributes)

Expand All @@ -814,10 +803,9 @@ def _is_attribute_container(self):
return 'attribute_values' in self.__dict__

def _make_attribute(self):
# WARNING! This function is only intended to be called from the AttributeContainerMeta metaclass.
# WARNING! This function is only intended to be called from the __set_name__ function.
if not self._is_attribute_container():
# This instance has already been initialized by another AttributeContainer class.
return False
raise AssertionError("MapAttribute._make_attribute called on an initialized instance")
# During initialization the kwargs were stored in `attribute_kwargs`. Remove them and re-initialize the class.
kwargs = self.attribute_kwargs
del self.attribute_kwargs
Expand All @@ -829,10 +817,9 @@ def _make_attribute(self):
# we have to store the local copy directly into __dict__ to prevent calling attr.__set__.
# Use deepcopy so that `attr_path` and any local attributes are also copied.
self.__dict__[name] = deepcopy(attr)
return True

def _update_attribute_paths(self, path_segment):
# WARNING! This function is only intended to be called from the AttributeContainerMeta metaclass.
# WARNING! This function is only intended to be called from the __set_name__ function.
if self._is_attribute_container():
raise AssertionError("MapAttribute._update_attribute_paths called before MapAttribute._make_attribute")
for name in self.get_attributes().keys():
Expand Down Expand Up @@ -910,6 +897,19 @@ def __set__(self, instance: Any, value: Union[None, 'MapAttribute[_KT, _VT]', Ma
value = type(self)(**value) # type: ignore
return super().__set__(instance, value) # type: ignore

def __set_name__(self, owner: Type[Any], name: str) -> None:
if issubclass(owner, AttributeContainer):
# MapAttribute instances that are class attributes of an AttributeContainer class
# should behave like an Attribute instance and not an AttributeContainer instance.
self._make_attribute()

super().__set_name__(owner, name)

# To support creating expressions from nested attributes, MapAttribute instances
# store local copies of the attributes in cls._attributes with `attr_path` set.
# Prepend the `attr_path` lists with the dynamo attribute name.
self._update_attribute_paths(self.attr_name)

def _set_attributes(self, **attrs):
"""
Sets the attributes for this object
Expand Down
11 changes: 7 additions & 4 deletions pynamodb/indexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
PynamoDB Indexes
"""
from inspect import getmembers
from typing import Any, Dict, Generic, List, Optional, TypeVar
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar
from typing import TYPE_CHECKING

from pynamodb._compat import GenericMeta
Expand Down Expand Up @@ -38,9 +38,6 @@ def __init__(self, name, bases, attrs, *args, **kwargs):
meta_cls = attrs.get(META_CLASS_NAME)
if meta_cls is not None:
meta_cls.attributes = None
elif isinstance(attr_obj, Attribute):
if attr_obj.attr_name is None:
attr_obj.attr_name = attr_name


class Index(Generic[_M], metaclass=IndexMeta):
Expand All @@ -55,6 +52,12 @@ def __init__(self) -> None:
if not hasattr(self.Meta, "projection"):
raise ValueError("No projection defined, define a projection for this class")

def __set_name__(self, owner: Type[_M], name: str):
if not hasattr(self.Meta, "model"):
self.Meta.model = owner
if not hasattr(self.Meta, "index_name"):
self.Meta.index_name = name

@classmethod
def count(
cls,
Expand Down
4 changes: 0 additions & 4 deletions pynamodb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,6 @@ def _initialize_indexes(cls):
"""
cls._indexes = {}
for name, index in getmembers(cls, lambda o: isinstance(o, Index)):
if not hasattr(index.Meta, "model"):
index.Meta.model = cls
if not hasattr(index.Meta, "index_name"):
index.Meta.index_name = name
cls._indexes[index.Meta.index_name] = index


Expand Down

0 comments on commit 55c06a2

Please sign in to comment.