From c13749b827e9bbecc874a8d756240b7a69ff76f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Sat, 4 Mar 2023 15:32:12 +0100 Subject: [PATCH 1/6] Correctly parse List[X] annotated objects from dictionaries --- pycardano/plutus.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pycardano/plutus.py b/pycardano/plutus.py index d0feed08..4fd3e722 100644 --- a/pycardano/plutus.py +++ b/pycardano/plutus.py @@ -589,6 +589,25 @@ def _dfs(obj): raise DeserializeException( f"Unexpected data structure: {f}." ) + elif ( + hasattr(f_info.type, "__origin__") + and f_info.type.__origin__ is list + ): + t_args = f_info.type.__args__ + if len(t_args) != 1: + raise DeserializeException( + f"List types need exactly one type argument, but got {t_args}" + ) + if "list" not in f: + raise DeserializeException(f"Expected type \"list\" for constructor List but got {f}") + t = t_args[0] + if ( + inspect.isclass(t) + and issubclass(t, PlutusData) + ): + converted_fields.append(t.from_dict(f)) + else: + converted_fields.append(_dfs(f)) else: converted_fields.append(_dfs(f)) return cls(*converted_fields) From 454ca4f265ef78ba4293903edc748d8024abc177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 14 Mar 2023 09:36:58 +0100 Subject: [PATCH 2/6] Formatting --- pycardano/plutus.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pycardano/plutus.py b/pycardano/plutus.py index 4fd3e722..3630a583 100644 --- a/pycardano/plutus.py +++ b/pycardano/plutus.py @@ -590,8 +590,8 @@ def _dfs(obj): f"Unexpected data structure: {f}." ) elif ( - hasattr(f_info.type, "__origin__") - and f_info.type.__origin__ is list + hasattr(f_info.type, "__origin__") + and f_info.type.__origin__ is list ): t_args = f_info.type.__args__ if len(t_args) != 1: @@ -599,12 +599,11 @@ def _dfs(obj): f"List types need exactly one type argument, but got {t_args}" ) if "list" not in f: - raise DeserializeException(f"Expected type \"list\" for constructor List but got {f}") + raise DeserializeException( + f'Expected type "list" for constructor List but got {f}' + ) t = t_args[0] - if ( - inspect.isclass(t) - and issubclass(t, PlutusData) - ): + if inspect.isclass(t) and issubclass(t, PlutusData): converted_fields.append(t.from_dict(f)) else: converted_fields.append(_dfs(f)) From 6f62e9a3c99c94bd218a43483af9b9cae4224640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 14 Mar 2023 09:45:25 +0100 Subject: [PATCH 3/6] Add testcase for data list --- test/pycardano/test_plutus.py | 37 +++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/test/pycardano/test_plutus.py b/test/pycardano/test_plutus.py index 8e0fdcf1..7fb046a9 100644 --- a/test/pycardano/test_plutus.py +++ b/test/pycardano/test_plutus.py @@ -1,6 +1,8 @@ -from dataclasses import dataclass, field +import unittest + +from dataclasses import dataclass from test.pycardano.util import check_two_way_cbor -from typing import Union, Optional +from typing import Union, List import pytest @@ -39,6 +41,11 @@ class LargestTest(PlutusData): CONSTR_ID = 9 +@dataclass +class ListTest(PlutusData): + a: List[LargestTest] + + @dataclass class VestingParam(PlutusData): CONSTR_ID = 1 @@ -72,6 +79,19 @@ def test_plutus_data(): check_two_way_cbor(my_vesting) +@unittest.skip( + "From CBOR is generally not correctly implemented for tags > 7, so this test fails" +) +def test_plutus_data_list_cbor(): + test = ListTest([LargestTest(), LargestTest()]) + + encoded_cbor = test.to_cbor() + + assert "d8799f82d9050280d9050280ff" == encoded_cbor + + assert test == ListTest.from_cbor(encoded_cbor) + + def test_plutus_data_json(): key_hash = bytes.fromhex("c2ff616e11299d9094ce0a7eb5b7284b705147a822f4ffbd471f971a") deadline = 1643235300000 @@ -95,6 +115,19 @@ def test_plutus_data_json(): assert my_vesting == VestingParam.from_json(encoded_json) +def test_plutus_data_list_json(): + test = ListTest([LargestTest(), LargestTest()]) + + encoded_json = test.to_json(separators=(",", ":")) + + assert ( + '{"constructor":0,"fields":[[{"constructor":9,"fields":[]},{"constructor":9,"fields":[]}]]}' + == encoded_json + ) + + assert test == ListTest.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 21580378f33c2faee0f2195ed670efdf9bf848aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 14 Mar 2023 09:50:48 +0100 Subject: [PATCH 4/6] Fix to_json of lists --- pycardano/plutus.py | 6 ++---- test/pycardano/test_plutus.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pycardano/plutus.py b/pycardano/plutus.py index 3630a583..579b66e9 100644 --- a/pycardano/plutus.py +++ b/pycardano/plutus.py @@ -529,16 +529,14 @@ def _dfs(obj): return {"int": obj} elif isinstance(obj, bytes): return {"bytes": obj.hex()} - elif isinstance(obj, list): - return [_dfs(item) for item in obj] - elif isinstance(obj, IndefiniteList): + elif isinstance(obj, IndefiniteList) or isinstance(obj, list): return {"list": [_dfs(item) for item in obj]} elif isinstance(obj, dict): return {"map": [{"v": _dfs(v), "k": _dfs(k)} for k, v in obj.items()]} elif isinstance(obj, PlutusData): return { "constructor": obj.CONSTR_ID, - "fields": _dfs([getattr(obj, f.name) for f in fields(obj)]), + "fields": [_dfs(getattr(obj, f.name)) for f in fields(obj)], } else: raise TypeError(f"Unexpected type {type(obj)}") diff --git a/test/pycardano/test_plutus.py b/test/pycardano/test_plutus.py index 7fb046a9..46968545 100644 --- a/test/pycardano/test_plutus.py +++ b/test/pycardano/test_plutus.py @@ -121,7 +121,7 @@ def test_plutus_data_list_json(): encoded_json = test.to_json(separators=(",", ":")) assert ( - '{"constructor":0,"fields":[[{"constructor":9,"fields":[]},{"constructor":9,"fields":[]}]]}' + '{"constructor":0,"fields":[{"list":[{"constructor":9,"fields":[]},{"constructor":9,"fields":[]}]}]}' == encoded_json ) From 1144117f5209904f76e2ca3669e11a31ef8358fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 14 Mar 2023 14:37:06 +0100 Subject: [PATCH 5/6] Fix parsing List[x] from cbor --- pycardano/serialization.py | 11 +++++++++++ test/pycardano/test_plutus.py | 3 --- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pycardano/serialization.py b/pycardano/serialization.py index e4dcc32f..f6b0c831 100644 --- a/pycardano/serialization.py +++ b/pycardano/serialization.py @@ -413,6 +413,17 @@ def _restore_dataclass_field( return f.metadata["object_hook"](v) elif isclass(f.type) and issubclass(f.type, CBORSerializable): return f.type.from_primitive(v) + elif hasattr(f.type, "__origin__") and (f.type.__origin__ is list): + t_args = f.type.__args__ + if len(t_args) != 1: + raise DeserializeException( + f"List types need exactly one type argument, but got {t_args}" + ) + t = t_args[0] + if isclass(t) and issubclass(t, CBORSerializable): + return IndefiniteList([t.from_primitive(w) for w in v]) + else: + return IndefiniteList(v) elif isclass(f.type) and issubclass(f.type, IndefiniteList): return IndefiniteList(v) elif hasattr(f.type, "__origin__") and ( diff --git a/test/pycardano/test_plutus.py b/test/pycardano/test_plutus.py index 46968545..3f92236e 100644 --- a/test/pycardano/test_plutus.py +++ b/test/pycardano/test_plutus.py @@ -79,9 +79,6 @@ def test_plutus_data(): check_two_way_cbor(my_vesting) -@unittest.skip( - "From CBOR is generally not correctly implemented for tags > 7, so this test fails" -) def test_plutus_data_list_cbor(): test = ListTest([LargestTest(), LargestTest()]) From e375088b9dee5df18bce2587c49975143d7f90a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Tue, 14 Mar 2023 14:46:00 +0100 Subject: [PATCH 6/6] Raise useful exception when encountering wrong type --- pycardano/serialization.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pycardano/serialization.py b/pycardano/serialization.py index f6b0c831..160f2512 100644 --- a/pycardano/serialization.py +++ b/pycardano/serialization.py @@ -420,6 +420,8 @@ def _restore_dataclass_field( f"List types need exactly one type argument, but got {t_args}" ) t = t_args[0] + if not isinstance(v, list): + raise DeserializeException(f"Expected type list but got {type(v)}") if isclass(t) and issubclass(t, CBORSerializable): return IndefiniteList([t.from_primitive(w) for w in v]) else: