Skip to content

Commit 9a5e224

Browse files
authored
Merge pull request #40 from algorandfoundation/feat/tuple-storage
feat: support storing tuples in state
2 parents 2b20087 + e987789 commit 9a5e224

File tree

66 files changed

+12843
-807
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+12843
-807
lines changed

src/_algopy_testing/arc4.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,9 @@ def __repr__(self) -> str:
742742

743743

744744
class _DynamicArrayTypeInfo(_TypeInfo):
745-
def __init__(self, item_type: _TypeInfo):
745+
_subclass_type: Callable[[], type] | None
746+
747+
def __init__(self, item_type: _TypeInfo) -> None:
746748
self.item_type = item_type
747749

748750
@property
@@ -888,9 +890,20 @@ def __repr__(self) -> str:
888890
return f"{_arc4_type_repr(type(self))}({', '.join(items)})"
889891

890892

893+
class _DynamicBytesTypeInfo(_DynamicArrayTypeInfo):
894+
def __init__(self) -> None:
895+
super().__init__(Byte._type_info)
896+
897+
@property
898+
def typ(self) -> type:
899+
return DynamicBytes
900+
901+
891902
class DynamicBytes(DynamicArray[Byte]):
892903
"""A variable sized array of bytes."""
893904

905+
_type_info: _DynamicBytesTypeInfo = _DynamicBytesTypeInfo()
906+
894907
@typing.overload
895908
def __init__(self, *values: Byte | UInt8 | int): ...
896909

@@ -996,6 +1009,12 @@ def __init__(self, _items: tuple[typing.Unpack[_TTuple]] = (), /): # type: igno
9961009
)
9971010
self._value = _encode(items)
9981011

1012+
def __bool__(self) -> bool:
1013+
try:
1014+
return bool(self.native)
1015+
except ValueError:
1016+
return False
1017+
9991018
def __len__(self) -> int:
10001019
return len(self.native)
10011020

@@ -1103,6 +1122,8 @@ def _update_backing_value(self) -> None:
11031122
def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self:
11041123
tuple_type = _tuple_type_from_struct(cls)
11051124
tuple_value = tuple_type.from_bytes(value)
1125+
if not tuple_value:
1126+
return typing.cast(typing.Self, tuple_value)
11061127
return cls(*tuple_value.native)
11071128

11081129
@property

src/_algopy_testing/models/contract.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,12 @@ def _get_state_totals(contract: Contract, cls_state_totals: StateTotals) -> _Sta
201201

202202
global_bytes = global_uints = local_bytes = local_uints = 0
203203
for type_ in get_global_states(contract).values():
204-
if issubclass(type_, UInt64 | UInt64Backed | bool):
204+
if isinstance(type_, type) and issubclass(type_, UInt64 | UInt64Backed | bool):
205205
global_uints += 1
206206
else:
207207
global_bytes += 1
208208
for type_ in get_local_states(contract).values():
209-
if issubclass(type_, UInt64 | UInt64Backed | bool):
209+
if isinstance(type_, type) and issubclass(type_, UInt64 | UInt64Backed | bool):
210210
local_uints += 1
211211
else:
212212
local_bytes += 1

src/_algopy_testing/serialize.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,16 @@ def native_to_arc4(value: object) -> "_ABIEncoded":
136136
return arc4_value
137137

138138

139+
def compare_type(value_type: type, typ: type) -> bool:
140+
if typing.NamedTuple in getattr(typ, "__orig_bases__", []):
141+
tuple_fields: Sequence[type] = list(inspect.get_annotations(typ).values())
142+
typ = tuple[*tuple_fields] # type: ignore[valid-type]
143+
return value_type == typ
144+
145+
139146
def deserialize_from_bytes(typ: type[_T], bites: bytes) -> _T:
140147
serializer = get_native_to_arc4_serializer(typ)
141148
arc4_value = serializer.arc4_type.from_bytes(bites)
142149
native_value = serializer.arc4_to_native(arc4_value)
143-
assert isinstance(native_value, typ)
144-
return native_value
150+
assert compare_type(type_of(native_value), typ) or isinstance(native_value, typ)
151+
return native_value # type: ignore[no-any-return]

src/_algopy_testing/state/box.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,9 +322,9 @@ def get(self, key: _TKey, *, default: _TValue) -> _TValue:
322322
def maybe(self, key: _TKey) -> tuple[_TValue, bool]:
323323
key_bytes = self._full_key(key)
324324
box_exists = lazy_context.ledger.box_exists(self.app_id, key_bytes)
325-
if not box_exists:
326-
return self._value_type(), False
327-
box_content_bytes = lazy_context.ledger.get_box(self.app_id, key_bytes)
325+
box_content_bytes = (
326+
b"" if not box_exists else lazy_context.ledger.get_box(self.app_id, key_bytes)
327+
)
328328
box_content = cast_from_bytes(self._value_type, box_content_bytes)
329329
return box_content, box_exists
330330

