Skip to content

Commit 874d18e

Browse files
garrettheeldanielhochman
authored andcommitted
Always create map attributes when setting a dict (#223)
1 parent d16e2bb commit 874d18e

File tree

4 files changed

+216
-54
lines changed

4 files changed

+216
-54
lines changed

pynamodb/attributes.py

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ def __init__(self, hash_key=False, range_key=False, null=None, default=None, att
419419
null=null,
420420
default=default,
421421
attr_name=attr_name)
422+
self._get_attributes() # Ensure attributes are always inited
422423
self.attribute_values = {}
423424
self._set_defaults()
424425
self._set_attributes(**attrs)
@@ -429,38 +430,33 @@ def __iter__(self):
429430
def __getitem__(self, item):
430431
return self.attribute_values[item]
431432

433+
def __getattr__(self, attr):
434+
return self.attribute_values[attr]
435+
436+
def __set__(self, instance, value):
437+
if isinstance(value, collections.Mapping):
438+
value = type(self)(**value)
439+
return super(MapAttribute, self).__set__(instance, value)
440+
432441
def _set_attributes(self, **attrs):
433442
"""
434443
Sets the attributes for this object
435444
"""
436-
for attr_name, attr in self._get_attributes().items():
437-
if attr.attr_name in attrs:
438-
value = attrs.get(attr_name)
439-
if not isinstance(value, collections.Mapping) or type(attr) == MapAttribute:
440-
setattr(self, attr_name, attrs.get(attr.attr_name))
441-
else:
442-
# it's a sub model which means we need to instantiate that type first
443-
# pass in the attributes of that model, then set the field on this object to point to that model
444-
sub_model = value
445-
instance = type(attr)(**sub_model)
446-
setattr(self, attr_name, instance)
447-
448-
elif attr_name in attrs:
449-
setattr(self, attr_name, attrs.get(attr_name))
450-
451-
def get_values(self):
452-
attributes = self._get_attributes()
453-
result = {}
454-
for k, v in six.iteritems(attributes):
455-
result[k] = getattr(self, k)
456-
return result
445+
for attr_name, value in six.iteritems(attrs):
446+
attribute = self._get_attributes().get(attr_name)
447+
if self.is_raw():
448+
self.attribute_values[attr_name] = value
449+
elif not attribute:
450+
raise AttributeError("Attribute {0} specified does not exist".format(attr_name))
451+
else:
452+
setattr(self, attr_name, value)
457453

458454
def is_correctly_typed(self, key, attr):
459455
can_be_null = attr.null
460456
value = getattr(self, key)
461457
if can_be_null and value is None:
462458
return True
463-
if value is None:
459+
if getattr(self, key) is None:
464460
raise ValueError("Attribute '{0}' cannot be None".format(key))
465461
return True # TODO: check that the actual type of `value` meets requirements of `attr`
466462

@@ -486,16 +482,37 @@ def serialize(self, values):
486482

487483
def deserialize(self, values):
488484
"""
489-
Decode numbers from list of AttributeValue types.
485+
Decode as a dict.
490486
"""
491487
deserialized_dict = dict()
492488
for k in values:
493489
v = values[k]
494-
attr_class = _get_class_for_deserialize(v)
495490
attr_value = _get_value_for_deserialize(v)
496-
deserialized_dict[k] = attr_class.deserialize(attr_value)
491+
key = self._dynamo_to_python_attr(k)
492+
attr_class = self._get_deserialize_class(key, v)
493+
494+
deserialized_dict[key] = attr_class.deserialize(attr_value)
495+
496+
# If this is a subclass of a MapAttribute (i.e typed), instantiate an instance
497+
if not self.is_raw():
498+
return type(self)(**deserialized_dict)
497499
return deserialized_dict
498500

501+
@classmethod
502+
def is_raw(cls):
503+
return cls == MapAttribute
504+
505+
def as_dict(self):
506+
result = {}
507+
for key, value in six.iteritems(self.attribute_values):
508+
result[key] = value.as_dict() if isinstance(value, MapAttribute) else value
509+
return result
510+
511+
@classmethod
512+
def _get_deserialize_class(cls, key, value):
513+
if not cls.is_raw():
514+
return cls._get_attributes().get(key)
515+
return _get_class_for_deserialize(value)
499516

500517
def _get_value_for_deserialize(value):
501518
return value[list(value.keys())[0]]
@@ -562,17 +579,10 @@ def deserialize(self, values):
562579
"""
563580
deserialized_lst = []
564581
for v in values:
565-
attr_class = _get_class_for_deserialize(v)
582+
class_for_deserialize = self.element_type() if self.element_type else _get_class_for_deserialize(v)
566583
attr_value = _get_value_for_deserialize(v)
567-
deserialized_lst.append(attr_class.deserialize(attr_value))
568-
569-
if not self.element_type:
570-
return deserialized_lst
571-
572-
lst_of_type = []
573-
for item in deserialized_lst:
574-
lst_of_type.append(self.element_type(**item))
575-
return lst_of_type
584+
deserialized_lst.append(class_for_deserialize.deserialize(attr_value))
585+
return deserialized_lst
576586

577587
DESERIALIZE_CLASS_MAP = {
578588
LIST_SHORT: ListAttribute(),

pynamodb/models.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -500,10 +500,7 @@ def from_raw_data(cls, data):
500500
attr_name = cls._dynamo_to_python_attr(name)
501501
attr = cls._get_attributes().get(attr_name, None)
502502
if attr:
503-
deserialized_attr = attr.deserialize(attr.get_value(value))
504-
if isinstance(attr, MapAttribute) and not type(attr) == MapAttribute:
505-
deserialized_attr = type(attr)(**deserialized_attr)
506-
kwargs[attr_name] = deserialized_attr
503+
kwargs[attr_name] = attr.deserialize(attr.get_value(value))
507504
return cls(*args, **kwargs)
508505

509506
@classmethod
@@ -1295,7 +1292,6 @@ def _serialize(self, attr_map=False, null_check=True):
12951292
if isinstance(value, MapAttribute):
12961293
if not value.validate():
12971294
raise ValueError("Attribute '{0}' is not correctly typed".format(attr.attr_name))
1298-
value = value.get_values()
12991295

13001296
serialized = self._serialize_value(attr, value, null_check)
13011297
if NULL in serialized:

pynamodb/tests/test_attributes.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class Meta:
3333
datetime_attr = UTCDateTimeAttribute()
3434
bool_attr = BooleanAttribute()
3535
json_attr = JSONAttribute()
36+
map_attr = MapAttribute()
3637

3738

3839
class CustomAttrMap(MapAttribute):
@@ -576,6 +577,62 @@ def test_defaults(self):
576577
}
577578
})
578579

580+
def test_raw_map_from_dict(self):
581+
item = AttributeTestModel(
582+
map_attr={
583+
"foo": "bar",
584+
"num": 3,
585+
"nested": {
586+
"nestedfoo": "nestedbar"
587+
}
588+
}
589+
)
590+
591+
self.assertEqual(item.map_attr['foo'], 'bar')
592+
self.assertEqual(item.map_attr['num'], 3)
593+
594+
def test_raw_map_access(self):
595+
raw = {
596+
"foo": "bar",
597+
"num": 3,
598+
"nested": {
599+
"nestedfoo": "nestedbar"
600+
}
601+
}
602+
attr = MapAttribute(**raw)
603+
604+
for k, v in six.iteritems(raw):
605+
self.assertEquals(attr[k], v)
606+
607+
def test_raw_map_json_serialize(self):
608+
raw = {
609+
"foo": "bar",
610+
"num": 3,
611+
"nested": {
612+
"nestedfoo": "nestedbar"
613+
}
614+
}
615+
616+
serialized_raw = json.dumps(raw)
617+
self.assertEqual(json.dumps(AttributeTestModel(map_attr=raw).map_attr.as_dict()),
618+
serialized_raw)
619+
self.assertEqual(json.dumps(AttributeTestModel(map_attr=MapAttribute(**raw)).map_attr.as_dict()),
620+
serialized_raw)
621+
622+
def test_typed_and_raw_map_json_serialize(self):
623+
class TypedMap(MapAttribute):
624+
map_attr = MapAttribute()
625+
626+
class SomeModel(Model):
627+
typed_map = TypedMap()
628+
629+
item = SomeModel(
630+
typed_map=TypedMap(map_attr={'foo': 'bar'})
631+
)
632+
633+
self.assertEqual(json.dumps({'map_attr': {'foo': 'bar'}}),
634+
json.dumps(item.typed_map.as_dict()))
635+
579636

580637
class MapAndListAttributeTestCase(TestCase):
581638

pynamodb/tests/test_model.py

Lines changed: 113 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,11 @@ class ModelTestCase(TestCase):
447447
"""
448448
Tests for the models API
449449
"""
450+
@staticmethod
451+
def init_table_meta(model_clz, table_data):
452+
with patch(PATCH_METHOD) as req:
453+
req.return_value = table_data
454+
model_clz._get_meta_data()
450455

451456
def assert_dict_lists_equal(self, list1, list2):
452457
"""
@@ -3347,7 +3352,7 @@ def test_raw_map_deserializes(self):
33473352
instance = ExplicitRawMapModel(map_attr=map_native)
33483353
instance._deserialize(map_serialized)
33493354
actual = instance.map_attr
3350-
for k,v in six.iteritems(map_native):
3355+
for k, v in six.iteritems(map_native):
33513356
self.assertEqual(v, actual[k])
33523357

33533358
def test_raw_map_from_raw_data_works(self):
@@ -3361,10 +3366,10 @@ def test_raw_map_from_raw_data_works(self):
33613366
EXPLICIT_RAW_MAP_MODEL_ITEM_DATA,
33623367
'map_id', 'N',
33633368
'123')
3364-
with patch(PATCH_METHOD, new=fake_db) as req:
3369+
with patch(PATCH_METHOD, new=fake_db):
33653370
item = ExplicitRawMapModel.get(123)
33663371
actual = item.map_attr
3367-
self.assertEqual(map_native.get('listy')[2], actual.get('listy')[2])
3372+
self.assertEqual(map_native.get('listy')[2], actual['listy'][2])
33683373
for k, v in six.iteritems(map_native):
33693374
self.assertEqual(v, actual[k])
33703375

