From 6d6a8be7a25abf6f0ab5df7d8697c9fe871f7c4c Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Wed, 10 Feb 2021 20:38:11 -0500 Subject: [PATCH] Fix type errors when deriving from a MapAttribute and another type (#904) --- docs/release_notes.rst | 8 ++++++++ mypy.ini | 1 + pynamodb/__init__.py | 2 +- pynamodb/attributes.py | 24 ++++++++---------------- pynamodb/models.py | 12 ++++++++++++ tests/test_mypy.py | 10 ++++++++++ 6 files changed, 40 insertions(+), 17 deletions(-) diff --git a/docs/release_notes.rst b/docs/release_notes.rst index bf5f4c5c5..5c05f1819 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -1,6 +1,14 @@ Release Notes ============= +v5.0.1 +---------- + +:date: 2021-02-10 + +* Fix type errors when deriving from a MapAttribute and another type (#904) + + v5.0.0 ---------- diff --git a/mypy.ini b/mypy.ini index 8c8acc0b9..5dfc38c9a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -8,6 +8,7 @@ warn_unused_configs = True warn_redundant_casts = True warn_incomplete_stub = True follow_imports = normal +show_error_codes = True # TODO: burn these down [mypy-tests.*] diff --git a/pynamodb/__init__.py b/pynamodb/__init__.py index 918a0c802..e69e580c3 100644 --- a/pynamodb/__init__.py +++ b/pynamodb/__init__.py @@ -7,4 +7,4 @@ """ __author__ = 'Jharrod LaFon' __license__ = 'MIT' -__version__ = '5.0.0' +__version__ = '5.0.1' diff --git a/pynamodb/attributes.py b/pynamodb/attributes.py index 2eacd9e5a..b2d902392 100644 --- a/pynamodb/attributes.py +++ b/pynamodb/attributes.py @@ -339,7 +339,7 @@ def _set_attributes(self, **attributes: Attribute) -> None: raise ValueError("Attribute {} specified does not exist".format(attr_name)) setattr(self, attr_name, attr_value) - def serialize(self, null_check=True) -> Dict[str, Dict[str, Any]]: + def _container_serialize(self, null_check: bool = True) -> Dict[str, Dict[str, Any]]: """ Serialize attribute values for DynamoDB """ @@ -357,7 +357,7 @@ def serialize(self, null_check=True) -> Dict[str, Dict[str, Any]]: attribute_values[attr.attr_name] = {attr.attr_type: attr_value} return attribute_values - def deserialize(self, attribute_values: Dict[str, Dict[str, Any]]) -> None: + def _container_deserialize(self, attribute_values: Dict[str, Dict[str, Any]]) -> None: """ Sets attributes sent back from DynamoDB on this object """ @@ -387,17 +387,9 @@ def _instantiate(cls: Type[_ACT], attribute_values: Dict[str, Dict[str, Any]]) - raise ValueError("Cannot instantiate a {} from the returned class: {}".format( cls.__name__, stored_cls.__name__)) instance = (stored_cls or cls)(_user_instantiated=False) - AttributeContainer.deserialize(instance, attribute_values) + AttributeContainer._container_deserialize(instance, attribute_values) return instance - def __eq__(self, other: Any) -> bool: - # This is required so that MapAttribute can call this method. - return self is other - - def __ne__(self, other: Any) -> bool: - # This is required so that MapAttribute can call this method. - return self is not other - class DiscriminatorAttribute(Attribute[type]): attr_type = STRING @@ -835,14 +827,14 @@ def _update_attribute_paths(self, path_segment): if isinstance(local_attr, MapAttribute): local_attr._update_attribute_paths(path_segment) - def __eq__(self, other): + def __eq__(self, other: Any) -> 'Comparison': # type: ignore[override] if self._is_attribute_container(): - return AttributeContainer.__eq__(self, other) + return self is other # type: ignore return Attribute.__eq__(self, other) - def __ne__(self, other): + def __ne__(self, other: Any) -> 'Comparison': # type: ignore[override] if self._is_attribute_container(): - return AttributeContainer.__ne__(self, other) + return self is not other # type: ignore return Attribute.__ne__(self, other) def __iter__(self): @@ -940,7 +932,7 @@ def serialize(self, values): setattr(instance, name, values[name]) values = instance - return AttributeContainer.serialize(values) + return AttributeContainer._container_serialize(values) # Continue to serialize NULL values in "raw" map attributes for backwards compatibility. # This special case behavior for "raw" attributes should be removed in the future. diff --git a/pynamodb/models.py b/pynamodb/models.py index 2b15bdad7..c6789960c 100644 --- a/pynamodb/models.py +++ b/pynamodb/models.py @@ -1120,6 +1120,18 @@ def _serialize_keys(cls, hash_key, range_key=None) -> Tuple[_KeyType, _KeyType]: range_key = cls._range_key_attribute().serialize(range_key) return hash_key, range_key + def serialize(self, null_check: bool = True) -> Dict[str, Dict[str, Any]]: + """ + Serialize attribute values for DynamoDB + """ + return self._container_serialize(null_check=null_check) + + def deserialize(self, attribute_values: Dict[str, Dict[str, Any]]) -> None: + """ + Sets attributes sent back from DynamoDB on this object + """ + return self._container_deserialize(attribute_values=attribute_values) + class _ModelFuture(Generic[_T]): """ diff --git a/tests/test_mypy.py b/tests/test_mypy.py index c8aa3cc2b..23b5c0dd1 100644 --- a/tests/test_mypy.py +++ b/tests/test_mypy.py @@ -160,6 +160,7 @@ class MyModel(Model): reveal_type(MyModel.my_list[0] == MyModel()) # N: Revealed type is 'pynamodb.expressions.condition.Comparison' # the following string indexing is not type checked - not by mypy nor in runtime reveal_type(MyModel.my_list[0]['my_sub_attr'] == 'foobar') # N: Revealed type is 'pynamodb.expressions.condition.Comparison' + reveal_type(MyModel.my_map == 'foobar') # N: Revealed type is 'pynamodb.expressions.condition.Comparison' """) @@ -203,3 +204,12 @@ class MyModel(Model): model = next(typed_result) not_model = next(typed_result) # E: Incompatible types in assignment (expression has type "MyModel", variable has type "int") [assignment] """) + + +def test_map_attribute_derivation(assert_mypy_output): + assert_mypy_output(""" + from pynamodb.attributes import MapAttribute + + class MyMap(MapAttribute, object): + pass + """)