Skip to content

Commit 98e6816

Browse files
committed
feat: add stub implementation of algopy.size_of function for calculating static storage size of types
1 parent ff2c3ec commit 98e6816

File tree

4 files changed

+129
-2
lines changed

4 files changed

+129
-2
lines changed

src/_algopy_testing/arc4.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1256,10 +1256,20 @@ def _compress_multiple_bool(value_list: list[Bool]) -> int:
12561256
return result
12571257

12581258

1259+
def get_max_bytes_static_len(type_info: _TypeInfo) -> int | None:
1260+
return _get_max_bytes_len_impl(type_info, static_size_only=True)
1261+
1262+
12591263
def _get_max_bytes_len(type_info: _TypeInfo) -> int:
1264+
return _get_max_bytes_len_impl(type_info) or 0
1265+
1266+
1267+
def _get_max_bytes_len_impl(type_info: _TypeInfo, *, static_size_only: bool = False) -> int | None:
12601268
size = 0
12611269
if isinstance(type_info, _DynamicArrayTypeInfo):
12621270
size += _ABI_LENGTH_SIZE
1271+
if static_size_only:
1272+
return None
12631273
elif isinstance(type_info, _TupleTypeInfo | _StructTypeInfo | _StaticArrayTypeInfo):
12641274
i = 0
12651275
if isinstance(type_info, _TupleTypeInfo | _StructTypeInfo):
@@ -1276,12 +1286,18 @@ def _get_max_bytes_len(type_info: _TypeInfo) -> int:
12761286
if bool_num % 8 != 0:
12771287
size += 1
12781288
else:
1279-
child_byte_size = _get_max_bytes_len(child_types[i])
1289+
child_byte_size = _get_max_bytes_len_impl(
1290+
child_types[i], static_size_only=static_size_only
1291+
)
1292+
if child_byte_size is None:
1293+
return None
12801294
size += child_byte_size
12811295
i += 1
12821296
elif isinstance(type_info, _UIntTypeInfo):
12831297
size = type_info.max_bytes_len
12841298

1299+
elif static_size_only:
1300+
return None
12851301
return size
12861302

12871303

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from _algopy_testing.utilities.budget import OpUpFeeSource, ensure_budget
22
from _algopy_testing.utilities.log import log
3+
from _algopy_testing.utilities.size_of import size_of
34

4-
__all__ = ["OpUpFeeSource", "ensure_budget", "log"]
5+
__all__ = ["OpUpFeeSource", "ensure_budget", "log", "size_of"]
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import inspect
2+
import typing
3+
4+
from _algopy_testing.primitives.uint64 import UInt64
5+
6+
7+
def size_of(typ: type | object, /) -> UInt64:
8+
from _algopy_testing.arc4 import _TupleTypeInfo, _TypeInfo, get_max_bytes_static_len
9+
from _algopy_testing.models.account import Account
10+
from _algopy_testing.models.application import Application
11+
from _algopy_testing.models.asset import Asset
12+
from _algopy_testing.serialize import get_native_to_arc4_serializer
13+
14+
# Check for types with _type_info attribute
15+
if hasattr(typ, "_type_info") or isinstance(typ, _TypeInfo):
16+
size = get_max_bytes_static_len(typ if isinstance(typ, _TypeInfo) else typ._type_info)
17+
if size is not None:
18+
return UInt64(size)
19+
else:
20+
raise ValueError(f"{typ} is dynamically sized")
21+
22+
# Fixed-size types
23+
fixed_sizes = {UInt64: 8, Account: 32, Application: 8, Asset: 8, bool: 8}
24+
25+
# Check if type matches any fixed-size type
26+
for cls, size in fixed_sizes.items():
27+
if isinstance(typ, cls) or typ == cls:
28+
return UInt64(size)
29+
30+
# Handle tuple types
31+
is_tuple = (isinstance(typ, type) and issubclass(typ, tuple)) or typing.get_origin(
32+
typ
33+
) is tuple
34+
if is_tuple:
35+
if typing.NamedTuple in getattr(typ, "__orig_bases__", []):
36+
tuple_fields = list(
37+
inspect.get_annotations(typ if isinstance(typ, type) else typ.__class__).values()
38+
)
39+
else:
40+
tuple_fields = list(typing.get_args(typ))
41+
42+
type_infos = [get_native_to_arc4_serializer(f).arc4_type._type_info for f in tuple_fields]
43+
return size_of(_TupleTypeInfo(type_infos))
44+
45+
raise ValueError(f"{typ} is dynamically sized")

tests/utilities/test_size_of.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import re
2+
import typing
3+
4+
import pytest
5+
from _algopy_testing.utilities.size_of import size_of
6+
from algopy import Account, Application, Asset, Bytes, String, UInt64, arc4
7+
8+
9+
class Swapped(arc4.Struct):
10+
a: arc4.UInt64
11+
b: arc4.Bool
12+
c: arc4.Tuple[arc4.UInt64, arc4.Bool, arc4.Bool]
13+
d: arc4.StaticArray[arc4.Bool, typing.Literal[10]]
14+
e: arc4.Tuple[arc4.UInt64, arc4.StaticArray[arc4.UInt64, typing.Literal[3]]]
15+
16+
17+
class WhatsMySize(typing.NamedTuple):
18+
foo: UInt64
19+
bar: bool
20+
baz: Swapped
21+
22+
23+
class MyTuple(typing.NamedTuple):
24+
foo: UInt64
25+
bar: bool
26+
baz: bool
27+
28+
29+
class MyDynamicSizedTuple(typing.NamedTuple):
30+
foo: UInt64
31+
bar: String
32+
33+
34+
def test_size_of() -> None:
35+
x = arc4.UInt64(0)
36+
assert size_of(x) == 8
37+
assert size_of(arc4.UInt64) == 8
38+
assert size_of(UInt64) == 8
39+
assert size_of(arc4.Address) == 32
40+
assert size_of(Account) == 32
41+
assert size_of(Application) == 8
42+
assert size_of(Asset) == 8
43+
assert size_of(bool) == 8
44+
assert size_of(tuple[bool]) == 1
45+
assert size_of(tuple[bool, bool, bool, bool, bool, bool, bool, bool]) == 1
46+
assert size_of(tuple[bool, bool, bool, bool, bool, bool, bool, bool, bool]) == 2
47+
assert size_of(tuple[arc4.UInt64, UInt64, bool, arc4.Bool]) == 17
48+
assert size_of(arc4.Tuple[arc4.UInt64, arc4.Bool, arc4.Bool] == 9)
49+
assert size_of(MyTuple) == 9
50+
assert size_of(WhatsMySize) == 61
51+
assert size_of(arc4.StaticArray[arc4.Byte, typing.Literal[7]]) == 7
52+
assert size_of(arc4.StaticArray(arc4.Byte(), arc4.Byte())) == 2
53+
assert size_of(Swapped) == 52
54+
55+
with pytest.raises(ValueError, match=re.compile("is dynamically sized")):
56+
size_of(arc4.StaticArray[arc4.DynamicBytes, typing.Literal[7]])
57+
58+
with pytest.raises(ValueError, match=re.compile("is dynamically sized")):
59+
size_of(tuple[arc4.DynamicBytes, Bytes])
60+
61+
with pytest.raises(ValueError, match=re.compile("is dynamically sized")):
62+
size_of(arc4.Tuple[arc4.UInt64, arc4.String])
63+
64+
with pytest.raises(ValueError, match=re.compile("is dynamically sized")):
65+
size_of(MyDynamicSizedTuple)

0 commit comments

Comments
 (0)