@@ -3396,11 +3401,11 @@ def _get_raw_map_as_sub_map_test_data(self):
33963401
map_serialized = {
33973402
'M': {
33983403
'foo': {'S': 'bar'},
3399-
'num': {'N': 1},
3404+
'num': {'N': '1'},
34003405
'bool_type': {'BOOL': True},
34013406
'other_b_type': {'BOOL': False},
3402-
'floaty': {'N': 1.2},
3403-
'listy': {'L': [{'N': 1}, {'N': 2}, {'N': 3}]},
3407+
'floaty': {'N': '1.2'},
3408+
'listy': {'L': [{'N': '1'}, {'N': '2'}, {'N': '3'}]},
34043409
'mapy': {'M': {'baz': {'S': 'bongo'}}}
34053410
}
34063411
}
@@ -3415,13 +3420,22 @@ def _get_raw_map_as_sub_map_test_data(self):
34153420
)
34163421
return map_native, map_serialized, sub_attr, instance
34173422

3418-
def test_raw_map_as_sub_map_deserializes(self):
3423+
def test_raw_map_as_sub_map(self):
34193424
map_native, map_serialized, sub_attr, instance = self._get_raw_map_as_sub_map_test_data()
3420-
instance._deserialize(map_serialized)
34213425
actual = instance.sub_attr
34223426
self.assertEqual(sub_attr, actual)
3423-
self.assertEqual(sub_attr.map_field.get('floaty'), map_native.get('floaty'))
3424-
self.assertEqual(sub_attr.map_field.get('mapy', {}).get('baz'), map_native.get('mapy', {}).get('baz'))
3427+
self.assertEqual(actual.map_field['floaty'], map_native.get('floaty'))
3428+
self.assertEqual(actual.map_field['mapy']['baz'], map_native.get('mapy').get('baz'))
3429+
3430+
def test_raw_map_as_sub_map_deserialize(self):
3431+
map_native, map_serialized, _, _ = self._get_raw_map_as_sub_map_test_data()
3432+
3433+
actual = MapAttrSubClassWithRawMapAttr().deserialize({
3434+
"map_field": map_serialized
3435+
})
3436+
3437+
for k, v in six.iteritems(map_native):
3438+
self.assertEqual(actual.map_field[k], v)
34253439

