Skip to content

feat: implement Box.create function #37

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 1 commit into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all 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: 41 additions & 1 deletion src/_algopy_testing/state/box.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from __future__ import annotations

import typing
import warnings

import _algopy_testing
from _algopy_testing.constants import MAX_BOX_SIZE
from _algopy_testing.context_helpers import lazy_context
from _algopy_testing.mutable import set_attr_on_mutate, set_item_on_mutate
from _algopy_testing.state.utils import cast_from_bytes, cast_to_bytes
from _algopy_testing.utils import as_bytes, as_string
from _algopy_testing.utils import as_bytes, as_int64, as_string, get_static_size_of

_TKey = typing.TypeVar("_TKey")
_TValue = typing.TypeVar("_TValue")
Expand Down Expand Up @@ -38,6 +39,45 @@ def __init__(
def __bool__(self) -> bool:
return lazy_context.ledger.box_exists(self.app_id, self.key)

def create(self, *, size: algopy.UInt64 | int | None = None) -> bool:
missing_size_err_msg = (
f"{self._type} does not have a fixed byte size. Please specify a size argument"
)

size_arg = None if size is None else as_int64(size)
content_type_size = get_static_size_of(self._type)

if content_type_size is None and size_arg is None:
raise ValueError(missing_size_err_msg)
if content_type_size is not None and size_arg is not None:
if size_arg < content_type_size:
warnings.warn(
f"Box size should not be less than {content_type_size}",
stacklevel=2,
)

if size_arg > content_type_size:
warnings.warn(
f"Size is set to {size_arg} but {self._type} has a fixed size of {content_type_size}", # noqa: E501
stacklevel=2,
)

size_int = size_arg if size_arg is not None else content_type_size
assert size_int is not None, missing_size_err_msg

if size_int > MAX_BOX_SIZE:
raise ValueError(f"Box size cannot exceed {MAX_BOX_SIZE}")

box_exists = lazy_context.ledger.box_exists(self.app_id, self.key)
if box_exists:
if self.length == size_int:
return False
else:
raise ValueError(f"Box already exists with a different size: {self.length}")

lazy_context.ledger.set_box(self.app_id, self.key, b"\x00" * size_int)
return True

@property
def key(self) -> algopy.Bytes:
if not self._key:
Expand Down
17 changes: 2 additions & 15 deletions src/_algopy_testing/utilities/size_of.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,9 @@
import types

from _algopy_testing.primitives.uint64 import UInt64
from _algopy_testing.utils import get_static_size_of


def size_of(typ: type | object, /) -> UInt64:
from _algopy_testing.arc4 import get_max_bytes_static_len
from _algopy_testing.serialize import get_native_to_arc4_serializer

if isinstance(typ, types.GenericAlias):
pass
elif not isinstance(typ, type):
typ = type(typ)

if typ is bool: # treat bool on its own as a uint64
typ = UInt64
serializer = get_native_to_arc4_serializer(typ) # type: ignore[arg-type]
type_info = serializer.arc4_type._type_info
size = get_max_bytes_static_len(type_info)
size = get_static_size_of(typ)
if size is None:
raise ValueError(f"{typ} is dynamically sized")
return UInt64(size)
22 changes: 20 additions & 2 deletions src/_algopy_testing/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import secrets
import types
import typing
from typing import TYPE_CHECKING

Expand All @@ -22,8 +23,6 @@
)

if TYPE_CHECKING:
import types

import algopy

from _algopy_testing.op.global_values import GlobalFields
Expand Down Expand Up @@ -195,3 +194,22 @@ def raise_mocked_function_error(func_name: str) -> typing.Never:
f"{func_name!r} is not available in test context. "
"Mock using your preferred testing framework."
)


def get_static_size_of(typ: type | object, /) -> int | None:
from _algopy_testing import UInt64
from _algopy_testing.arc4 import get_max_bytes_static_len
from _algopy_testing.serialize import get_native_to_arc4_serializer

if isinstance(typ, types.GenericAlias):
pass
elif not isinstance(typ, type):
typ = type(typ)

if typ is bool: # treat bool on its own as a uint64
typ = UInt64
serializer = get_native_to_arc4_serializer(typ) # type: ignore[arg-type]
type_info = serializer.arc4_type._type_info
size = get_max_bytes_static_len(type_info)

return size
139 changes: 139 additions & 0 deletions tests/models/test_box.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
import re
import typing
from collections.abc import Generator

