From 19eeb5ad15574e40fe40b90d81fc7cdbf298c716 Mon Sep 17 00:00:00 2001 From: John Liu Date: Fri, 9 Jun 2017 13:26:05 -0700 Subject: [PATCH] Key attribute_values off the field name instead of attr_name (#292) --- .travis.yml | 5 ++-- pynamodb/attributes.py | 13 +++++++--- pynamodb/models.py | 1 - pynamodb/tests/test_attributes.py | 42 +++++++++++++++++++++++-------- pynamodb/tests/test_model.py | 29 ++++++++++++++++++++- 5 files changed, 72 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 19764d870..caf7a1d09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ env: - AWS_SECRET_ACCESS_KEY=fake_key AWS_ACCESS_KEY_ID=fake_id before_install: - - pip install six==1.8.0 + - pip install six==1.9.0 install: - pip install -r requirements-dev.txt @@ -20,4 +20,5 @@ script: - py.test --cov-report term-missing --cov=pynamodb pynamodb/tests/ after_success: - - coveralls \ No newline at end of file + - coveralls + diff --git a/pynamodb/attributes.py b/pynamodb/attributes.py index a30e50bbb..2d79aa0a4 100644 --- a/pynamodb/attributes.py +++ b/pynamodb/attributes.py @@ -40,11 +40,13 @@ def __init__(self, def __set__(self, instance, value): if instance: - instance.attribute_values[self.attr_name] = value + attr_name = instance._dynamo_to_python_attrs.get(self.attr_name, self.attr_name) + instance.attribute_values[attr_name] = value def __get__(self, instance, owner): if instance: - return instance.attribute_values.get(self.attr_name, None) + attr_name = instance._dynamo_to_python_attrs.get(self.attr_name, self.attr_name) + return instance.attribute_values.get(attr_name, None) else: return self @@ -451,7 +453,12 @@ def __getitem__(self, item): return self.attribute_values[item] def __getattr__(self, attr): - return self.attribute_values[attr] + # Should only be called for non-subclassed, otherwise we would go through + # the descriptor instead. + try: + return self.attribute_values[attr] + except KeyError: + raise AttributeError("'{0}' has no attribute '{1}'".format(self.__class__.__name__, attr)) def __set__(self, instance, value): if isinstance(value, collections.Mapping): diff --git a/pynamodb/models.py b/pynamodb/models.py index f446f9586..5c1289213 100644 --- a/pynamodb/models.py +++ b/pynamodb/models.py @@ -245,7 +245,6 @@ def _conditional_operator_check(cls, conditional_operator): if conditional_operator is not None and cls.has_map_or_list_attributes(): raise NotImplementedError('Map and List attribute do not support conditional_operator yet') - @classmethod def batch_get(cls, items, consistent_read=None, attributes_to_get=None): """ diff --git a/pynamodb/tests/test_attributes.py b/pynamodb/tests/test_attributes.py index 7d2426008..0806cc74d 100644 --- a/pynamodb/tests/test_attributes.py +++ b/pynamodb/tests/test_attributes.py @@ -668,18 +668,38 @@ class CustomMapAttribute(MapAttribute): } assert serialized_datetime == expected_serialized_value - def test_serialize_datetime(self): - class CustomMapAttribute(MapAttribute): - date_attr = UTCDateTimeAttribute() + def test_complex_map_accessors(self): + class NestedThing(MapAttribute): + double_nested = MapAttribute() + double_nested_renamed = MapAttribute(attr_name='something_else') - cm = CustomMapAttribute(date_attr=datetime(2017, 1, 1)) - serialized_datetime = cm.serialize(cm) - expected_serialized_value = { - 'date_attr': { - 'S': u'2017-01-01T00:00:00.000000+0000' - } - } - assert serialized_datetime == expected_serialized_value + class ThingModel(Model): + nested = NestedThing() + + t = ThingModel(nested=NestedThing( + double_nested={'hello': 'world'}, + double_nested_renamed={'foo': 'bar'}) + ) + + assert t.nested.double_nested.as_dict() == {'hello': 'world'} + assert t.nested.double_nested_renamed.as_dict() == {'foo': 'bar'} + assert t.nested.double_nested.hello == 'world' + assert t.nested.double_nested_renamed.foo == 'bar' + assert t.nested['double_nested'].as_dict() == {'hello': 'world'} + assert t.nested['double_nested_renamed'].as_dict() == {'foo': 'bar'} + assert t.nested['double_nested']['hello'] == 'world' + assert t.nested['double_nested_renamed']['foo'] == 'bar' + + with pytest.raises(AttributeError): + bad = t.nested.double_nested.bad + with pytest.raises(AttributeError): + bad = t.nested.bad + with pytest.raises(AttributeError): + bad = t.nested.something_else + with pytest.raises(KeyError): + bad = t.nested.double_nested['bad'] + with pytest.raises(KeyError): + bad = t.nested['something_else'] class TestValueDeserialize: diff --git a/pynamodb/tests/test_model.py b/pynamodb/tests/test_model.py index ce52b012b..b22257659 100644 --- a/pynamodb/tests/test_model.py +++ b/pynamodb/tests/test_model.py @@ -10,6 +10,7 @@ import six from botocore.client import ClientError from botocore.vendored import requests +import pytest from pynamodb.compat import CompatTestCase as TestCase from pynamodb.tests.deep_eq import deep_eq @@ -3178,7 +3179,33 @@ def test_model_with_maps_with_nulls_retrieve_from_db(self): GET_OFFICE_EMPLOYEE_ITEM_DATA_WITH_NULL.get(ITEM).get('person').get( MAP_SHORT).get('firstName').get(STRING_SHORT)) self.assertIsNone(item.person.age) - self.assertIsNone(item.person.is_dude) + self.assertIsNone(item.person.is_male) + + def test_model_with_maps_with_pythonic_attributes(self): + fake_db = self.database_mocker( + OfficeEmployee, + OFFICE_EMPLOYEE_MODEL_TABLE_DATA, + GET_OFFICE_EMPLOYEE_ITEM_DATA, + 'office_employee_id', + 'N', + '123' + ) + + with patch(PATCH_METHOD, new=fake_db) as req: + req.return_value = GET_OFFICE_EMPLOYEE_ITEM_DATA + item = OfficeEmployee.get(123) + self.assertEqual( + item.person.fname, + GET_OFFICE_EMPLOYEE_ITEM_DATA + .get(ITEM) + .get('person') + .get(MAP_SHORT) + .get('firstName') + .get(STRING_SHORT) + ) + assert item.person.is_male + with pytest.raises(AttributeError): + item.person.is_dude def test_model_with_list_retrieve_from_db(self): fake_db = self.database_mocker(GroceryList, GROCERY_LIST_MODEL_TABLE_DATA,