34263440
def test_raw_map_as_sub_map_from_raw_data_works(self):
34273441
map_native, map_serialized, sub_attr, instance = self._get_raw_map_as_sub_map_test_data()
@@ -3430,9 +3444,94 @@ def test_raw_map_as_sub_map_from_raw_data_works(self):
34303444
EXPLICIT_RAW_MAP_MODEL_AS_SUB_MAP_IN_TYPED_MAP_ITEM_DATA,
34313445
'map_id', 'N',
34323446
'123')
3433-
with patch(PATCH_METHOD, new=fake_db) as req:
3447+
with patch(PATCH_METHOD, new=fake_db):
34343448
item = ExplicitRawMapAsMemberOfSubClass.get(123)
3435-
self.assertEqual(sub_attr.map_field.get('floaty'),
3449+
actual = item.sub_attr
3450+
self.assertEqual(sub_attr.map_field['floaty'],
34363451
map_native.get('floaty'))
3437-
self.assertEqual(sub_attr.map_field.get('mapy', {}).get('baz'),
3438-
map_native.get('mapy', {}).get('baz'))
3452+
self.assertEqual(actual.map_field['mapy']['baz'],
3453+
map_native.get('mapy').get('baz'))
3454+
3455+
3456+
class ModelInitTestCase(TestCase):
3457+
3458+
def test_raw_map_attribute_with_dict_init(self):
3459+
attribute = {
3460+
'foo': 123,
3461+
'bar': 'baz'
3462+
}
3463+
actual = ExplicitRawMapModel(map_id=3, map_attr=attribute)
3464+
self.assertEquals(actual.map_attr['foo'], attribute['foo'])
3465+
3466+
def test_raw_map_attribute_with_initialized_instance_init(self):
3467+
attribute = {
3468+
'foo': 123,
3469+
'bar': 'baz'
3470+
}
3471+
initialized_instance = MapAttribute(**attribute)
3472+
actual = ExplicitRawMapModel(map_id=3, map_attr=initialized_instance)
3473+
self.assertEquals(actual.map_attr['foo'], initialized_instance['foo'])
3474+
self.assertEquals(actual.map_attr['foo'], attribute['foo'])
3475+
3476+
def test_subclassed_map_attribute_with_dict_init(self):
3477+
attribute = {
3478+
'make': 'Volkswagen',
3479+
'model': 'Super Beetle'
3480+
}
3481+
expected_model = CarInfoMap(**attribute)
3482+
actual = CarModel(car_id=1, car_info=attribute)
3483+
self.assertEquals(expected_model.make, actual.car_info.make)
3484+
self.assertEquals(expected_model.model, actual.car_info.model)
3485+
3486+
def test_subclassed_map_attribute_with_initialized_instance_init(self):
3487+
attribute = {
3488+
'make': 'Volkswagen',
3489+
'model': 'Super Beetle'
3490+
}
3491+
expected_model = CarInfoMap(**attribute)
3492+
actual = CarModel(car_id=1, car_info=expected_model)
3493+
self.assertEquals(expected_model.make, actual.car_info.make)
3494+
self.assertEquals(expected_model.model, actual.car_info.model)
3495+
3496+
def _get_bin_tree(self, multiplier=1):
3497+
return {
3498+
'value': 5 * multiplier,
3499+
'left': {
3500+
'value': 2 * multiplier,
3501+
'left': {
3502+
'value': 1 * multiplier
3503+
},
3504+
'right': {
3505+
'value': 3 * multiplier
3506+
}
3507+
},
3508+
'right': {
3509+
'value': 7 * multiplier,
3510+
'left': {
3511+
'value': 6 * multiplier
3512+
},
3513+
'right': {
3514+
'value': 8 * multiplier
3515+
}
3516+
}
3517+
}
3518+
3519+
def test_subclassed_map_attribute_with_map_attributes_member_with_dict_init(self):
3520+
left = self._get_bin_tree()
3521+
right = self._get_bin_tree(multiplier=2)
3522+
actual = TreeModel(tree_key='key', left=left, right=right)
3523+
self.assertEquals(actual.left.left.right.value, 3)
3524+
self.assertEquals(actual.left.left.value, 2)
3525+
self.assertEquals(actual.right.right.left.value, 12)
3526+
self.assertEquals(actual.right.right.value, 14)
3527+
3528+
def test_subclassed_map_attribute_with_map_attribute_member_with_initialized_instance_init(self):
3529+
left = self._get_bin_tree()
3530+
right = self._get_bin_tree(multiplier=2)
3531+
left_instance = TreeLeaf(**left)
3532+
right_instance = TreeLeaf(**right)
3533+
actual = TreeModel(tree_key='key', left=left_instance, right=right_instance)
3534+
self.assertEquals(actual.left.left.right.value, left_instance.left.right.value)
3535+
self.assertEquals(actual.left.left.value, left_instance.left.value)
3536+
self.assertEquals(actual.right.right.left.value, right_instance.right.left.value)
3537+
self.assertEquals(actual.right.right.value, right_instance.right.value)

0 commit comments

Comments
 (0)