Skip to content

Commit 8dba711

Browse files
committed
feat: add stubs for box create function
1 parent 732da43 commit 8dba711

File tree

4 files changed

+205
-21
lines changed

4 files changed

+205
-21
lines changed

src/_algopy_testing/state/box.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from __future__ import annotations
22

33
import typing
4+
import warnings
45

56
import _algopy_testing
67
from _algopy_testing.constants import MAX_BOX_SIZE
78
from _algopy_testing.context_helpers import lazy_context
89
from _algopy_testing.mutable import set_attr_on_mutate, set_item_on_mutate
910
from _algopy_testing.state.utils import cast_from_bytes, cast_to_bytes
10-
from _algopy_testing.utils import as_bytes, as_string
11+
from _algopy_testing.utils import as_bytes, as_int64, as_string, get_static_size_of
1112

1213
_TKey = typing.TypeVar("_TKey")
1314
_TValue = typing.TypeVar("_TValue")
@@ -38,6 +39,45 @@ def __init__(
3839
def __bool__(self) -> bool:
3940
return lazy_context.ledger.box_exists(self.app_id, self.key)
4041

42+
def create(self, *, size: algopy.UInt64 | int | None = None) -> bool:
43+
missing_size_err_msg = (
44+
f"{self._type} does not have a fixed byte size. Please specify a size argument"
45+
)
46+
47+
size_arg = None if size is None else as_int64(size)
48+
content_type_size = get_static_size_of(self._type)
49+
50+
if content_type_size is None and size_arg is None:
51+
raise ValueError(missing_size_err_msg)
52+
if content_type_size is not None and size_arg is not None:
53+
if size_arg < content_type_size:
54+
warnings.warn(
55+
f"Box size should not be less than {content_type_size}",
56+
stacklevel=2,
57+
)
58+
59+
if size_arg > content_type_size:
60+
warnings.warn(
61+
f"Size is set to {size_arg} but {self._type} has a fixed size of {content_type_size}", # noqa: E501
62+
stacklevel=2,
63+
)
64+
65+
size_int = size_arg if size_arg is not None else content_type_size
66+
assert size_int is not None, missing_size_err_msg
67+
68+
if size_int > MAX_BOX_SIZE:
69+
raise ValueError(f"Box size cannot exceed {MAX_BOX_SIZE}")
70+
71+
box_exists = lazy_context.ledger.box_exists(self.app_id, self.key)
72+
if box_exists:
73+
if self.length == size_int:
74+
return False
75+
else:
76+
raise ValueError(f"Box already exists with a different size: {self.length}")
77+
78+
lazy_context.ledger.set_box(self.app_id, self.key, b"\x00" * size_int)
79+
return True
80+
4181
@property
4282
def key(self) -> algopy.Bytes:
4383
if not self._key:
Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,9 @@
1-
import types
2-
31
from _algopy_testing.primitives.uint64 import UInt64
2+
from _algopy_testing.utils import get_static_size_of
43

54

65
def size_of(typ: type | object, /) -> UInt64:
7-
from _algopy_testing.arc4 import get_max_bytes_static_len
8-
from _algopy_testing.serialize import get_native_to_arc4_serializer
9-
10-
if isinstance(typ, types.GenericAlias):
11-
pass
12-
elif not isinstance(typ, type):
13-
typ = type(typ)
14-
15-
if typ is bool: # treat bool on its own as a uint64
16-
typ = UInt64
17-
serializer = get_native_to_arc4_serializer(typ) # type: ignore[arg-type]
18-
type_info = serializer.arc4_type._type_info
19-
size = get_max_bytes_static_len(type_info)
6+
size = get_static_size_of(typ)
207
if size is None:
218
raise ValueError(f"{typ} is dynamically sized")
229
return UInt64(size)

src/_algopy_testing/utils.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import secrets
4+
import types
45
import typing
56
from typing import TYPE_CHECKING
67

@@ -22,8 +23,6 @@
2223
)
2324

2425
if TYPE_CHECKING:
25-
import types
26-
2726
import algopy
2827

2928
from _algopy_testing.op.global_values import GlobalFields
@@ -195,3 +194,22 @@ def raise_mocked_function_error(func_name: str) -> typing.Never:
195194
f"{func_name!r} is not available in test context. "
196195
"Mock using your preferred testing framework."
197196
)
197+
198+
199+
def get_static_size_of(typ: type | object, /) -> int | None:
200+
from _algopy_testing import UInt64
201+
from _algopy_testing.arc4 import get_max_bytes_static_len
202+
from _algopy_testing.serialize import get_native_to_arc4_serializer
203+
204+
if isinstance(typ, types.GenericAlias):
205+
pass
206+
elif not isinstance(typ, type):
207+
typ = type(typ)
208+
209+
if typ is bool: # treat bool on its own as a uint64
210+
typ = UInt64
211+
serializer = get_native_to_arc4_serializer(typ) # type: ignore[arg-type]
212+
type_info = serializer.arc4_type._type_info
213+
size = get_max_bytes_static_len(type_info)
214+
215+
return size

tests/models/test_box.py

Lines changed: 142 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,48 @@
1+
import re
12
import typing
23
from collections.abc import Generator
34

4-
import algopy
55
import pytest
6+
7+
import algopy
68
from _algopy_testing import algopy_testing_context, arc4
79
from _algopy_testing.context import AlgopyTestContext
10+
from _algopy_testing.models.account import Account
11+
from _algopy_testing.models.application import Application
12+
from _algopy_testing.models.asset import Asset
813
from _algopy_testing.op.pure import itob
914
from _algopy_testing.primitives.biguint import BigUInt
1015
from _algopy_testing.primitives.bytes import Bytes
1116
from _algopy_testing.primitives.string import String
1217
from _algopy_testing.primitives.uint64 import UInt64
1318
from _algopy_testing.state.box import Box
19+
from _algopy_testing.state.utils import cast_to_bytes
1420
from _algopy_testing.utils import as_bytes, as_string
15-
1621
from tests.artifacts.BoxContract.contract import BoxContract
1722

