Skip to content

Commit 1f1f2ea

Browse files
fix: ensure mutable types (ARC4 tuple, array and structs) have their changes propagated back to container-like types (global/local state, boxes, ARC4 mutable types) (#14)
* fix: add default __eq__ implementation for ARC4 types * fix: ensure mutable types (ARC4 tuple, array and structs) have their changes propagated back to container-like types (global/local state, boxes, ARC4 mutable types) * feat: add __str__ and __repr__ implementations for ARC4 types * refactor: make ARC4 type_info private * refactor: make ARC4 struct inherit _ABIEncoded * refactor: removing get_app_for_contract; expanding get_app; parsing on_complete --------- Co-authored-by: Altynbek Orumbayev <altynbek.orumbayev@makerx.com.au>
1 parent 9744378 commit 1f1f2ea

File tree

27 files changed

+1467
-209
lines changed

27 files changed

+1467
-209
lines changed

examples/auction/test_contract.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def test_opt_into_asset(context: AlgopyTestContext) -> None:
2626
assert contract.asa.id == asset.id
2727
inner_txn = context.txn.last_group.last_itxn.asset_transfer
2828
assert (
29-
inner_txn.asset_receiver == context.get_app_for_contract(contract).address
29+
inner_txn.asset_receiver == context.ledger.get_app(contract).address
3030
), "Asset receiver does not match"
3131
assert inner_txn.xfer_asset == asset, "Transferred asset does not match"
3232

@@ -36,7 +36,7 @@ def test_start_auction(
3636
) -> None:
3737
# Arrange
3838
contract = AuctionContract()
39-
app = context.get_app_for_contract(contract)
39+
app = context.ledger.get_app(contract)
4040
latest_timestamp = context.any.uint64(1, 1000)
4141
starting_price = context.any.uint64()
4242
auction_duration = context.any.uint64(100, 1000)

examples/marketplace/test_contract.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def test_first_deposit(
4141
test_nonce: arc4.UInt64,
4242
) -> None:
4343
# Arrange
44-
test_app = context.get_app_for_contract(contract)
44+
test_app = context.ledger.get_app(contract)
4545

4646
# Act
4747
contract.first_deposit(
@@ -74,7 +74,7 @@ def test_deposit(
7474
test_nonce: arc4.UInt64,
7575
) -> None:
7676
# Arrange
77-
test_app = context.get_app_for_contract(contract)
77+
test_app = context.ledger.get_app(contract)
7878
listing_key = ListingKey(
7979
owner=arc4.Address(str(context.default_sender)),
8080
asset=arc4.UInt64(test_asset.id),
@@ -225,7 +225,7 @@ def test_bid(
225225
test_nonce: arc4.UInt64,
226226
) -> None:
227227
# Arrange
228-
app = context.get_app_for_contract(contract)
228+
app = context.ledger.get_app(contract)
229229
owner = arc4.Address(str(context.default_sender))
230230
initial_price = context.any.arc4.uint64(max_value=int(10e6))
231231
initial_deposit = context.any.arc4.uint64(max_value=int(1e6))

examples/scratch_storage/test_contract.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_simple_contract(context: AlgopyTestContext) -> None:
3535
with context.txn.create_group(
3636
gtxns=[
3737
context.any.txn.application_call(
38-
app_id=context.get_app_for_contract(contract), scratch_space=[0, 5, b"Hello World"]
38+
app_id=context.ledger.get_app(contract), scratch_space=[0, 5, b"Hello World"]
3939
)
4040
]
4141
):

examples/simple_voting/test_contract.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def test_vote(context: AlgopyTestContext) -> None:
3636
gtxns=[
3737
context.any.txn.application_call(
3838
sender=voter,
39-
app_id=context.get_app_for_contract(contract),
39+
app_id=context.ledger.get_app(contract),
4040
app_args=[algopy.Bytes(b"vote"), voter.bytes],
4141
),
4242
context.any.txn.payment(

examples/zk_whitelist/test_contract.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def test_add_address_to_whitelist_invalid_proof(
7171
@pytest.mark.usefixtures("context")
7272
def test_is_on_whitelist(context: AlgopyTestContext, contract: ZkWhitelistContract) -> None:
7373
# Arrange
74-
dummy_account = context.any.account(opted_apps=[context.get_app_for_contract(contract)])
74+
dummy_account = context.any.account(opted_apps=[context.ledger.get_app(contract)])
7575
contract.whitelist[dummy_account] = True
7676

7777
# Act
@@ -84,7 +84,7 @@ def test_is_on_whitelist(context: AlgopyTestContext, contract: ZkWhitelistContra
8484
@pytest.mark.usefixtures("context")
8585
def test_is_not_on_whitelist(context: AlgopyTestContext, contract: ZkWhitelistContract) -> None:
8686
# Arrange
87-
dummy_account = context.any.account(opted_apps=[context.get_app_for_contract(contract)])
87+
dummy_account = context.any.account(opted_apps=[context.ledger.get_app(contract)])
8888
contract.whitelist[dummy_account] = False
8989

9090
# Act

src/algopy_testing/_context_helpers/ledger_context.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,18 +138,21 @@ def update_asset(self, asset_id: int, **asset_fields: typing.Unpack[AssetFields]
138138
raise ValueError("Asset not found in testing context!")
139139
self.asset_data[asset_id].update(asset_fields)
140140

141-
def get_app(self, app: algopy.UInt64 | int) -> algopy.Application:
142-
"""Get an application by ID.
141+
def get_app(
142+
self, app_id: algopy.Contract | algopy.Application | algopy.UInt64 | int
143+
) -> algopy.Application:
144+
"""Get an application by ID, contract or app reference.
143145
144146
Args:
145-
app (algopy.UInt64 | int): The application ID.
147+
app_id (algopy.Contract | algopy.UInt64 | int): The application ID. If not provided as
148+
int, auto extracts id from contract or app reference.
146149
147150
Returns:
148151
algopy.Application: The application object.
149152
"""
150153
import algopy
151154

152-
app_data = self._get_app_data(app)
155+
app_data = self._get_app_data(app_id)
153156
return algopy.Application(app_data.app_id)
154157

155158
def app_exists(self, app: algopy.UInt64 | int) -> bool:

src/algopy_testing/_context_helpers/txn_context.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def defer_app_call(
118118
app_id=app_id,
119119
arc4_signature=arc4_metadata.arc4_signature,
120120
args=ordered_args,
121+
allow_actions=arc4_metadata.allow_actions or [OnCompleteAction.NoOp],
121122
)
122123
# Handle bare methods
123124
else:

src/algopy_testing/_mutable.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from __future__ import annotations
2+
3+
import copy
4+
import typing
5+
6+
if typing.TYPE_CHECKING:
7+
from collections.abc import Callable
8+
9+
TKey = typing.TypeVar("TKey")
10+
TItem = typing.TypeVar("TItem")
11+
12+
13+
class MutableBytes:
14+
"""Helper class to ensure mutable algopy types (currently ARC4 array, tuple and
15+
structs) cascade their changes to parent container types (global/local state, boxes,
16+
ARC4 array, tuple and structs)"""
17+
18+
def __init__(self) -> None:
19+
# callback to call once when _value is modified
20+
self._on_mutate: Callable[[typing.Any], None] | None = None
21+
22+
@property
23+
def _value(self) -> bytes:
24+
return self.__value
25+
26+
@_value.setter
27+
def _value(self, value: bytes) -> None:
28+
self.__value = value
29+
if self._on_mutate:
30+
self._on_mutate(self)
31+
self._on_mutate = None
32+
33+
def copy(self) -> typing.Self:
34+
# when copying a value discard the _on_mutate callback
35+
clone = copy.deepcopy(self)
36+
clone._on_mutate = None
37+
return clone
38+
39+
40+
def set_item_on_mutate(container: object, index: object, value: TItem) -> TItem:
41+
"""Used to update a container-like type when an item is modified."""
42+
43+
def callback(new_value: TItem) -> None:
44+
container[index] = new_value # type: ignore[index]
45+
46+
return add_mutable_callback(callback, value)
47+
48+
49+
def set_attr_on_mutate(parent: object, name: str, value: TItem) -> TItem:
50+
"""Used to update an object-like type when an attr is modified."""
51+
52+
def callback(new_value: TItem) -> None:
53+
setattr(parent, name, new_value)
54+
55+
return add_mutable_callback(callback, value)
56+
57+
58+
def add_mutable_callback(on_mutate: Callable[[typing.Any], None], value: TItem) -> TItem:
59+
"""Add on_mutate callback to value, if value is mutable.
60+
61+
Returns value to simplify usage
62+
"""
63+
if isinstance(value, MutableBytes):
64+
value._on_mutate = on_mutate
65+
return value

0 commit comments

Comments
 (0)