src/_algopy_testing/state/global_state.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from _algopy_testing.context_helpers import lazy_context
77
from _algopy_testing.mutable import set_attr_on_mutate
88
from _algopy_testing.primitives import Bytes, String
9+
from _algopy_testing.serialize import type_of
910
from _algopy_testing.state.utils import deserialize, serialize
1011

1112
if typing.TYPE_CHECKING:
@@ -49,10 +50,10 @@ def __init__(
4950
self._key: Bytes | None = None
5051
self._pending_value: _T | None = None
5152

52-
if isinstance(type_or_value, type):
53-
self.type_: type[_T] = type_or_value
53+
if isinstance(type_or_value, type) or isinstance(typing.get_origin(type_or_value), type):
54+
self.type_: type[_T] = typing.cast(type[_T], type_or_value)
5455
else:
55-
self.type_ = type(type_or_value)
56+
self.type_ = type_of(type_or_value)
5657
self._pending_value = type_or_value
5758

5859
self.set_key(key)
@@ -119,13 +120,11 @@ def value(self) -> None:
119120
def __bool__(self) -> bool:
120121
return self._key is not None or self._pending_value is not None
121122

122-
def get(self, default: _T | None = None) -> _T:
123+
def get(self, default: _T) -> _T:
123124
try:
124125
return self.value
125126
except ValueError:
126-
if default is not None:
127-
return default
128-
return self.type_()
127+
return default
129128

130129
def maybe(self) -> tuple[_T | None, bool]:
131130
try:

src/_algopy_testing/state/local_state.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,19 @@ def __contains__(self, key: algopy.Account | algopy.UInt64 | int) -> bool:
7070
return False
7171
return True
7272

73-
def get(self, key: algopy.Account | algopy.UInt64 | int, default: _T | None = None) -> _T:
73+
def get(self, key: algopy.Account | algopy.UInt64 | int, default: _T) -> _T:
7474
account = _get_account(key)
7575
try:
7676
return self[account]
7777
except KeyError:
78-
return default if default is not None else self.type_()
78+
return default
7979

8080
def maybe(self, key: algopy.Account | algopy.UInt64 | int) -> tuple[_T, bool]:
8181
account = _get_account(key)
8282
try:
8383
return self[account], True
8484
except KeyError:
85-
return self.type_(), False
85+
return typing.cast(_T, None), False
8686

8787

8888
# TODO: make a util function along with one used by ops

src/_algopy_testing/state/utils.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
from _algopy_testing.primitives.bytes import Bytes
66
from _algopy_testing.primitives.uint64 import UInt64
77
from _algopy_testing.protocols import BytesBacked, Serializable, UInt64Backed
8+
from _algopy_testing.serialize import (
9+
deserialize_from_bytes,
10+
serialize_to_bytes,
11+
)
812

913
_TValue = typing.TypeVar("_TValue")
1014
SerializableValue = int | bytes
@@ -21,12 +25,16 @@ def serialize(value: _TValue) -> SerializableValue:
2125
return value.bytes.value
2226
elif isinstance(value, Serializable):
2327
return value.serialize()
28+
elif isinstance(value, tuple):
29+
return serialize_to_bytes(value)
2430
else:
2531
raise TypeError(f"Unsupported type: {type(value)}")
2632

2733

2834
def deserialize(typ: type[_TValue], value: SerializableValue) -> _TValue:
29-
if issubclass(typ, bool):
35+
if (typing.get_origin(typ) is tuple or issubclass(typ, tuple)) and isinstance(value, bytes):
36+
return () if not value else deserialize_from_bytes(typ, value) # type: ignore[return-value]
37+
elif issubclass(typ, bool):
3038
return value != 0 # type: ignore[return-value]
3139
elif issubclass(typ, UInt64 | Bytes):
3240
return typ(value) # type: ignore[arg-type, return-value]
@@ -55,7 +63,7 @@ def cast_from_bytes(typ: type[_TValue], value: bytes) -> _TValue:
5563
"""
5664
from _algopy_testing.utils import as_int64
5765

58-
if issubclass(typ, bool | UInt64Backed | UInt64):
66+
if isinstance(typ, type) and issubclass(typ, bool | UInt64Backed | UInt64):
5967
if len(value) > 8:
6068
raise ValueError("uint64 value too big")
6169
serialized: SerializableValue = int.from_bytes(value)

0 commit comments

Comments
 (0)