Skip to content

Commit 7300341

Browse files
authored
Plomin deser (#420)
* Add OrderedSet as a serialization type * Minor fixes * All tests passing * Fix qa * Fix failed tests in ^3.11
1 parent f21348f commit 7300341

19 files changed

+673
-238
lines changed

.github/workflows/main.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
strategy:
1515
matrix:
1616
os: [ubuntu-latest, macos-latest]
17-
python-version: ['3.8', '3.9', '3.10', '3.11']
17+
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
1818

1919
steps:
2020
- uses: actions/checkout@v4

Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ clean-test: ## remove test and coverage artifacts
5757
test: ## runs tests
5858
poetry run pytest -vv -n 4
5959

60+
test-integration: ## runs integration tests
61+
cd integration-test && ./run_tests.sh
62+
6063
test-single: ## runs tests with "single" markers
6164
poetry run pytest -s -vv -m single
6265

integration-test/configs/local-chang/shelley-genesis.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"keyDeposit": 1000000,
2222
"protocolVersion": {
2323
"minor": 0,
24-
"major": 9
24+
"major": 10
2525
},
2626
"poolDeposit": 1000000,
2727
"a0": 0.0,

integration-test/docker-compose-chang.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ networks:
99
services:
1010

1111
cardano-node:
12-
image: ghcr.io/intersectmbo/cardano-node:${CARDANO_NODE_VERSION:-10.1.3}
12+
image: ghcr.io/intersectmbo/cardano-node:${CARDANO_NODE_VERSION:-10.1.4}
1313
platform: linux/amd64
1414
entrypoint: bash
1515
environment:
@@ -35,7 +35,7 @@ services:
3535
max-file: "10"
3636

3737
cardano-pool:
38-
image: ghcr.io/intersectmbo/cardano-node:${CARDANO_NODE_VERSION:-10.1.3}
38+
image: ghcr.io/intersectmbo/cardano-node:${CARDANO_NODE_VERSION:-10.1.4}
3939
platform: linux/amd64
4040
entrypoint: bash
4141
environment:
@@ -56,7 +56,7 @@ services:
5656
max-file: "10"
5757

5858
ogmios:
59-
image: cardanosolutions/ogmios:v6.9.0
59+
image: cardanosolutions/ogmios:v6.11.0
6060
platform: linux/amd64
6161
environment:
6262
NETWORK: "${NETWORK:-local-alonzo}"

integration-test/run_tests.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ set -o pipefail
55

66
ROOT=$(pwd)
77

8-
poetry install
8+
poetry install -C ..
99
#poetry run pip install ogmios
1010

1111
##########

poetry.lock

+87-83
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pycardano/certificate.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def __post_init__(self):
4545
self._CODE = 1
4646

4747
@classmethod
48-
@limit_primitive_type(list)
48+
@limit_primitive_type(list, tuple)
4949
def from_primitive(
5050
cls: Type[StakeCredential], values: Union[list, tuple]
5151
) -> StakeCredential:
@@ -67,7 +67,7 @@ def __post_init__(self):
6767
self._CODE = 0
6868

6969
@classmethod
70-
@limit_primitive_type(list)
70+
@limit_primitive_type(list, tuple)
7171
def from_primitive(
7272
cls: Type[StakeRegistration], values: Union[list, tuple]
7373
) -> StakeRegistration:
@@ -87,7 +87,7 @@ def __post_init__(self):
8787
self._CODE = 1
8888

8989
@classmethod
90-
@limit_primitive_type(list)
90+
@limit_primitive_type(list, tuple)
9191
def from_primitive(
9292
cls: Type[StakeDeregistration], values: Union[list, tuple]
9393
) -> StakeDeregistration:
@@ -109,7 +109,7 @@ def __post_init__(self):
109109
self._CODE = 2
110110

111111
@classmethod
112-
@limit_primitive_type(list)
112+
@limit_primitive_type(list, tuple)
113113
def from_primitive(
114114
cls: Type[StakeDelegation], values: Union[list, tuple]
115115
) -> StakeDelegation:
@@ -138,7 +138,7 @@ def to_primitive(self):
138138
return super().to_primitive()
139139

140140
@classmethod
141-
@limit_primitive_type(list)
141+
@limit_primitive_type(list, tuple)
142142
def from_primitive(
143143
cls: Type[PoolRegistration], values: Union[list, tuple]
144144
) -> PoolRegistration:
@@ -166,7 +166,7 @@ def __post_init__(self):
166166
self._CODE = 4
167167

168168
@classmethod
169-
@limit_primitive_type(list)
169+
@limit_primitive_type(list, tuple)
170170
def from_primitive(
171171
cls: Type[PoolRetirement], values: Union[list, tuple]
172172
) -> PoolRetirement:

pycardano/metadata.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ def to_primitive(self) -> Primitive:
123123
return self.data.to_primitive()
124124

125125
@classmethod
126-
def from_primitive(cls: Type[AuxiliaryData], value: Primitive) -> AuxiliaryData:
126+
def from_primitive(
127+
cls: Type[AuxiliaryData], value: Primitive, type_args: Optional[tuple] = None
128+
) -> AuxiliaryData:
127129
for t in [AlonzoMetadata, ShelleyMarryMetadata, Metadata]:
128130
# The schema of metadata in different eras are mutually exclusive, so we can try deserializing
129131
# them one by one without worrying about mismatch.

pycardano/nativescript.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class NativeScript(ArrayCBORSerializable):
3535
json_field: ClassVar[str]
3636

3737
@classmethod
38-
@limit_primitive_type(list)
38+
@limit_primitive_type(list, tuple)
3939
def from_primitive(
4040
cls: Type[NativeScript], value: list
4141
) -> Union[

pycardano/plutus.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -989,7 +989,7 @@ class Redeemer(ArrayCBORSerializable):
989989
ex_units: Optional[ExecutionUnits] = None
990990

991991
@classmethod
992-
@limit_primitive_type(list)
992+
@limit_primitive_type(list, tuple)
993993
def from_primitive(cls: Type[Redeemer], values: list) -> Redeemer:
994994
if isinstance(values[2], CBORTag) and cls is Redeemer:
995995
values[2] = RawPlutusData.from_primitive(values[2])
@@ -1028,7 +1028,7 @@ class RedeemerValue(ArrayCBORSerializable):
10281028
ex_units: ExecutionUnits
10291029

10301030
@classmethod
1031-
@limit_primitive_type(list)
1031+
@limit_primitive_type(list, tuple)
10321032
def from_primitive(cls: Type[RedeemerValue], values: list) -> RedeemerValue:
10331033
if isinstance(values[0], CBORTag) and cls is RedeemerValue:
10341034
values[0] = RawPlutusData.from_primitive(values[0])

pycardano/pool_params.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def to_primitive(self) -> list:
165165
]
166166

167167
@classmethod
168-
@limit_primitive_type(list)
168+
@limit_primitive_type(list, tuple)
169169
def from_primitive(
170170
cls: Type[SingleHostAddr], values: Union[list, tuple]
171171
) -> SingleHostAddr:
@@ -190,7 +190,7 @@ def __post_init__(self):
190190
self._CODE = 1
191191

192192
@classmethod
193-
@limit_primitive_type(list)
193+
@limit_primitive_type(list, tuple)
194194
def from_primitive(
195195
cls: Type[SingleHostName], values: Union[list, tuple]
196196
) -> SingleHostName:
@@ -213,7 +213,7 @@ def __post_init__(self):
213213
self._CODE = 2
214214

215215
@classmethod
216-
@limit_primitive_type(list)
216+
@limit_primitive_type(list, tuple)
217217
def from_primitive(
218218
cls: Type[MultiHostName], values: Union[list, tuple]
219219
) -> MultiHostName:

pycardano/serialization.py

+126-7
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,35 @@
1010
from datetime import datetime
1111
from decimal import Decimal
1212
from functools import wraps
13-
from inspect import isclass
13+
from inspect import getfullargspec, isclass
1414
from typing import (
1515
Any,
1616
Callable,
1717
ClassVar,
1818
Dict,
19+
Generic,
20+
Iterable,
1921
List,
2022
Optional,
23+
Set,
2124
Type,
2225
TypeVar,
2326
Union,
27+
cast,
2428
get_type_hints,
2529
)
2630

31+
import cbor2
32+
33+
from pycardano.logging import logger
34+
35+
# Remove the semantic decoder for 258 (CBOR tag for set) as we care about the order of elements
36+
try:
37+
cbor2._decoder.semantic_decoders.pop(258)
38+
except Exception as e:
39+
logger.warning("Failed to remove semantic decoder for CBOR tag 258", e)
40+
pass
41+
2742
from cbor2 import CBOREncoder, CBORSimpleValue, CBORTag, dumps, loads, undefined
2843
from frozendict import frozendict
2944
from frozenlist import FrozenList
@@ -44,8 +59,12 @@
4459
"RawCBOR",
4560
"list_hook",
4661
"limit_primitive_type",
62+
"OrderedSet",
63+
"NonEmptyOrderedSet",
4764
]
4865

