From c5394f5951b6437bc91f6eec9150b77b3935312f Mon Sep 17 00:00:00 2001 From: Adam Tankanow Date: Mon, 12 Sep 2022 13:21:37 -0400 Subject: [PATCH 1/2] ISSUE-1503: ISSUE-1503: ISSUE-1503: fix: add Mapping abc and missing methods to DictWrapper ISSUE-1503: Add DictWrapper Mapping abc tests ISSUE-1503: add StreamRecord tests --- .../utilities/data_classes/common.py | 10 ++++-- .../data_classes/dynamo_db_stream_event.py | 4 ++- tests/functional/test_data_classes.py | 31 +++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/common.py b/aws_lambda_powertools/utilities/data_classes/common.py index 2109ee3dd3e..83c5c87b216 100644 --- a/aws_lambda_powertools/utilities/data_classes/common.py +++ b/aws_lambda_powertools/utilities/data_classes/common.py @@ -1,9 +1,9 @@ import base64 import json -from typing import Any, Dict, Optional +from typing import Any, Dict, Iterator, Mapping, Optional -class DictWrapper: +class DictWrapper(Mapping): """Provides a single read only access to a wrapper dict""" def __init__(self, data: Dict[str, Any]): @@ -19,6 +19,12 @@ def __eq__(self, other: Any) -> bool: return self._data == other._data + def __iter__(self) -> Iterator: + return iter(self._data) + + def __len__(self) -> int: + return len(self._data) + def get(self, key: str, default: Optional[Any] = None) -> Optional[Any]: return self._data.get(key, default) diff --git a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py index 7e209fab3e2..28bbffdb510 100644 --- a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py +++ b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py @@ -182,8 +182,10 @@ def approximate_creation_date_time(self) -> Optional[int]: item = self.get("ApproximateCreationDateTime") return None if item is None else int(item) + # This override breaks the Mapping protocol of DictWrapper, it's left here for backwards compatibility with + # a 'type: ignore' comment. This is currently the only subclass of DictWrapper that breaks this protocol. @property - def keys(self) -> Optional[Dict[str, AttributeValue]]: + def keys(self) -> Optional[Dict[str, AttributeValue]]: # type: ignore """The primary key attribute(s) for the DynamoDB item that was modified.""" return _attribute_value_dict(self._data, "Keys") diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index dbef57162e2..1ff0e32e7c4 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -74,6 +74,7 @@ AttributeValueType, DynamoDBRecordEventName, DynamoDBStreamEvent, + StreamRecord, StreamViewType, ) from aws_lambda_powertools.utilities.data_classes.event_source import event_source @@ -101,6 +102,19 @@ def message(self) -> str: assert DataClassSample(data1).raw_event is data1 +def test_dict_wrapper_imlements_mapping(): + class DataClassSample(DictWrapper): + pass + + data = {"message": "foo1"} + dcs = DataClassSample(data) + assert len(dcs) == len(data) + assert list(dcs) == list(data) + assert dcs.keys() == data.keys() + assert list(dcs.values()) == list(data.values()) + assert dcs.items() == data.items() + + def test_cloud_watch_dashboard_event(): event = CloudWatchDashboardCustomWidgetEvent(load_event("cloudWatchDashboardEvent.json")) assert event.describe is False @@ -617,6 +631,23 @@ def test_dynamo_attribute_value_type_error(): print(attribute_value.get_type) +def test_stream_record_keys_with_valid_keys(): + attribute_value = {"Foo": "Bar"} + sr = StreamRecord({"Keys": {"Key1": attribute_value}}) + assert sr.keys == {"Key1": AttributeValue(attribute_value)} + + +def test_stream_record_keys_with_no_keys(): + sr = StreamRecord({}) + assert sr.keys is None + + +def test_stream_record_keys_overrides_dict_wrapper_keys(): + data = {"Keys": {"key1": {"attr1": "value1"}}} + sr = StreamRecord(data) + assert sr.keys != data.keys() + + def test_event_bridge_event(): event = EventBridgeEvent(load_event("eventBridgeEvent.json")) From 9875e224b20fd4486f5e94928775918c891a3053 Mon Sep 17 00:00:00 2001 From: Adam Tankanow Date: Wed, 28 Sep 2022 09:34:02 -0400 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Heitor Lessa --- .../utilities/data_classes/common.py | 3 ++- .../data_classes/dynamo_db_stream_event.py | 6 ++--- tests/functional/test_data_classes.py | 26 +++++++++---------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/common.py b/aws_lambda_powertools/utilities/data_classes/common.py index 83c5c87b216..1b671489cdd 100644 --- a/aws_lambda_powertools/utilities/data_classes/common.py +++ b/aws_lambda_powertools/utilities/data_classes/common.py @@ -1,6 +1,7 @@ import base64 import json -from typing import Any, Dict, Iterator, Mapping, Optional +from collections.abc import Mapping +from typing import Any, Dict, Iterator, Optional class DictWrapper(Mapping): diff --git a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py index 28bbffdb510..eb674c86b60 100644 --- a/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py +++ b/aws_lambda_powertools/utilities/data_classes/dynamo_db_stream_event.py @@ -182,10 +182,10 @@ def approximate_creation_date_time(self) -> Optional[int]: item = self.get("ApproximateCreationDateTime") return None if item is None else int(item) - # This override breaks the Mapping protocol of DictWrapper, it's left here for backwards compatibility with - # a 'type: ignore' comment. This is currently the only subclass of DictWrapper that breaks this protocol. + # NOTE: This override breaks the Mapping protocol of DictWrapper, it's left here for backwards compatibility with + # a 'type: ignore' comment. See #1516 for discussion @property - def keys(self) -> Optional[Dict[str, AttributeValue]]: # type: ignore + def keys(self) -> Optional[Dict[str, AttributeValue]]: # type: ignore[override] """The primary key attribute(s) for the DynamoDB item that was modified.""" return _attribute_value_dict(self._data, "Keys") diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index 1ff0e32e7c4..f0ac4af0af0 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -102,17 +102,17 @@ def message(self) -> str: assert DataClassSample(data1).raw_event is data1 -def test_dict_wrapper_imlements_mapping(): +def test_dict_wrapper_implements_mapping(): class DataClassSample(DictWrapper): pass data = {"message": "foo1"} - dcs = DataClassSample(data) - assert len(dcs) == len(data) - assert list(dcs) == list(data) - assert dcs.keys() == data.keys() - assert list(dcs.values()) == list(data.values()) - assert dcs.items() == data.items() + event_source = DataClassSample(data) + assert len(event_source) == len(data) + assert list(event_source) == list(data) + assert event_source.keys() == data.keys() + assert list(event_source.values()) == list(data.values()) + assert event_source.items() == data.items() def test_cloud_watch_dashboard_event(): @@ -633,19 +633,19 @@ def test_dynamo_attribute_value_type_error(): def test_stream_record_keys_with_valid_keys(): attribute_value = {"Foo": "Bar"} - sr = StreamRecord({"Keys": {"Key1": attribute_value}}) - assert sr.keys == {"Key1": AttributeValue(attribute_value)} + record = StreamRecord({"Keys": {"Key1": attribute_value}}) + assert record.keys == {"Key1": AttributeValue(attribute_value)} def test_stream_record_keys_with_no_keys(): - sr = StreamRecord({}) - assert sr.keys is None + record = StreamRecord({}) + assert record.keys is None def test_stream_record_keys_overrides_dict_wrapper_keys(): data = {"Keys": {"key1": {"attr1": "value1"}}} - sr = StreamRecord(data) - assert sr.keys != data.keys() + record = StreamRecord(data) + assert record.keys != data.keys() def test_event_bridge_event():