From 71fdd5fff64478e752cdb278268c600fc7583b50 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Wed, 14 May 2025 17:22:34 +0200 Subject: [PATCH 1/5] fix: fixes type checking of tuples with primitive types Adds checks to ensure that type validation applies to classes only. --- src/_algopy_testing/serialize.py | 109 ++++++++++++++++--------------- 1 file changed, 57 insertions(+), 52 deletions(-) diff --git a/src/_algopy_testing/serialize.py b/src/_algopy_testing/serialize.py index f5a337b..73ba6cf 100644 --- a/src/_algopy_testing/serialize.py +++ b/src/_algopy_testing/serialize.py @@ -25,66 +25,71 @@ def identity(i: _T) -> _T: return i -def get_native_to_arc4_serializer(typ: type[_T]) -> _Serializer: # type: ignore[type-arg] # noqa: PLR0911 +def get_native_to_arc4_serializer(typ: type[_T]) -> _Serializer: # type: ignore[type-arg] # noqa: PLR0911, PLR0912 from _algopy_testing import arc4 from _algopy_testing.models import Account from _algopy_testing.primitives import BigUInt, Bytes, ImmutableArray, String from _algopy_testing.protocols import UInt64Backed - if issubclass(typ, arc4._ABIEncoded): + origin_typ = typing.get_origin(typ) + + if inspect.isclass(typ) and issubclass(typ, arc4._ABIEncoded): return _Serializer( native_type=typ, arc4_type=typ, native_to_arc4=identity, arc4_to_native=identity ) - if issubclass(typ, bool): - return _Serializer( - native_type=typ, - arc4_type=arc4.Bool, - native_to_arc4=arc4.Bool, - arc4_to_native=lambda n: n.native, - ) - if issubclass(typ, UInt64Backed): - return _Serializer( - native_type=typ, - arc4_type=arc4.UInt64, - native_to_arc4=lambda n: arc4.UInt64(n.int_), - arc4_to_native=lambda a: typ.from_int(a.native), - ) - if issubclass(typ, BigUInt): - return _Serializer( - native_type=typ, - arc4_type=arc4.UInt512, - native_to_arc4=arc4.UInt512, - arc4_to_native=lambda a: a.native, - ) - if issubclass(typ, Account): - return _Serializer( - native_type=typ, - arc4_type=arc4.Address, - native_to_arc4=arc4.Address, - arc4_to_native=lambda a: a.native, - ) - if issubclass(typ, UInt64): - return _Serializer( - native_type=typ, - arc4_type=arc4.UInt64, - native_to_arc4=arc4.UInt64, - arc4_to_native=lambda a: a.native, - ) - if issubclass(typ, Bytes): - return _Serializer( - native_type=typ, - arc4_type=arc4.DynamicBytes, - native_to_arc4=arc4.DynamicBytes, - arc4_to_native=lambda a: a.native, - ) - if issubclass(typ, String): - return _Serializer( - native_type=typ, - arc4_type=arc4.String, - native_to_arc4=arc4.String, - arc4_to_native=lambda a: a.native, - ) - if issubclass(typ, tuple) or typing.get_origin(typ) is tuple: + # For types that are expected to be simple classes for these specific checks + if inspect.isclass(typ): + if issubclass(typ, bool): + return _Serializer( + native_type=typ, + arc4_type=arc4.Bool, + native_to_arc4=arc4.Bool, + arc4_to_native=lambda n: n.native, + ) + if issubclass(typ, UInt64Backed): + return _Serializer( + native_type=typ, + arc4_type=arc4.UInt64, + native_to_arc4=lambda n: arc4.UInt64(n.int_), + arc4_to_native=lambda a: typ.from_int(a.native), + ) + if issubclass(typ, BigUInt): + return _Serializer( + native_type=typ, + arc4_type=arc4.UInt512, + native_to_arc4=arc4.UInt512, + arc4_to_native=lambda a: a.native, + ) + if issubclass(typ, Account): + return _Serializer( + native_type=typ, + arc4_type=arc4.Address, + native_to_arc4=arc4.Address, + arc4_to_native=lambda a: a.native, + ) + if issubclass(typ, UInt64): + return _Serializer( + native_type=typ, + arc4_type=arc4.UInt64, + native_to_arc4=arc4.UInt64, + arc4_to_native=lambda a: a.native, + ) + if issubclass(typ, Bytes): + return _Serializer( + native_type=typ, + arc4_type=arc4.DynamicBytes, + native_to_arc4=arc4.DynamicBytes, + arc4_to_native=lambda a: a.native, + ) + if issubclass(typ, String): + return _Serializer( + native_type=typ, + arc4_type=arc4.String, + native_to_arc4=arc4.String, + arc4_to_native=lambda a: a.native, + ) + + if origin_typ is tuple or (inspect.isclass(typ) and issubclass(typ, tuple)): if typing.NamedTuple in getattr(typ, "__orig_bases__", []): tuple_fields: Sequence[type] = list(inspect.get_annotations(typ).values()) else: From 8189ec866e9ef448f27bcb88c7cac81639af5075 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Wed, 14 May 2025 17:48:20 +0200 Subject: [PATCH 2/5] chore: add simple test for tuple with primitives in return type --- tests/arc4/test_tuple.py | 15 +++ tests/artifacts/Tuples/__init__.py | 0 tests/artifacts/Tuples/contract.py | 11 ++ .../Tuples/data/TuplesContract.approval.teal | 42 ++++++++ .../Tuples/data/TuplesContract.arc32.json | 50 +++++++++ .../Tuples/data/TuplesContract.arc56.json | 102 ++++++++++++++++++ .../Tuples/data/TuplesContract.clear.teal | 7 ++ 7 files changed, 227 insertions(+) create mode 100644 tests/artifacts/Tuples/__init__.py create mode 100644 tests/artifacts/Tuples/contract.py create mode 100644 tests/artifacts/Tuples/data/TuplesContract.approval.teal create mode 100644 tests/artifacts/Tuples/data/TuplesContract.arc32.json create mode 100644 tests/artifacts/Tuples/data/TuplesContract.arc56.json create mode 100644 tests/artifacts/Tuples/data/TuplesContract.clear.teal diff --git a/tests/arc4/test_tuple.py b/tests/arc4/test_tuple.py index 05c7a23..2b19644 100644 --- a/tests/arc4/test_tuple.py +++ b/tests/arc4/test_tuple.py @@ -2,8 +2,10 @@ import pytest from _algopy_testing import arc4 +from algopy_testing import AlgopyTestContext, algopy_testing_context from algosdk import abi +from tests.artifacts.Tuples.contract import TuplesContract from tests.util import int_to_bytes _abi_string = "hello" @@ -15,6 +17,14 @@ _arc4_uint8 = arc4.UInt8(42) _arc4_bool = arc4.Bool(True) + +# New fixture +@pytest.fixture() +def context() -> typing.Generator[AlgopyTestContext, None, None]: + with algopy_testing_context() as ctx: + yield ctx + + _test_data = [ ( abi.ABIType.from_string("(uint8,bool,bool)"), @@ -279,3 +289,8 @@ def _compare_abi_and_arc4_values( assert arc4_value.native == abi_value else: assert arc4_value.bytes == int_to_bytes(abi_value, len(arc4_value.bytes)) + + +def test_tuple_return_with_primitive_type(context: AlgopyTestContext) -> None: # noqa: ARG001 + contract = TuplesContract() + contract.test_tuple_with_primitive_type() diff --git a/tests/artifacts/Tuples/__init__.py b/tests/artifacts/Tuples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/artifacts/Tuples/contract.py b/tests/artifacts/Tuples/contract.py new file mode 100644 index 0000000..1ef54a2 --- /dev/null +++ b/tests/artifacts/Tuples/contract.py @@ -0,0 +1,11 @@ +from algopy import ( + ARC4Contract, + UInt64, + arc4, +) + + +class TuplesContract(ARC4Contract, avm_version=11): + @arc4.abimethod() + def test_tuple_with_primitive_type(self) -> tuple[UInt64, bool]: + return (UInt64(0), True) diff --git a/tests/artifacts/Tuples/data/TuplesContract.approval.teal b/tests/artifacts/Tuples/data/TuplesContract.approval.teal new file mode 100644 index 0000000..5aaa50f --- /dev/null +++ b/tests/artifacts/Tuples/data/TuplesContract.approval.teal @@ -0,0 +1,42 @@ +#pragma version 11 +#pragma typetrack false + +// algopy.arc4.ARC4Contract.approval_program() -> uint64: +main: + // tests/artifacts/Tuples/contract.py:8 + // class TuplesContract(ARC4Contract, avm_version=11): + txn NumAppArgs + bz main_bare_routing@6 + pushbytes 0x7229d79a // method "test_tuple_with_primitive_type()(uint64,bool)" + txna ApplicationArgs 0 + match main_test_tuple_with_primitive_type_route@3 + +main_after_if_else@10: + // tests/artifacts/Tuples/contract.py:8 + // class TuplesContract(ARC4Contract, avm_version=11): + pushint 0 // 0 + return + +main_test_tuple_with_primitive_type_route@3: + // tests/artifacts/Tuples/contract.py:9 + // @arc4.abimethod() + txn OnCompletion + ! + assert // OnCompletion is not NoOp + txn ApplicationID + assert // can only call when not creating + pushbytes 0x151f7c75000000000000000080 + log + pushint 1 // 1 + return + +main_bare_routing@6: + // tests/artifacts/Tuples/contract.py:8 + // class TuplesContract(ARC4Contract, avm_version=11): + txn OnCompletion + bnz main_after_if_else@10 + txn ApplicationID + ! + assert // can only call when creating + pushint 1 // 1 + return diff --git a/tests/artifacts/Tuples/data/TuplesContract.arc32.json b/tests/artifacts/Tuples/data/TuplesContract.arc32.json new file mode 100644 index 0000000..0636f7a --- /dev/null +++ b/tests/artifacts/Tuples/data/TuplesContract.arc32.json @@ -0,0 +1,50 @@ +{ + "hints": { + "test_tuple_with_primitive_type()(uint64,bool)": { + "call_config": { + "no_op": "CALL" + } + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDExCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuYXBwcm92YWxfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9UdXBsZXMvY29udHJhY3QucHk6OAogICAgLy8gY2xhc3MgVHVwbGVzQ29udHJhY3QoQVJDNENvbnRyYWN0LCBhdm1fdmVyc2lvbj0xMSk6CiAgICB0eG4gTnVtQXBwQXJncwogICAgYnogbWFpbl9iYXJlX3JvdXRpbmdANgogICAgcHVzaGJ5dGVzIDB4NzIyOWQ3OWEgLy8gbWV0aG9kICJ0ZXN0X3R1cGxlX3dpdGhfcHJpbWl0aXZlX3R5cGUoKSh1aW50NjQsYm9vbCkiCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBtYWluX3Rlc3RfdHVwbGVfd2l0aF9wcmltaXRpdmVfdHlwZV9yb3V0ZUAzCgptYWluX2FmdGVyX2lmX2Vsc2VAMTA6CiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvVHVwbGVzL2NvbnRyYWN0LnB5OjgKICAgIC8vIGNsYXNzIFR1cGxlc0NvbnRyYWN0KEFSQzRDb250cmFjdCwgYXZtX3ZlcnNpb249MTEpOgogICAgcHVzaGludCAwIC8vIDAKICAgIHJldHVybgoKbWFpbl90ZXN0X3R1cGxlX3dpdGhfcHJpbWl0aXZlX3R5cGVfcm91dGVAMzoKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9UdXBsZXMvY29udHJhY3QucHk6OQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgcHVzaGJ5dGVzIDB4MTUxZjdjNzUwMDAwMDAwMDAwMDAwMDAwODAKICAgIGxvZwogICAgcHVzaGludCAxIC8vIDEKICAgIHJldHVybgoKbWFpbl9iYXJlX3JvdXRpbmdANjoKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9UdXBsZXMvY29udHJhY3QucHk6OAogICAgLy8gY2xhc3MgVHVwbGVzQ29udHJhY3QoQVJDNENvbnRyYWN0LCBhdm1fdmVyc2lvbj0xMSk6CiAgICB0eG4gT25Db21wbGV0aW9uCiAgICBibnogbWFpbl9hZnRlcl9pZl9lbHNlQDEwCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgIQogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBjcmVhdGluZwogICAgcHVzaGludCAxIC8vIDEKICAgIHJldHVybgo=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDExCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuY2xlYXJfc3RhdGVfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIHB1c2hpbnQgMSAvLyAxCiAgICByZXR1cm4K" + }, + "state": { + "global": { + "num_byte_slices": 0, + "num_uints": 0 + }, + "local": { + "num_byte_slices": 0, + "num_uints": 0 + } + }, + "schema": { + "global": { + "declared": {}, + "reserved": {} + }, + "local": { + "declared": {}, + "reserved": {} + } + }, + "contract": { + "name": "TuplesContract", + "methods": [ + { + "name": "test_tuple_with_primitive_type", + "args": [], + "readonly": false, + "returns": { + "type": "(uint64,bool)" + } + } + ], + "networks": {} + }, + "bare_call_config": { + "no_op": "CREATE" + } +} \ No newline at end of file diff --git a/tests/artifacts/Tuples/data/TuplesContract.arc56.json b/tests/artifacts/Tuples/data/TuplesContract.arc56.json new file mode 100644 index 0000000..4153bea --- /dev/null +++ b/tests/artifacts/Tuples/data/TuplesContract.arc56.json @@ -0,0 +1,102 @@ +{ + "name": "TuplesContract", + "structs": {}, + "methods": [ + { + "name": "test_tuple_with_primitive_type", + "args": [], + "returns": { + "type": "(uint64,bool)" + }, + "actions": { + "create": [], + "call": [ + "NoOp" + ] + }, + "readonly": false, + "events": [], + "recommendations": {} + } + ], + "arcs": [ + 22, + 28 + ], + "networks": {}, + "state": { + "schema": { + "global": { + "ints": 0, + "bytes": 0 + }, + "local": { + "ints": 0, + "bytes": 0 + } + }, + "keys": { + "global": {}, + "local": {}, + "box": {} + }, + "maps": { + "global": {}, + "local": {}, + "box": {} + } + }, + "bareActions": { + "create": [ + "NoOp" + ], + "call": [] + }, + "sourceInfo": { + "approval": { + "sourceInfo": [ + { + "pc": [ + 25 + ], + "errorMessage": "OnCompletion is not NoOp" + }, + { + "pc": [ + 56 + ], + "errorMessage": "can only call when creating" + }, + { + "pc": [ + 28 + ], + "errorMessage": "can only call when not creating" + } + ], + "pcOffsetMethod": "none" + }, + "clear": { + "sourceInfo": [], + "pcOffsetMethod": "none" + } + }, + "source": { + "approval": "I3ByYWdtYSB2ZXJzaW9uIDExCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuYXBwcm92YWxfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9UdXBsZXMvY29udHJhY3QucHk6OAogICAgLy8gY2xhc3MgVHVwbGVzQ29udHJhY3QoQVJDNENvbnRyYWN0LCBhdm1fdmVyc2lvbj0xMSk6CiAgICB0eG4gTnVtQXBwQXJncwogICAgYnogbWFpbl9iYXJlX3JvdXRpbmdANgogICAgcHVzaGJ5dGVzIDB4NzIyOWQ3OWEgLy8gbWV0aG9kICJ0ZXN0X3R1cGxlX3dpdGhfcHJpbWl0aXZlX3R5cGUoKSh1aW50NjQsYm9vbCkiCiAgICB0eG5hIEFwcGxpY2F0aW9uQXJncyAwCiAgICBtYXRjaCBtYWluX3Rlc3RfdHVwbGVfd2l0aF9wcmltaXRpdmVfdHlwZV9yb3V0ZUAzCgptYWluX2FmdGVyX2lmX2Vsc2VAMTA6CiAgICAvLyB0ZXN0cy9hcnRpZmFjdHMvVHVwbGVzL2NvbnRyYWN0LnB5OjgKICAgIC8vIGNsYXNzIFR1cGxlc0NvbnRyYWN0KEFSQzRDb250cmFjdCwgYXZtX3ZlcnNpb249MTEpOgogICAgcHVzaGludCAwIC8vIDAKICAgIHJldHVybgoKbWFpbl90ZXN0X3R1cGxlX3dpdGhfcHJpbWl0aXZlX3R5cGVfcm91dGVAMzoKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9UdXBsZXMvY29udHJhY3QucHk6OQogICAgLy8gQGFyYzQuYWJpbWV0aG9kKCkKICAgIHR4biBPbkNvbXBsZXRpb24KICAgICEKICAgIGFzc2VydCAvLyBPbkNvbXBsZXRpb24gaXMgbm90IE5vT3AKICAgIHR4biBBcHBsaWNhdGlvbklECiAgICBhc3NlcnQgLy8gY2FuIG9ubHkgY2FsbCB3aGVuIG5vdCBjcmVhdGluZwogICAgcHVzaGJ5dGVzIDB4MTUxZjdjNzUwMDAwMDAwMDAwMDAwMDAwODAKICAgIGxvZwogICAgcHVzaGludCAxIC8vIDEKICAgIHJldHVybgoKbWFpbl9iYXJlX3JvdXRpbmdANjoKICAgIC8vIHRlc3RzL2FydGlmYWN0cy9UdXBsZXMvY29udHJhY3QucHk6OAogICAgLy8gY2xhc3MgVHVwbGVzQ29udHJhY3QoQVJDNENvbnRyYWN0LCBhdm1fdmVyc2lvbj0xMSk6CiAgICB0eG4gT25Db21wbGV0aW9uCiAgICBibnogbWFpbl9hZnRlcl9pZl9lbHNlQDEwCiAgICB0eG4gQXBwbGljYXRpb25JRAogICAgIQogICAgYXNzZXJ0IC8vIGNhbiBvbmx5IGNhbGwgd2hlbiBjcmVhdGluZwogICAgcHVzaGludCAxIC8vIDEKICAgIHJldHVybgo=", + "clear": "I3ByYWdtYSB2ZXJzaW9uIDExCiNwcmFnbWEgdHlwZXRyYWNrIGZhbHNlCgovLyBhbGdvcHkuYXJjNC5BUkM0Q29udHJhY3QuY2xlYXJfc3RhdGVfcHJvZ3JhbSgpIC0+IHVpbnQ2NDoKbWFpbjoKICAgIHB1c2hpbnQgMSAvLyAxCiAgICByZXR1cm4K" + }, + "byteCode": { + "approval": "CzEbQQAqgARyKdeaNhoAjgEAA4EAQzEZFEQxGESADRUffHUAAAAAAAAAAICwgQFDMRlA/94xGBREgQFD", + "clear": "C4EBQw==" + }, + "compilerInfo": { + "compiler": "puya", + "compilerVersion": { + "major": 4, + "minor": 8, + "patch": 1 + } + }, + "events": [], + "templateVariables": {} +} \ No newline at end of file diff --git a/tests/artifacts/Tuples/data/TuplesContract.clear.teal b/tests/artifacts/Tuples/data/TuplesContract.clear.teal new file mode 100644 index 0000000..8843a4b --- /dev/null +++ b/tests/artifacts/Tuples/data/TuplesContract.clear.teal @@ -0,0 +1,7 @@ +#pragma version 11 +#pragma typetrack false + +// algopy.arc4.ARC4Contract.clear_state_program() -> uint64: +main: + pushint 1 // 1 + return From 49592d79b8c39efbcf6dcc4cfb5e743c32760327 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Thu, 15 May 2025 13:28:10 +0200 Subject: [PATCH 3/5] chore: add matrix to run on both 3.12 and 3.13 --- .github/workflows/ci.yaml | 33 +++++++++++++++++++++++++++------ pyproject.toml | 22 +++++++++++++++++++++- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 86c13d7..a66ccb0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,11 +37,32 @@ jobs: - name: Check wheels can be built run: hatch build - - name: Run tests (codebase) - run: hatch run tests - - - name: Run tests (examples) - run: hatch run examples:tests - - name: Check doctests run: hatch run docs:test + + test-python-matrix: + runs-on: "ubuntu-latest" + strategy: + matrix: + python-version: ["3.12", "3.13"] + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + + - name: Install hatch + run: pip install hatch + + - name: Start LocalNet + run: pipx install algokit && algokit localnet start + + - name: Run tests with Python ${{ matrix.python-version }} + run: hatch run test:ci + + - name: Run examples tests with Python ${{ matrix.python-version }} + run: hatch run examples:tests diff --git a/pyproject.toml b/pyproject.toml index 00e3463..ce83a15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ classifiers = [ "Topic :: Software Development :: Testing", "Programming Language :: Python", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dependencies = [ # ========================================================================== @@ -44,7 +45,7 @@ allow-direct-references = true packages = ["src/algopy", 'src/algopy_testing', 'src/_algopy_testing'] [[tool.hatch.envs.all.matrix]] -python = ["3.12"] +python = ["3.12", "3.13"] # default dev environment [tool.hatch.envs.default] @@ -124,6 +125,25 @@ dependencies = [ [tool.hatch.envs.cicd.scripts] clean_dist = "rm -rf dist" +# Testing environment with matrix +[tool.hatch.envs.test] +dependencies = [ + "pytest>=7.4", + "pytest-mock>=3.10.0", + "pytest-xdist[psutil]>=3.3", + "pytest-cov>=4.1.0", + "py-algorand-sdk>=2.4.0", + "algokit-utils>=3.0.0", + "puyapy>=3.0", +] + +[tool.hatch.envs.test.scripts] +run = "pytest --cov=src --cov-report=xml {args}" +ci = "pytest --cov=src --cov-report=xml --cov-report=term" + +[[tool.hatch.envs.test.matrix]] +python = ["3.12", "3.13"] + # docs environment [tool.hatch.envs.docs] path = ".venv.docs" From fd24e5fb82d6c9585e7bf6eb57ae3e8e1b8f6e09 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Thu, 15 May 2025 13:52:00 +0200 Subject: [PATCH 4/5] chore: tweak ci --- .github/workflows/ci.yaml | 13 +++++++++++-- pyproject.toml | 8 ++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a66ccb0..ae275f0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -62,7 +62,16 @@ jobs: run: pipx install algokit && algokit localnet start - name: Run tests with Python ${{ matrix.python-version }} - run: hatch run test:ci + run: hatch run test.py${{ matrix.python-version }}:ci - name: Run examples tests with Python ${{ matrix.python-version }} - run: hatch run examples:tests + run: hatch run examples.py${{ matrix.python-version }}:tests + + - name: Upload coverage artifacts + uses: actions/upload-artifact@v4 + if: ${{ matrix.python-version == '3.13' }} + with: + name: coverage-reports + path: | + ./coverage.xml + retention-days: 14 diff --git a/pyproject.toml b/pyproject.toml index ce83a15..5d266ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,9 +44,6 @@ allow-direct-references = true [tool.hatch.build.targets.wheel] packages = ["src/algopy", 'src/algopy_testing', 'src/_algopy_testing'] -[[tool.hatch.envs.all.matrix]] -python = ["3.12", "3.13"] - # default dev environment [tool.hatch.envs.default] type = "virtual" @@ -122,6 +119,7 @@ path = ".venv.cicd" dependencies = [ "python-semantic-release>=9.8.5", ] + [tool.hatch.envs.cicd.scripts] clean_dist = "rm -rf dist" @@ -179,7 +177,6 @@ dev = "hatch run docs:test && sphinx-autobuild docs docs/_build" [tool.hatch.envs.examples] type = "virtual" path = ".venv.examples" -python = "3.12" dev-mode = true skip-install = false post-install-commands = [ @@ -208,6 +205,9 @@ check = [ "hatch run mypy examples", ] +[[tool.hatch.envs.examples.matrix]] +python = ["3.12", "3.13"] + # tool configurations [tool.black] line-length = 99 From 06b2a7fde32598c5315ef923e1de2a334274a44d Mon Sep 17 00:00:00 2001 From: Daniel McGregor Date: Fri, 16 May 2025 14:44:31 +0800 Subject: [PATCH 5/5] refactor: simplify get_native_to_arc4_serializer implementation --- src/_algopy_testing/serialize.py | 165 ++++++++++++++----------------- 1 file changed, 73 insertions(+), 92 deletions(-) diff --git a/src/_algopy_testing/serialize.py b/src/_algopy_testing/serialize.py index 73ba6cf..84b73d0 100644 --- a/src/_algopy_testing/serialize.py +++ b/src/_algopy_testing/serialize.py @@ -1,4 +1,5 @@ import dataclasses +import functools import inspect import typing from collections.abc import Callable, Sequence @@ -15,7 +16,6 @@ @dataclasses.dataclass(frozen=True) class _Serializer(typing.Generic[_T, _U]): - native_type: type[_T] arc4_type: type[_U] native_to_arc4: Callable[[_T], _U] arc4_to_native: Callable[[_U], _T] @@ -25,111 +25,92 @@ def identity(i: _T) -> _T: return i -def get_native_to_arc4_serializer(typ: type[_T]) -> _Serializer: # type: ignore[type-arg] # noqa: PLR0911, PLR0912 +def get_native_to_arc4_serializer(typ: type) -> _Serializer[typing.Any, typing.Any]: from _algopy_testing import arc4 - from _algopy_testing.models import Account - from _algopy_testing.primitives import BigUInt, Bytes, ImmutableArray, String + from _algopy_testing.primitives import ImmutableArray from _algopy_testing.protocols import UInt64Backed - origin_typ = typing.get_origin(typ) - - if inspect.isclass(typ) and issubclass(typ, arc4._ABIEncoded): - return _Serializer( - native_type=typ, arc4_type=typ, native_to_arc4=identity, arc4_to_native=identity - ) - # For types that are expected to be simple classes for these specific checks - if inspect.isclass(typ): - if issubclass(typ, bool): - return _Serializer( - native_type=typ, - arc4_type=arc4.Bool, - native_to_arc4=arc4.Bool, - arc4_to_native=lambda n: n.native, - ) + origin_type = typing.get_origin(typ) + if origin_type is tuple: + return _get_tuple_serializer(typing.get_args(typ)) + elif isinstance(typ, type): + if issubclass(typ, arc4._ABIEncoded): + return _Serializer(arc4_type=typ, native_to_arc4=identity, arc4_to_native=identity) + for native_type, simple_arc4_type in _simple_native_to_arc4_type_map().items(): + if issubclass(typ, native_type): + return _Serializer( + arc4_type=simple_arc4_type, + native_to_arc4=simple_arc4_type, + arc4_to_native=lambda n: n.native, + ) if issubclass(typ, UInt64Backed): return _Serializer( - native_type=typ, arc4_type=arc4.UInt64, native_to_arc4=lambda n: arc4.UInt64(n.int_), arc4_to_native=lambda a: typ.from_int(a.native), ) - if issubclass(typ, BigUInt): - return _Serializer( - native_type=typ, - arc4_type=arc4.UInt512, - native_to_arc4=arc4.UInt512, - arc4_to_native=lambda a: a.native, - ) - if issubclass(typ, Account): - return _Serializer( - native_type=typ, - arc4_type=arc4.Address, - native_to_arc4=arc4.Address, - arc4_to_native=lambda a: a.native, - ) - if issubclass(typ, UInt64): - return _Serializer( - native_type=typ, - arc4_type=arc4.UInt64, - native_to_arc4=arc4.UInt64, - arc4_to_native=lambda a: a.native, - ) - if issubclass(typ, Bytes): - return _Serializer( - native_type=typ, - arc4_type=arc4.DynamicBytes, - native_to_arc4=arc4.DynamicBytes, - arc4_to_native=lambda a: a.native, - ) - if issubclass(typ, String): + if typing.NamedTuple in getattr(typ, "__orig_bases__", []): + tuple_fields = tuple(inspect.get_annotations(typ).values()) + if any(isinstance(f, str) for f in tuple_fields): + raise TypeError("string annotations in typing.NamedTuple fields are not supported") + return _get_tuple_serializer(tuple_fields) + if issubclass(typ, ImmutableArray): + native_element_type = typ._element_type + element_serializer = get_native_to_arc4_serializer(native_element_type) + arc4_element_type = element_serializer.arc4_type + arc4_type = arc4.DynamicArray[arc4_element_type] # type: ignore[valid-type] return _Serializer( - native_type=typ, - arc4_type=arc4.String, - native_to_arc4=arc4.String, - arc4_to_native=lambda a: a.native, + arc4_type=arc4_type, + native_to_arc4=lambda arr: arc4_type( + *(element_serializer.native_to_arc4(e) for e in arr) + ), + arc4_to_native=lambda arr: typ( + *(element_serializer.arc4_to_native(e) for e in arr) + ), ) - - if origin_typ is tuple or (inspect.isclass(typ) and issubclass(typ, tuple)): - if typing.NamedTuple in getattr(typ, "__orig_bases__", []): - tuple_fields: Sequence[type] = list(inspect.get_annotations(typ).values()) - else: - tuple_fields = typing.get_args(typ) - serializers = [get_native_to_arc4_serializer(i) for i in tuple_fields] - - def _items_to_arc4(items: Sequence[object]) -> tuple[object, ...]: - result = [] - for item, serializer in zip(items, serializers, strict=True): - result.append(serializer.native_to_arc4(item)) - return tuple(result) - - def _items_to_native(items: Sequence[object]) -> tuple[object, ...]: - result = [] - for item, serializer in zip(items, serializers, strict=True): - result.append(serializer.arc4_to_native(item)) - return tuple(result) - - return _Serializer( - native_type=typ, - arc4_type=arc4.Tuple[*(s.arc4_type for s in serializers)], # type: ignore[misc] - native_to_arc4=lambda t: arc4.Tuple(_items_to_arc4(t)), - arc4_to_native=lambda t: _items_to_native(t), - ) - if issubclass(typ, ImmutableArray): - native_element_type = typ._element_type - element_serializer = get_native_to_arc4_serializer(native_element_type) - arc4_element_type = element_serializer.arc4_type - arc4_type = arc4.DynamicArray[arc4_element_type] # type: ignore[valid-type] - return _Serializer( - native_type=typ, - arc4_type=arc4_type, - native_to_arc4=lambda arr: arc4_type( - *(element_serializer.native_to_arc4(e) for e in arr) - ), - arc4_to_native=lambda arr: typ(*(element_serializer.arc4_to_native(e) for e in arr)), - ) raise TypeError(f"unserializable type: {typ}") +@functools.cache +def _simple_native_to_arc4_type_map() -> dict[type, type]: + from _algopy_testing import arc4 + from _algopy_testing.models import Account + from _algopy_testing.primitives import BigUInt, Bytes, String + + return { + bool: arc4.Bool, + UInt64: arc4.UInt64, + BigUInt: arc4.UInt512, + Account: arc4.Address, + Bytes: arc4.DynamicBytes, + String: arc4.String, + } + + +def _get_tuple_serializer(item_types: tuple[type, ...]) -> _Serializer[typing.Any, typing.Any]: + from _algopy_testing import arc4 + + serializers = [get_native_to_arc4_serializer(i) for i in item_types] + + def _items_to_arc4(items: Sequence[object]) -> tuple[object, ...]: + result = [] + for item, serializer in zip(items, serializers, strict=True): + result.append(serializer.native_to_arc4(item)) + return tuple(result) + + def _items_to_native(items: Sequence[object]) -> tuple[object, ...]: + result = [] + for item, serializer in zip(items, serializers, strict=True): + result.append(serializer.arc4_to_native(item)) + return tuple(result) + + return _Serializer( + arc4_type=arc4.Tuple[*(s.arc4_type for s in serializers)], # type: ignore[misc] + native_to_arc4=lambda t: arc4.Tuple(_items_to_arc4(t)), + arc4_to_native=lambda t: _items_to_native(t), + ) + + def serialize_to_bytes(value: object) -> bytes: return native_to_arc4(value).bytes.value