1823
BOX_NOT_CREATED_ERROR = "Box has not been created"
1924

2025

26+
class Swapped(arc4.Struct):
27+
b: arc4.UInt64
28+
c: arc4.Bool
29+
d: arc4.Address
30+
31+
32+
# TODO: add tests for tuple and namedtuple once they are supported
33+
# class MyStruct(typing.NamedTuple):
34+
# a: UInt64
35+
# b: bool
36+
# c: arc4.Bool
37+
# d: arc4.UInt64
38+
39+
2140
class ATestContract(algopy.Contract):
2241
def __init__(self) -> None:
2342
self.uint_64_box = algopy.Box(algopy.UInt64)
2443

2544

26-
@pytest.fixture()
45+
@pytest.fixture
2746
def context() -> Generator[AlgopyTestContext, None, None]:
2847
with algopy_testing_context() as ctx: # noqa: SIM117
2948
with ctx.txn.create_group([ctx.any.txn.application_call()]):
@@ -68,6 +87,126 @@ def test_init_with_key(
6887
_ = box.length
6988

7089

90+
@pytest.mark.parametrize(
91+
("value_type", "expected_size"),
92+
[
93+
(arc4.UInt64, 8),
94+
(UInt64, 8),
95+
(arc4.Address, 32),
96+
(Account, 32),
97+
(Application, 8),
98+
(Asset, 8),
99+
(bool, 8),
100+
(arc4.StaticArray[arc4.Byte, typing.Literal[7]], 7),
101+
(Swapped, 41),
102+
# TODO: add tests for tuple and namedtuple once they are supported
103+
# (tuple[arc4.UInt64, arc4.Bool, arc4.Address], 41),
104+
# (MyStruct, 9),
105+
],
106+
)
107+
def test_create_for_static_value_type(
108+
context: AlgopyTestContext, # noqa: ARG001
109+
value_type: type,
110+
expected_size: int,
111+
) -> None:
112+
key = b"test_key"
113+
box = Box(value_type, key=key) # type: ignore[var-annotated]
114+
assert not box
115+
116+
box.create()
117+
assert box
118+
119+
op_box_content, op_box_exists = algopy.op.Box.get(key)
120+
assert op_box_exists
121+
assert op_box_content == b"\x00" * expected_size
122+
123+
box_content, box_exists = box.maybe()
124+
assert box_exists
125+
assert cast_to_bytes(box_content) == b"\x00" * expected_size
126+
127+
assert box.length == expected_size
128+
129+
130+
@pytest.mark.parametrize(
131+
("value_type", "size", "expected_size"),
132+
[
133+
(arc4.UInt64, 7, 8),
134+
(UInt64, 0, 8),
135+
(arc4.Address, 16, 32),
136+
(Account, 31, 32),
137+
(Application, 1, 8),
138+
(Asset, 0, 8),
139+
(bool, 1, 8),
140+
(arc4.StaticArray[arc4.Byte, typing.Literal[7]], 2, 7),
141+
],
142+
)
143+
def test_create_smaller_box_for_static_value_type(
144+
context: AlgopyTestContext, # noqa: ARG001
145+
value_type: type,
146+
size: int,
147+
expected_size: int,
148+
) -> None:
149+
key = b"test_key"
150+
box = Box(value_type, key=key) # type: ignore[var-annotated]
151+
assert not box
152+
153+
with pytest.warns(UserWarning, match=f"Box size should not be less than {expected_size}"):
154+
box.create(size=size)
155+
156+
157+
@pytest.mark.parametrize(
158+
("value_type", "size"),
159+
[
160+
(arc4.String, 7),
161+
(arc4.DynamicArray[arc4.UInt64], 0),
162+
(arc4.DynamicArray[arc4.Address], 16),
163+
(Bytes, 31),
164+
(arc4.StaticArray[arc4.String, typing.Literal[7]], 2),
165+
],
166+
)
167+
def test_create_box_for_dynamic_value_type(
168+
context: AlgopyTestContext, # noqa: ARG001
169+
value_type: type,
170+
size: int,
171+
) -> None:
172+
key = b"test_key"
173+
box = Box(value_type, key=key) # type: ignore[var-annotated]
174+
assert not box
175+
176+
box.create(size=size)
177+
178+
op_box_content, op_box_exists = algopy.op.Box.get(key)
179+
assert op_box_exists
180+
assert op_box_content == b"\x00" * size
181+
182+
assert box.length == size
183+
184+
185+
@pytest.mark.parametrize(
186+
"value_type",
187+
[
188+
arc4.String,
189+
arc4.DynamicArray[arc4.UInt64],
190+
arc4.DynamicArray[arc4.Address],
191+
Bytes,
192+
arc4.StaticArray[arc4.String, typing.Literal[7]],
193+
],
194+
)
195+
def test_create_box_for_dynamic_value_type_with_no_size(
196+
context: AlgopyTestContext, # noqa: ARG001
197+
value_type: type,
198+
) -> None:
199+
key = b"test_key"
200+
box = Box(value_type, key=key) # type: ignore[var-annotated]
201+
assert not box
202+
203+
with pytest.raises(
204+
ValueError,
205+
match=re.compile("does not have a fixed byte size. Please specify a size argument"),
206+
):
207+
box.create()
208+
209+
71210
@pytest.mark.parametrize(
72211
("value_type", "value"),
73212
[

0 commit comments

Comments
 (0)