8
8
from datetime import datetime
9
9
from dateutil .parser import parse
10
10
from dateutil .tz import tzutc
11
+ from inspect import getargspec
11
12
from pynamodb .constants import (
12
13
STRING , STRING_SHORT , NUMBER , BINARY , UTC , DATETIME_FORMAT , BINARY_SET , STRING_SET , NUMBER_SET ,
13
14
MAP , MAP_SHORT , LIST , LIST_SHORT , DEFAULT_ENCODING , BOOLEAN , ATTR_TYPE_MAP , NUMBER_SHORT , NULL , SHORT_ATTR_TYPES
@@ -20,7 +21,6 @@ class Attribute(object):
20
21
"""
21
22
An attribute of a model
22
23
"""
23
- attr_name = None
24
24
attr_type = None
25
25
null = False
26
26
@@ -36,8 +36,7 @@ def __init__(self,
36
36
self .null = null
37
37
self .is_hash_key = hash_key
38
38
self .is_range_key = range_key
39
- if attr_name is not None :
40
- self .attr_name = attr_name
39
+ self .attr_name = attr_name
41
40
42
41
def __set__ (self , instance , value ):
43
42
if instance and not self ._is_map_attribute_class_object (instance ):
@@ -52,7 +51,7 @@ def __get__(self, instance, owner):
52
51
return self
53
52
54
53
def _is_map_attribute_class_object (self , instance ):
55
- return isinstance (instance , MapAttribute ) and getattr ( instance , '_class_object' , False )
54
+ return isinstance (instance , MapAttribute ) and not instance . _is_attribute_container ( )
56
55
57
56
def serialize (self , value ):
58
57
"""
@@ -176,14 +175,9 @@ def _initialize_attributes(cls):
176
175
if issubclass (item_cls , Attribute ):
177
176
instance = getattr (cls , item_name )
178
177
if isinstance (instance , MapAttribute ):
179
- # Attributes are data descriptors that bind their value to the containing object.
180
- # When subclassing MapAttribute and using them as AttributeContainers on a Model,
181
- # their internal attributes are bound to the instance in the Model class.
182
- # The `_class_object` attribute is used to indicate that the MapAttribute instance
183
- # belongs to a class object and not a class instance, overriding the binding.
184
- # Without this, Model.MapAttribute().attribute would the value and not the object;
185
- # whereas Model.attribute always returns the object.
186
- instance ._class_object = True
178
+ # MapAttribute instances that are class attributes of an AttributeContainer class
179
+ # should behave like an Attribute instance and not an AttributeContainer instance.
180
+ instance ._make_attribute ()
187
181
188
182
cls ._attributes [item_name ] = instance
189
183
if instance .attr_name is not None :
@@ -195,6 +189,16 @@ def _initialize_attributes(cls):
195
189
@add_metaclass (AttributeContainerMeta )
196
190
class AttributeContainer (object ):
197
191
192
+ def __init__ (self , ** attributes ):
193
+ # The `attribute_values` dictionary is used by the Attribute data descriptors in cls._attributes
194
+ # to store the values that are bound to this instance. Attributes store values in the dictionary
195
+ # using the `python_attr_name` as the dictionary key. "Raw" (i.e. non-subclassed) MapAttribute
196
+ # instances do not have any Attributes defined and instead use this dictionary to store their
197
+ # collection of name-value pairs.
198
+ self .attribute_values = {}
199
+ self ._set_defaults ()
200
+ self ._set_attributes (** attributes )
201
+
198
202
@classmethod
199
203
def _get_attributes (cls ):
200
204
"""
@@ -226,6 +230,15 @@ def _set_defaults(self):
226
230
if value is not None :
227
231
setattr (self , name , value )
228
232
233
+ def _set_attributes (self , ** attributes ):
234
+ """
235
+ Sets the attributes for this object
236
+ """
237
+ for attr_name , attr_value in six .iteritems (attributes ):
238
+ if attr_name not in self ._get_attributes ():
239
+ raise ValueError ("Attribute {0} specified does not exist" .format (attr_name ))
240
+ setattr (self , attr_name , attr_value )
241
+
229
242
230
243
class SetMixin (object ):
231
244
"""
@@ -513,18 +526,101 @@ class MapAttributeMeta(AttributeContainerMeta):
513
526
514
527
515
528
@add_metaclass (MapAttributeMeta )
516
- class MapAttribute (AttributeContainer , Attribute ):
529
+ class MapAttribute (Attribute , AttributeContainer ):
530
+ """
531
+ A Map Attribute
532
+
533
+ The MapAttribute class can be used to store a JSON document as "raw" name-value pairs, or
534
+ it can be subclassed and the document fields represented as class attributes using Attribute instances.
535
+
536
+ To support the ability to subclass MapAttribute and use it as an AttributeContainer, instances of
537
+ MapAttribute behave differently based both on where they are instantiated and on their type.
538
+ Because of this complicated behavior, a bit of an introduction is warranted.
539
+
540
+ Models that contain a MapAttribute define its properties using a class attribute on the model.
541
+ For example, below we define "MyModel" which contains a MapAttribute "my_map":
542
+
543
+ class MyModel(Model):
544
+ my_map = MapAttribute(attr_name="dynamo_name", default={})
545
+
546
+ When instantiated in this manner (as a class attribute of an AttributeContainer class), the MapAttribute
547
+ class acts as an instance of the Attribute class. The instance stores data about the attribute (in this
548
+ example the dynamo name and default value), and acts as a data descriptor, storing any value bound to it
549
+ on the `attribute_values` dictionary of the containing instance (in this case an instance of MyModel).
550
+
551
+ Unlike other Attribute types, the value that gets bound to the containing instance is a new instance of
552
+ MapAttribute, not an instance of the primitive type. For example, a UnicodeAttribute stores strings in
553
+ the `attribute_values` of the containing instance; a MapAttribute does not store a dict but instead stores
554
+ a new instance of itself. This difference in behavior is necessary when subclassing MapAttribute in order
555
+ to access the Attribute data descriptors that represent the document fields.
556
+
557
+ For example, below we redefine "MyModel" to use a subclass of MapAttribute as "my_map":
558
+
559
+ class MyMapAttribute(MapAttribute):
560
+ my_internal_map = MapAttribute()
561
+
562
+ class MyModel(Model):
563
+ my_map = MyMapAttribute(attr_name="dynamo_name", default = {})
564
+
565
+ In order to set the value of my_internal_map on an instance of MyModel we need the bound value for "my_map"
566
+ to be an instance of MapAttribute so that it acts as a data descriptor:
567
+
568
+ MyModel().my_map.my_internal_map = {'foo': 'bar'}
569
+
570
+ That is the attribute access of "my_map" must return a MyMapAttribute instance and not a dict.
571
+
572
+ When an instance is used in this manner (bound to an instance of an AttributeContainer class),
573
+ the MapAttribute class acts as an AttributeContainer class itself. The instance does not store data
574
+ about the attribute, and does not act as a data descriptor. The instance stores name-value pairs in its
575
+ internal `attribute_values` dictionary.
576
+
577
+ Thus while MapAttribute multiply inherits from Attribute and AttributeContainer, a MapAttribute instance
578
+ does not behave as both an Attribute AND an AttributeContainer. Rather an instance of MapAttribute behaves
579
+ EITHER as an Attribute OR as an AttributeContainer, depending on where it was instantiated.
580
+
581
+ So, how do we create this dichotomous behavior? Using the AttributeContainerMeta metaclass.
582
+ All MapAttribute instances are initialized as AttributeContainers only. During construction of
583
+ AttributeContainer classes (subclasses of MapAttribute and Model), any instances that are class attributes
584
+ are transformed from AttributeContainers to Attributes (via the `_make_attribute` method call).
585
+ """
517
586
attr_type = MAP
518
587
519
- def __init__ (self , hash_key = False , range_key = False , null = None , default = None , attr_name = None , ** attrs ):
520
- super (MapAttribute , self ).__init__ (hash_key = hash_key ,
521
- range_key = range_key ,
522
- null = null ,
523
- default = default ,
524
- attr_name = attr_name )
525
- self .attribute_values = {}
526
- self ._set_defaults ()
527
- self ._set_attributes (** attrs )
588
+ attribute_args = getargspec (Attribute .__init__ ).args [1 :]
589
+
590
+ def __init__ (self , ** attributes ):
591
+ # Store the kwargs used by Attribute.__init__ in case `_make_attribute` is called.
592
+ self .attribute_kwargs = dict ((arg , attributes .pop (arg )) for arg in self .attribute_args if arg in attributes )
593
+
594
+ # Assume all instances should behave like an AttributeContainer. Instances that are intended to be
595
+ # used as Attributes will be transformed by AttributeContainerMeta during creation of the containing class.
596
+ # Because of this do not use MRO or cooperative multiple inheritance, call the parent class directly.
597
+ AttributeContainer .__init__ (self , ** attributes )
598
+
599
+ # It is possible that attributes names can collide with argument names of Attribute.__init__.
600
+ # Assume that this is the case if any of the following are true:
601
+ # - 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
603
+ # cannot raise a ValueError (if this assumption is wrong, calling `_make_attribute` removes them)
604
+ # - the names of all attributes in self.attribute_kwargs match attributes defined on the class
605
+ if self .attribute_kwargs and (
606
+ attributes or self .is_raw () or all (arg in self ._get_attributes () for arg in self .attribute_kwargs )):
607
+ self ._set_attributes (** self .attribute_kwargs )
608
+
609
+ def _is_attribute_container (self ):
610
+ # Determine if this instance is being used as an AttributeContainer or an Attribute.
611
+ # AttributeContainer instances have an internal `attribute_values` dictionary that is removed
612
+ # by the `_make_attribute` call during initialization of the containing class.
613
+ return 'attribute_values' in self .__dict__
614
+
615
+ def _make_attribute (self ):
616
+ # WARNING! This function is only intended to be called from the AttributeContainerMeta metaclass.
617
+ if not self ._is_attribute_container ():
618
+ return
619
+ # During initialization the kwargs were stored in `attribute_kwargs`. Remove them and re-initialize the class.
620
+ kwargs = self .attribute_kwargs
621
+ del self .attribute_kwargs
622
+ del self .attribute_values
623
+ Attribute .__init__ (self , ** kwargs )
528
624
529
625
def __iter__ (self ):
530
626
return iter (self .attribute_values )
@@ -533,12 +629,22 @@ def __getitem__(self, item):
533
629
return self .attribute_values [item ]
534
630
535
631
def __getattr__ (self , attr ):
536
- # Should only be called for non-subclassed, otherwise we would go through
537
- # the descriptor instead.
538
- try :
539
- return self .attribute_values [attr ]
540
- except KeyError :
541
- raise AttributeError ("'{0}' has no attribute '{1}'" .format (self .__class__ .__name__ , attr ))
632
+ # This should only be called for "raw" (i.e. non-subclassed) MapAttribute instances.
633
+ # MapAttribute subclasses should access attributes via the Attribute descriptors.
634
+ if self .is_raw () and self ._is_attribute_container ():
635
+ try :
636
+ return self .attribute_values [attr ]
637
+ except KeyError :
638
+ pass
639
+ raise AttributeError ("'{0}' has no attribute '{1}'" .format (self .__class__ .__name__ , attr ))
640
+
641
+ def __setattr__ (self , name , value ):
642
+ # "Raw" (i.e. non-subclassed) instances set their name-value pairs in the `attribute_values` dictionary.
643
+ # MapAttribute subclasses should set attributes via the Attribute descriptors.
644
+ if self .is_raw () and self ._is_attribute_container ():
645
+ self .attribute_values [name ] = value
646
+ else :
647
+ object .__setattr__ (self , name , value )
542
648
543
649
def __set__ (self , instance , value ):
544
650
if isinstance (value , collections .Mapping ):
@@ -549,14 +655,11 @@ def _set_attributes(self, **attrs):
549
655
"""
550
656
Sets the attributes for this object
551
657
"""
552
- for attr_name , value in six .iteritems (attrs ):
553
- attribute = self ._get_attributes ().get (attr_name )
554
- if self .is_raw ():
555
- self .attribute_values [attr_name ] = value
556
- elif not attribute :
557
- raise AttributeError ("Attribute {0} specified does not exist" .format (attr_name ))
558
- else :
559
- setattr (self , attr_name , value )
658
+ if self .is_raw ():
659
+ for name , value in six .iteritems (attrs ):
660
+ setattr (self , name , value )
661
+ else :
662
+ super (MapAttribute , self )._set_attributes (** attrs )
560
663
561
664
def is_correctly_typed (self , key , attr ):
562
665
can_be_null = attr .null
0 commit comments