From dff520a1cb7ab4dc94e33b82d3732fba0da7d73d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 14 Mar 2023 14:23:07 +0100 Subject: [PATCH 1/4] Add support for complex dictionary types --- pycardano/plutus.py | 29 +++++++++++++++++++++++++++++ test/pycardano/test_plutus.py | 22 +++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/pycardano/plutus.py b/pycardano/plutus.py index d0feed08..6e6cab5b 100644 --- a/pycardano/plutus.py +++ b/pycardano/plutus.py @@ -589,6 +589,35 @@ def _dfs(obj): raise DeserializeException( f"Unexpected data structure: {f}." ) + elif ( + hasattr(f_info.type, "__origin__") + and f_info.type.__origin__ is dict + ): + t_args = f_info.type.__args__ + if len(t_args) != 2: + raise DeserializeException( + "Dict type with wrong number of arguments" + ) + if "map" not in f: + raise DeserializeException( + f'Expected type "map" in object but got "{f}"' + ) + key_t = t_args[0] + val_t = t_args[1] + if inspect.isclass(key_t) and issubclass(key_t, PlutusData): + key_convert = key_t.from_dict + else: + key_convert = _dfs + if inspect.isclass(val_t) and issubclass(val_t, PlutusData): + val_convert = val_t.from_dict + else: + val_convert = _dfs + converted_fields.append( + { + key_convert(pair["k"]): val_convert(pair["v"]) + for pair in f["map"] + } + ) else: converted_fields.append(_dfs(f)) return cls(*converted_fields) diff --git a/test/pycardano/test_plutus.py b/test/pycardano/test_plutus.py index 8e0fdcf1..298e8b71 100644 --- a/test/pycardano/test_plutus.py +++ b/test/pycardano/test_plutus.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field from test.pycardano.util import check_two_way_cbor -from typing import Union, Optional +from typing import Union, Optional, Dict import pytest @@ -39,6 +39,13 @@ class LargestTest(PlutusData): CONSTR_ID = 9 +@dataclass +class DictTest(PlutusData): + CONSTR_ID = 3 + + a: Dict[int, LargestTest] + + @dataclass class VestingParam(PlutusData): CONSTR_ID = 1 @@ -95,6 +102,19 @@ def test_plutus_data_json(): assert my_vesting == VestingParam.from_json(encoded_json) +def test_plutus_data_json_dict(): + test = DictTest({0: LargestTest(), 1: LargestTest()}) + + encoded_json = test.to_json(separators=(",", ":")) + + assert ( + '{"constructor":3,"fields":[{"map":[{"v":{"constructor":9,"fields":[]},"k":{"int":0}},{"v":{"constructor":9,"fields":[]},"k":{"int":1}}]}]}' + == encoded_json + ) + + assert test == DictTest.from_json(encoded_json) + + def test_plutus_data_to_json_wrong_type(): test = MyTest(123, b"1234", IndefiniteList([4, 5, 6]), {1: b"1", 2: b"2"}) test.a = "123" From 90595d32afc045d35adad7a43bf8b4db98a62062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 14 Mar 2023 14:26:35 +0100 Subject: [PATCH 2/4] Add a cbor test --- test/pycardano/test_plutus.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test/pycardano/test_plutus.py b/test/pycardano/test_plutus.py index 298e8b71..59a286fd 100644 --- a/test/pycardano/test_plutus.py +++ b/test/pycardano/test_plutus.py @@ -1,6 +1,8 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass +import unittest + from test.pycardano.util import check_two_way_cbor -from typing import Union, Optional, Dict +from typing import Union, Dict import pytest @@ -115,6 +117,17 @@ def test_plutus_data_json_dict(): assert test == DictTest.from_json(encoded_json) +@unittest.skip("Plutus tags not supported for cbor entirely right now") +def test_plutus_data_cbor_dict(): + test = DictTest({0: LargestTest(), 1: LargestTest()}) + + encoded_cbor = test.to_cbor() + + assert "d87c9fa200d905028001d9050280ff" == encoded_cbor + + assert test == DictTest.from_cbor(encoded_cbor) + + def test_plutus_data_to_json_wrong_type(): test = MyTest(123, b"1234", IndefiniteList([4, 5, 6]), {1: b"1", 2: b"2"}) test.a = "123" From 895d1a4be9a22d1c8a4b2be0b936f938d6510510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 14 Mar 2023 14:45:12 +0100 Subject: [PATCH 3/4] Fix parsing CBOR of dicts --- pycardano/serialization.py | 23 +++++++++++++++++++++++ test/pycardano/test_plutus.py | 1 - 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pycardano/serialization.py b/pycardano/serialization.py index e4dcc32f..cdb3e033 100644 --- a/pycardano/serialization.py +++ b/pycardano/serialization.py @@ -37,6 +37,10 @@ ] +def identity(x): + return x + + class IndefiniteList(UserList): def __init__(self, li: Primitive): # type: ignore super().__init__(li) # type: ignore @@ -415,6 +419,25 @@ def _restore_dataclass_field( return f.type.from_primitive(v) elif isclass(f.type) and issubclass(f.type, IndefiniteList): return IndefiniteList(v) + elif hasattr(f.type, "__origin__") and (f.type.__origin__ is dict): + t_args = f.type.__args__ + if len(t_args) != 2: + raise DeserializeException( + f"Dict types need exactly two type arguments, but got {t_args}" + ) + key_t = t_args[0] + val_t = t_args[1] + if isclass(key_t) and issubclass(key_t, CBORSerializable): + key_converter = key_t.from_primitive + else: + key_converter = identity + if isclass(val_t) and issubclass(val_t, CBORSerializable): + val_converter = val_t.from_primitive + else: + val_converter = identity + if not isinstance(v, dict): + raise DeserializeException(f"Expected dict type but got {type(v)}") + return {key_converter(key): val_converter(val) for key, val in v.items()} elif hasattr(f.type, "__origin__") and ( f.type.__origin__ is Union or f.type.__origin__ is Optional ): diff --git a/test/pycardano/test_plutus.py b/test/pycardano/test_plutus.py index 59a286fd..600f45c5 100644 --- a/test/pycardano/test_plutus.py +++ b/test/pycardano/test_plutus.py @@ -117,7 +117,6 @@ def test_plutus_data_json_dict(): assert test == DictTest.from_json(encoded_json) -@unittest.skip("Plutus tags not supported for cbor entirely right now") def test_plutus_data_cbor_dict(): test = DictTest({0: LargestTest(), 1: LargestTest()}) From 84624a20d548a0ab183ebc7dac52188a392fcfac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 15 Mar 2023 12:22:19 +0100 Subject: [PATCH 4/4] Hide identity --- pycardano/serialization.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pycardano/serialization.py b/pycardano/serialization.py index cdb3e033..4e226fa3 100644 --- a/pycardano/serialization.py +++ b/pycardano/serialization.py @@ -37,7 +37,7 @@ ] -def identity(x): +def _identity(x): return x @@ -430,11 +430,11 @@ def _restore_dataclass_field( if isclass(key_t) and issubclass(key_t, CBORSerializable): key_converter = key_t.from_primitive else: - key_converter = identity + key_converter = _identity if isclass(val_t) and issubclass(val_t, CBORSerializable): val_converter = val_t.from_primitive else: - val_converter = identity + val_converter = _identity if not isinstance(v, dict): raise DeserializeException(f"Expected dict type but got {type(v)}") return {key_converter(key): val_converter(val) for key, val in v.items()}