From dd4ad05d7234e930cc8d90903ba02f7abc7444aa Mon Sep 17 00:00:00 2001 From: Jakub Ptak Date: Tue, 13 Dec 2022 13:13:33 +0100 Subject: [PATCH 1/6] Add TupleDataclass --- starknet_py/utils/tuple_dataclass.py | 41 +++++++++++++++++++++++ starknet_py/utils/tuple_dataclass_test.py | 22 ++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 starknet_py/utils/tuple_dataclass.py create mode 100644 starknet_py/utils/tuple_dataclass_test.py diff --git a/starknet_py/utils/tuple_dataclass.py b/starknet_py/utils/tuple_dataclass.py new file mode 100644 index 000000000..e7cb13e8c --- /dev/null +++ b/starknet_py/utils/tuple_dataclass.py @@ -0,0 +1,41 @@ +from __future__ import annotations +from dataclasses import dataclass, fields, make_dataclass +from typing import Dict, Optional, Tuple + + +@dataclass(frozen=True) +class TupleDataclass: + """ + Dataclass that behaves like a tuple at the same time. + """ + + # getattr is called when attribute is not found in object + # This way pyright will know that there might be some arguments it doesn't know about and will stop complaining + # about some fields that don't exist statically. + def __getattr__(self, item): + # This should always fail - only attributes that don't exist end up in here. + # We use __getattribute__ to get the native error. + return super().__getattribute__(item) + + def __getitem__(self, item): + field = fields(self)[item] + return getattr(self, field.name) + + def __iter__(self): + return (getattr(self, field.name) for field in fields(self)) + + def as_tuple(self) -> Tuple: + return tuple(self) + + def as_dict(self) -> Dict: + return {field.name: getattr(self, field.name) for field in fields(self)} + + @staticmethod + def from_dict(data: Dict, *, name: Optional[str] = None) -> TupleDataclass: + result_class = make_dataclass( + name or "TupleDataclass", + fields=[(key, type(value)) for key, value in data.items()], + bases=(TupleDataclass,), + frozen=True, + ) + return result_class(**data) diff --git a/starknet_py/utils/tuple_dataclass_test.py b/starknet_py/utils/tuple_dataclass_test.py new file mode 100644 index 000000000..1fff31283 --- /dev/null +++ b/starknet_py/utils/tuple_dataclass_test.py @@ -0,0 +1,22 @@ +from starknet_py.utils.tuple_dataclass import TupleDataclass + + +def test_wrapped_named_tuple(): + input_dict = { + "first": 1, + "second": 2, + "third": {"key": "value"}, + } + input_tuple = tuple(input_dict.values()) + + result = TupleDataclass.from_dict(input_dict) + assert result.as_tuple() == input_tuple + assert result.as_dict() == input_dict + assert (result[0], result[1], result[2]) == input_tuple + assert (result.first, result.second, result.third) == input_tuple + assert str(result) == "TupleDataclass(first=1, second=2, third={'key': 'value'})" + assert repr(result) == "TupleDataclass(first=1, second=2, third={'key': 'value'})" + + result = TupleDataclass.from_dict(input_dict, name="CustomClass") + assert str(result) == "CustomClass(first=1, second=2, third={'key': 'value'})" + assert repr(result) == "CustomClass(first=1, second=2, third={'key': 'value'})" From 54960ffe332a0450e6c46a0598bf7419656e7d2c Mon Sep 17 00:00:00 2001 From: Jakub Ptak Date: Wed, 14 Dec 2022 08:28:09 +0100 Subject: [PATCH 2/6] Add review changes --- .../utils/data_transformer/function_calldata_serializer.py | 0 starknet_py/utils/tuple_dataclass.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 starknet_py/utils/data_transformer/function_calldata_serializer.py diff --git a/starknet_py/utils/data_transformer/function_calldata_serializer.py b/starknet_py/utils/data_transformer/function_calldata_serializer.py new file mode 100644 index 000000000..e69de29bb diff --git a/starknet_py/utils/tuple_dataclass.py b/starknet_py/utils/tuple_dataclass.py index e7cb13e8c..f53b90124 100644 --- a/starknet_py/utils/tuple_dataclass.py +++ b/starknet_py/utils/tuple_dataclass.py @@ -9,7 +9,7 @@ class TupleDataclass: Dataclass that behaves like a tuple at the same time. """ - # getattr is called when attribute is not found in object + # getattr is called when attribute is not found in object. For instance when using object.unknown_attribute. # This way pyright will know that there might be some arguments it doesn't know about and will stop complaining # about some fields that don't exist statically. def __getattr__(self, item): @@ -17,7 +17,7 @@ def __getattr__(self, item): # We use __getattribute__ to get the native error. return super().__getattribute__(item) - def __getitem__(self, item): + def __getitem__(self, item: int): field = fields(self)[item] return getattr(self, field.name) From afbbd7cb5488113fbb1ac755fdb23bb08c8dc802 Mon Sep 17 00:00:00 2001 From: Jakub Ptak Date: Wed, 14 Dec 2022 08:37:52 +0100 Subject: [PATCH 3/6] Add _addict method to TupleDataclass --- starknet_py/utils/tuple_dataclass.py | 4 ++++ starknet_py/utils/tuple_dataclass_test.py | 1 + 2 files changed, 5 insertions(+) diff --git a/starknet_py/utils/tuple_dataclass.py b/starknet_py/utils/tuple_dataclass.py index f53b90124..69f1e5b9b 100644 --- a/starknet_py/utils/tuple_dataclass.py +++ b/starknet_py/utils/tuple_dataclass.py @@ -30,6 +30,10 @@ def as_tuple(self) -> Tuple: def as_dict(self) -> Dict: return {field.name: getattr(self, field.name) for field in fields(self)} + # Added for backward compatibility with previous implementation based on NamedTuple + def _asdict(self): + return self.as_dict() + @staticmethod def from_dict(data: Dict, *, name: Optional[str] = None) -> TupleDataclass: result_class = make_dataclass( diff --git a/starknet_py/utils/tuple_dataclass_test.py b/starknet_py/utils/tuple_dataclass_test.py index 1fff31283..a04692723 100644 --- a/starknet_py/utils/tuple_dataclass_test.py +++ b/starknet_py/utils/tuple_dataclass_test.py @@ -12,6 +12,7 @@ def test_wrapped_named_tuple(): result = TupleDataclass.from_dict(input_dict) assert result.as_tuple() == input_tuple assert result.as_dict() == input_dict + assert result._asdict() == input_dict assert (result[0], result[1], result[2]) == input_tuple assert (result.first, result.second, result.third) == input_tuple assert str(result) == "TupleDataclass(first=1, second=2, third={'key': 'value'})" From c267d9db0524e13543d1829809b393a7fce9c5dd Mon Sep 17 00:00:00 2001 From: Jakub Ptak Date: Wed, 14 Dec 2022 08:56:05 +0100 Subject: [PATCH 4/6] Add __eq__ method to TupleDataclass --- starknet_py/utils/tuple_dataclass.py | 8 +++++++- starknet_py/utils/tuple_dataclass_test.py | 4 ++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/starknet_py/utils/tuple_dataclass.py b/starknet_py/utils/tuple_dataclass.py index 69f1e5b9b..1fe523388 100644 --- a/starknet_py/utils/tuple_dataclass.py +++ b/starknet_py/utils/tuple_dataclass.py @@ -3,7 +3,7 @@ from typing import Dict, Optional, Tuple -@dataclass(frozen=True) +@dataclass(frozen=True, eq=False) class TupleDataclass: """ Dataclass that behaves like a tuple at the same time. @@ -34,6 +34,11 @@ def as_dict(self) -> Dict: def _asdict(self): return self.as_dict() + def __eq__(self, other): + if isinstance(other, TupleDataclass): + return self.as_tuple() == other.as_tuple() + return self.as_tuple() == other + @staticmethod def from_dict(data: Dict, *, name: Optional[str] = None) -> TupleDataclass: result_class = make_dataclass( @@ -41,5 +46,6 @@ def from_dict(data: Dict, *, name: Optional[str] = None) -> TupleDataclass: fields=[(key, type(value)) for key, value in data.items()], bases=(TupleDataclass,), frozen=True, + eq=False, ) return result_class(**data) diff --git a/starknet_py/utils/tuple_dataclass_test.py b/starknet_py/utils/tuple_dataclass_test.py index a04692723..a131fd7c8 100644 --- a/starknet_py/utils/tuple_dataclass_test.py +++ b/starknet_py/utils/tuple_dataclass_test.py @@ -10,6 +10,10 @@ def test_wrapped_named_tuple(): input_tuple = tuple(input_dict.values()) result = TupleDataclass.from_dict(input_dict) + first = input_tuple == result + second = result == input_tuple + assert first + assert second assert result.as_tuple() == input_tuple assert result.as_dict() == input_dict assert result._asdict() == input_dict From be038ddda01645d20a39ba2ea1daf5aa433a04de Mon Sep 17 00:00:00 2001 From: Jakub Ptak Date: Wed, 14 Dec 2022 09:26:39 +0100 Subject: [PATCH 5/6] Change test --- starknet_py/utils/tuple_dataclass_test.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/starknet_py/utils/tuple_dataclass_test.py b/starknet_py/utils/tuple_dataclass_test.py index a131fd7c8..c1a243420 100644 --- a/starknet_py/utils/tuple_dataclass_test.py +++ b/starknet_py/utils/tuple_dataclass_test.py @@ -10,10 +10,9 @@ def test_wrapped_named_tuple(): input_tuple = tuple(input_dict.values()) result = TupleDataclass.from_dict(input_dict) - first = input_tuple == result - second = result == input_tuple - assert first - assert second + # __eq__ check + assert input_tuple == result + assert result == input_tuple assert result.as_tuple() == input_tuple assert result.as_dict() == input_dict assert result._asdict() == input_dict From 6fbda51cf36e0c9a1e02dabfce1862557885955a Mon Sep 17 00:00:00 2001 From: Jakub Ptak Date: Wed, 14 Dec 2022 11:19:03 +0100 Subject: [PATCH 6/6] Change test --- starknet_py/utils/tuple_dataclass_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/starknet_py/utils/tuple_dataclass_test.py b/starknet_py/utils/tuple_dataclass_test.py index c1a243420..e59df0d02 100644 --- a/starknet_py/utils/tuple_dataclass_test.py +++ b/starknet_py/utils/tuple_dataclass_test.py @@ -1,3 +1,5 @@ +import pytest + from starknet_py.utils.tuple_dataclass import TupleDataclass @@ -10,9 +12,12 @@ def test_wrapped_named_tuple(): input_tuple = tuple(input_dict.values()) result = TupleDataclass.from_dict(input_dict) + # __eq__ check assert input_tuple == result assert result == input_tuple + assert result == TupleDataclass.from_dict(input_dict, name="OtherNAme") + assert result.as_tuple() == input_tuple assert result.as_dict() == input_dict assert result._asdict() == input_dict @@ -24,3 +29,8 @@ def test_wrapped_named_tuple(): result = TupleDataclass.from_dict(input_dict, name="CustomClass") assert str(result) == "CustomClass(first=1, second=2, third={'key': 'value'})" assert repr(result) == "CustomClass(first=1, second=2, third={'key': 'value'})" + + with pytest.raises( + AttributeError, match="object has no attribute 'unknown_attribute'" + ): + result.unknown_attribute()