66+
T = TypeVar("T")
67+
4968

5069
def _identity(x):
5170
return x
@@ -314,10 +333,12 @@ def validate(self):
314333
def _check_recursive(value, type_hint):
315334
if type_hint is Any:
316335
return True
336+
337+
if isinstance(value, CBORSerializable):
338+
value.validate()
339+
317340
origin = getattr(type_hint, "__origin__", None)
318341
if origin is None:
319-
if isinstance(value, CBORSerializable):
320-
value.validate()
321342
return isinstance(value, type_hint)
322343
elif origin is ClassVar:
323344
return _check_recursive(value, type_hint.__args__[0])
@@ -329,7 +350,7 @@ def _check_recursive(value, type_hint):
329350
_check_recursive(k, key_type) and _check_recursive(v, value_type)
330351
for k, v in value.items()
331352
)
332-
elif origin in (list, set, tuple, frozenset):
353+
elif origin in (list, set, tuple, frozenset, OrderedSet):
333354
if value is None:
334355
return True
335356
args = type_hint.__args__
@@ -364,12 +385,15 @@ def to_validated_primitive(self) -> Primitive:
364385
return self.to_primitive()
365386

366387
@classmethod
367-
def from_primitive(cls: Type[CBORBase], value: Any) -> CBORBase:
388+
def from_primitive(
389+
cls: Type[CBORBase], value: Any, type_args: Optional[tuple] = None
390+
) -> CBORBase:
368391
"""Turn a CBOR primitive to its original class type.
369392
370393
Args:
371394
cls (CBORBase): The original class type.
372395
value (:const:`Primitive`): A CBOR primitive.
396+
type_args (Optional[tuple]): Type arguments for the class.
373397
374398
Returns:
375399
CBORBase: A CBOR serializable object.
@@ -519,10 +543,26 @@ def _restore_typed_primitive(
519543
Union[:const:`Primitive`, CBORSerializable]: A CBOR primitive or a CBORSerializable.
520544
"""
521545

