Skip to content

fix: fixes type checking of tuples with primitive types #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 16, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,41 @@ 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.py${{ matrix.python-version }}:ci

- name: Run examples tests with Python ${{ matrix.python-version }}
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
28 changes: 24 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ classifiers = [
"Topic :: Software Development :: Testing",
"Programming Language :: Python",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
# ==========================================================================
Expand All @@ -43,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"]

# default dev environment
[tool.hatch.envs.default]
type = "virtual"
Expand Down Expand Up @@ -121,9 +119,29 @@ path = ".venv.cicd"
dependencies = [
"python-semantic-release>=9.8.5",
]

[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"
Expand Down Expand Up @@ -159,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 = [
Expand Down Expand Up @@ -188,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
Expand Down
109 changes: 57 additions & 52 deletions src/_algopy_testing/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I think this should be declared closer to the usage or perhaps even inlined as it is used in only one spot.


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)):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also add inspect.isclass(typ) clause to the next if statement on line 117 just to be safe?

if typing.NamedTuple in getattr(typ, "__orig_bases__", []):
tuple_fields: Sequence[type] = list(inspect.get_annotations(typ).values())
else:
Expand Down
15 changes: 15 additions & 0 deletions tests/arc4/test_tuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)"),
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried reverting the fix and running this test and it still passed, so I think this isn't fully capturing the issue.

Was the issue related to tuples in storage? As support for this was only recently added to Puya. @boblat is currently working on adding support for that to this library

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daniel-makerx The error is unique to versions of python starting from 3.13 and higher python/cpython#101162

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a ci to run in matrix, here is a run from temporary commit reverting original code back you can see several tests failing due to new behaviour in 3.13 https://github.com/algorandfoundation/algorand-python-testing/actions/runs/15044342246/job/42283130835?pr=39

contract = TuplesContract()
contract.test_tuple_with_primitive_type()
Empty file.
11 changes: 11 additions & 0 deletions tests/artifacts/Tuples/contract.py
Original file line number Diff line number Diff line change
@@ -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)
42 changes: 42 additions & 0 deletions tests/artifacts/Tuples/data/TuplesContract.approval.teal
Original file line number Diff line number Diff line change
@@ -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
50 changes: 50 additions & 0 deletions tests/artifacts/Tuples/data/TuplesContract.arc32.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Loading