import algopy
import pytest
from _algopy_testing import algopy_testing_context, arc4
from _algopy_testing.context import AlgopyTestContext
from _algopy_testing.models.account import Account
from _algopy_testing.models.application import Application
from _algopy_testing.models.asset import Asset
from _algopy_testing.op.pure import itob
from _algopy_testing.primitives.biguint import BigUInt
from _algopy_testing.primitives.bytes import Bytes
from _algopy_testing.primitives.string import String
from _algopy_testing.primitives.uint64 import UInt64
from _algopy_testing.state.box import Box
from _algopy_testing.state.utils import cast_to_bytes
from _algopy_testing.utils import as_bytes, as_string

from tests.artifacts.BoxContract.contract import BoxContract

BOX_NOT_CREATED_ERROR = "Box has not been created"


class Swapped(arc4.Struct):
b: arc4.UInt64
c: arc4.Bool
d: arc4.Address


# TODO: add tests for tuple and namedtuple once they are supported
# class MyStruct(typing.NamedTuple):
# a: UInt64
# b: bool
# c: arc4.Bool
# d: arc4.UInt64


class ATestContract(algopy.Contract):
def __init__(self) -> None:
self.uint_64_box = algopy.Box(algopy.UInt64)
Expand Down Expand Up @@ -68,6 +87,126 @@ def test_init_with_key(
_ = box.length


@pytest.mark.parametrize(
("value_type", "expected_size"),
[
(arc4.UInt64, 8),
(UInt64, 8),
(arc4.Address, 32),
(Account, 32),
(Application, 8),
(Asset, 8),
(bool, 8),
(arc4.StaticArray[arc4.Byte, typing.Literal[7]], 7),
(Swapped, 41),
# TODO: add tests for tuple and namedtuple once they are supported
# (tuple[arc4.UInt64, arc4.Bool, arc4.Address], 41),
# (MyStruct, 9),
],
)
def test_create_for_static_value_type(
context: AlgopyTestContext, # noqa: ARG001
value_type: type,
expected_size: int,
) -> None:
key = b"test_key"
box = Box(value_type, key=key) # type: ignore[var-annotated]
assert not box

box.create()
assert box

op_box_content, op_box_exists = algopy.op.Box.get(key)
assert op_box_exists
assert op_box_content == b"\x00" * expected_size

box_content, box_exists = box.maybe()
assert box_exists
assert cast_to_bytes(box_content) == b"\x00" * expected_size

assert box.length == expected_size


@pytest.mark.parametrize(
("value_type", "size", "expected_size"),
[
(arc4.UInt64, 7, 8),
(UInt64, 0, 8),
(arc4.Address, 16, 32),
(Account, 31, 32),
(Application, 1, 8),
(Asset, 0, 8),
(bool, 1, 8),
(arc4.StaticArray[arc4.Byte, typing.Literal[7]], 2, 7),
],
)
def test_create_smaller_box_for_static_value_type(
context: AlgopyTestContext, # noqa: ARG001
value_type: type,
size: int,
expected_size: int,
) -> None:
key = b"test_key"
box = Box(value_type, key=key) # type: ignore[var-annotated]
assert not box

with pytest.warns(UserWarning, match=f"Box size should not be less than {expected_size}"):
box.create(size=size)


@pytest.mark.parametrize(
("value_type", "size"),
[
(arc4.String, 7),
(arc4.DynamicArray[arc4.UInt64], 0),
(arc4.DynamicArray[arc4.Address], 16),
(Bytes, 31),
(arc4.StaticArray[arc4.String, typing.Literal[7]], 2),
],
)
def test_create_box_for_dynamic_value_type(
context: AlgopyTestContext, # noqa: ARG001
value_type: type,
size: int,
) -> None:
key = b"test_key"
box = Box(value_type, key=key) # type: ignore[var-annotated]
assert not box

box.create(size=size)

op_box_content, op_box_exists = algopy.op.Box.get(key)
assert op_box_exists
assert op_box_content == b"\x00" * size

assert box.length == size


@pytest.mark.parametrize(
"value_type",
[
arc4.String,
arc4.DynamicArray[arc4.UInt64],
arc4.DynamicArray[arc4.Address],
Bytes,
arc4.StaticArray[arc4.String, typing.Literal[7]],
],
)
def test_create_box_for_dynamic_value_type_with_no_size(
context: AlgopyTestContext, # noqa: ARG001
value_type: type,
) -> None:
key = b"test_key"
box = Box(value_type, key=key) # type: ignore[var-annotated]
assert not box

with pytest.raises(
ValueError,
match=re.compile("does not have a fixed byte size. Please specify a size argument"),
):
box.create()


@pytest.mark.parametrize(
("value_type", "value"),
[
Expand Down