5
5
from six import add_metaclass
6
6
import json
7
7
from base64 import b64encode , b64decode
8
+ from copy import deepcopy
8
9
from datetime import datetime
9
10
from dateutil .parser import parse
10
11
from dateutil .tz import tzutc
@@ -36,15 +37,27 @@ def __init__(self,
36
37
self .null = null
37
38
self .is_hash_key = hash_key
38
39
self .is_range_key = range_key
39
- self .attr_name = attr_name
40
+ self .attr_path = [attr_name ]
41
+
42
+ @property
43
+ def attr_name (self ):
44
+ return self .attr_path [- 1 ]
45
+
46
+ @attr_name .setter
47
+ def attr_name (self , value ):
48
+ self .attr_path [- 1 ] = value
40
49
41
50
def __set__ (self , instance , value ):
42
51
if instance and not self ._is_map_attribute_class_object (instance ):
43
52
attr_name = instance ._dynamo_to_python_attrs .get (self .attr_name , self .attr_name )
44
53
instance .attribute_values [attr_name ] = value
45
54
46
55
def __get__ (self , instance , owner ):
47
- if instance and not self ._is_map_attribute_class_object (instance ):
56
+ if self ._is_map_attribute_class_object (instance ):
57
+ # MapAttribute class objects store a local copy of the attribute with `attr_path` set to the document path.
58
+ attr_name = instance ._dynamo_to_python_attrs .get (self .attr_name , self .attr_name )
59
+ return instance .__dict__ .get (attr_name , None ) or self
60
+ elif instance :
48
61
attr_name = instance ._dynamo_to_python_attrs .get (self .attr_name , self .attr_name )
49
62
return instance .attribute_values .get (attr_name , None )
50
63
else :
@@ -121,12 +134,14 @@ def contains(self, item):
121
134
class AttributePath (Path ):
122
135
123
136
def __init__ (self , attribute ):
124
- super (AttributePath , self ).__init__ (attribute .attr_name , attribute_name = True )
137
+ super (AttributePath , self ).__init__ (attribute .attr_path )
125
138
self .attribute = attribute
126
139
127
140
def __getitem__ (self , idx ):
128
141
if self .attribute .attr_type != LIST : # only list elements support the list dereference operator
129
142
raise TypeError ("'{0}' object has no attribute __getitem__" .format (self .attribute .__class__ .__name__ ))
143
+ # The __getitem__ call returns a new Path instance, not an AttributePath instance.
144
+ # This is intended since the list element is not the same attribute as the list itself.
130
145
return super (AttributePath , self ).__getitem__ (idx )
131
146
132
147
def contains (self , item ):
@@ -145,7 +160,7 @@ def _serialize(self, value):
145
160
return self .attribute .serialize ([value ])[0 ]
146
161
if self .attribute .attr_type == MAP and not isinstance (value , dict ):
147
162
# Map attributes assume the values to be serialized are maps.
148
- return self . attribute . serialize ({ '' : value })[ '' ]
163
+ return super ( AttributePath , self ). _serialize ( value )
149
164
return {ATTR_TYPE_MAP [self .attribute .attr_type ]: self .attribute .serialize (value )}
150
165
151
166
@@ -174,17 +189,24 @@ def _initialize_attributes(cls):
174
189
175
190
if issubclass (item_cls , Attribute ):
176
191
instance = getattr (cls , item_name )
192
+ initialized = False
177
193
if isinstance (instance , MapAttribute ):
178
194
# MapAttribute instances that are class attributes of an AttributeContainer class
179
195
# should behave like an Attribute instance and not an AttributeContainer instance.
180
- instance ._make_attribute ()
196
+ initialized = instance ._make_attribute ()
181
197
182
198
cls ._attributes [item_name ] = instance
183
199
if instance .attr_name is not None :
184
200
cls ._dynamo_to_python_attrs [instance .attr_name ] = item_name
185
201
else :
186
202
instance .attr_name = item_name
187
203
204
+ if initialized and isinstance (instance , MapAttribute ):
205
+ # To support creating expressions from nested attributes, MapAttribute instances
206
+ # store local copies of the attributes in cls._attributes with `attr_path` set.
207
+ # Prepend the `attr_path` lists with the dynamo attribute name.
208
+ instance ._update_attribute_paths (instance .attr_name )
209
+
188
210
189
211
@add_metaclass (AttributeContainerMeta )
190
212
class AttributeContainer (object ):
@@ -599,7 +621,7 @@ def __init__(self, **attributes):
599
621
# It is possible that attributes names can collide with argument names of Attribute.__init__.
600
622
# Assume that this is the case if any of the following are true:
601
623
# - the user passed in other attributes that did not match any argument names
602
- # - this is "raw" (i.e. non-subclassed) MapAttribute instance and attempting to store the attributes
624
+ # - this is a "raw" (i.e. non-subclassed) MapAttribute instance and attempting to store the attributes
603
625
# cannot raise a ValueError (if this assumption is wrong, calling `_make_attribute` removes them)
604
626
# - the names of all attributes in self.attribute_kwargs match attributes defined on the class
605
627
if self .attribute_kwargs and (
@@ -615,12 +637,30 @@ def _is_attribute_container(self):
615
637
def _make_attribute (self ):
616
638
# WARNING! This function is only intended to be called from the AttributeContainerMeta metaclass.
617
639
if not self ._is_attribute_container ():
618
- return
640
+ # This instance has already been initialized by another AttributeContainer class.
641
+ return False
619
642
# During initialization the kwargs were stored in `attribute_kwargs`. Remove them and re-initialize the class.
620
643
kwargs = self .attribute_kwargs
621
644
del self .attribute_kwargs
622
645
del self .attribute_values
623
646
Attribute .__init__ (self , ** kwargs )
647
+ for name , attr in self ._get_attributes ().items ():
648
+ # Set a local attribute with the same name that shadows the class attribute.
649
+ # Because attr is a data descriptor and the attribute already exists on the class,
650
+ # we have to store the local copy directly into __dict__ to prevent calling attr.__set__.
651
+ # Use deepcopy so that `attr_path` and any local attributes are also copied.
652
+ self .__dict__ [name ] = deepcopy (attr )
653
+ return True
654
+
655
+ def _update_attribute_paths (self , path_segment ):
656
+ # WARNING! This function is only intended to be called from the AttributeContainerMeta metaclass.
657
+ if self ._is_attribute_container ():
658
+ raise AssertionError ("MapAttribute._update_attribute_paths called before MapAttribute._make_attribute" )
659
+ for name in self ._get_attributes ().keys ():
660
+ local_attr = self .__dict__ [name ]
661
+ local_attr .attr_path .insert (0 , path_segment )
662
+ if isinstance (local_attr , MapAttribute ):
663
+ local_attr ._update_attribute_paths (path_segment )
624
664
625
665
def __iter__ (self ):
626
666
return iter (self .attribute_values )
0 commit comments