546+
is_cbor_serializable = False
547+
try:
548+
is_cbor_serializable = issubclass(t, CBORSerializable)
549+
except TypeError:
550+
# Handle the case when t is a generic alias
551+
origin = typing.get_origin(t)
552+
if origin is not None:
553+
try:
554+
is_cbor_serializable = issubclass(origin, CBORSerializable)
555+
except TypeError:
556+
pass
557+
522558
if t is Any or (t in PRIMITIVE_TYPES and isinstance(v, t)):
523559
return v
524-
elif isclass(t) and issubclass(t, CBORSerializable):
525-
return t.from_primitive(v)
560+
elif is_cbor_serializable:
561+
if "type_args" in getfullargspec(t.from_primitive).args:
562+
args = typing.get_args(t)
563+
return t.from_primitive(v, type_args=args)
564+
else:
565+
return t.from_primitive(v)
526566
elif hasattr(t, "__origin__") and (t.__origin__ is list):
527567
t_args = t.__args__
528568
if len(t_args) != 1:
@@ -941,3 +981,82 @@ def list_hook(
941981
CBORSerializables.
942982
"""
943983
return lambda vals: [cls.from_primitive(v) for v in vals]
984+
985+
986+
class OrderedSet(list, Generic[T], CBORSerializable):
987+
def __init__(self, iterable: Optional[List[T]] = None, use_tag: bool = True):
988+
super().__init__()
989+
self._set: Set[str] = set()
990+
self._use_tag = use_tag
991+
if iterable:
992+
self.extend(iterable)
993+
994+
def append(self, item: T) -> None:
995+
item_key = str(item)
996+
if item_key not in self._set:
997+
super().append(item)
998+
self._set.add(item_key)
999+
1000+
def extend(self, items: Iterable[T]) -> None:
1001+
for item in items:
1002+
self.append(item)
1003+
1004+
def __contains__(self, item: object) -> bool:
1005+
return str(item) in self._set
1006+
1007+
def __eq__(self, other: object) -> bool:
1008+
if not isinstance(other, OrderedSet):
1009+
if isinstance(other, list):
1010+
return list(self) == other
1011+
return False
1012+
return list(self) == list(other)
1013+
1014+
def __repr__(self) -> str:
1015+
return f"{self.__class__.__name__}({list(self)})"
1016+
1017+
def to_shallow_primitive(self) -> Union[CBORTag, List[T]]:
1018+
if self._use_tag:
1019+
return CBORTag(258, list(self))
1020+
return list(self)
1021+
1022+
@classmethod
1023+
def from_primitive(
1024+
cls: Type[OrderedSet[T]], value: Primitive, type_args: Optional[tuple] = None
1025+
) -> OrderedSet[T]:
1026+
assert (
1027+
type_args is None or len(type_args) == 1
1028+
), "OrderedSet should have exactly one type argument"
1029+
# Retrieve the type arguments from the class
1030+
type_arg = type_args[0] if type_args else None
1031+
1032+
if isinstance(value, CBORTag) and value.tag == 258:
1033+
if isclass(type_arg) and issubclass(type_arg, CBORSerializable):
1034+
value.value = [type_arg.from_primitive(v) for v in value.value]
1035+
return cls(value.value, use_tag=True)
1036+
1037+
if isinstance(value, (list, tuple, set)):
1038+
if isclass(type_arg) and issubclass(type_arg, CBORSerializable):
1039+
value = [type_arg.from_primitive(v) for v in value]
1040+
return cls(list(value), use_tag=False)
1041+
1042+
raise ValueError(f"Cannot deserialize {value} to {cls}")
1043+
1044+
1045+
class NonEmptyOrderedSet(OrderedSet[T]):
1046+
def __init__(self, iterable: Optional[List[T]] = None, use_tag: bool = True):
1047+
super().__init__(iterable, use_tag)
1048+
1049+
def validate(self):
1050+
if not self:
1051+
raise ValueError("NonEmptyOrderedSet cannot be empty")
1052+
1053+
@classmethod
1054+
def from_primitive(
1055+
cls: Type[NonEmptyOrderedSet[T]],
1056+
value: Primitive,
1057+
type_args: Optional[tuple] = None,
1058+
) -> NonEmptyOrderedSet[T]:
1059+
result = cast(NonEmptyOrderedSet[T], super().from_primitive(value, type_args))
1060+
if not result:
1061+
raise ValueError("NonEmptyOrderedSet cannot be empty")
1062+
return result

0 commit comments

Comments
 (0)