Skip to content

Commit 51c2817

Browse files
refactor: simplify attribute access for Asset and Application types (#19)
* refactor: simplify attribute access for Asset and Application types, and handle cases where id's are indexes * refactor: make internal data structures private on public classes * fix: Make Global.latest_timestamp constant for a transaction
1 parent 0248875 commit 51c2817

File tree

12 files changed

+90
-99
lines changed

12 files changed

+90
-99
lines changed

src/_algopy_testing/context_helpers/context_storage.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,13 @@ def get_app_data(
7878

7979
def get_asset_data(self, asset_id: int | algopy.UInt64) -> AssetFields:
8080
try:
81-
return self.ledger.asset_data[int(asset_id)]
81+
return self.ledger._asset_data[int(asset_id)]
8282
except KeyError:
8383
raise ValueError("Unknown asset, check correct testing context is active") from None
8484

8585
def get_account_data(self, account_public_key: str) -> AccountContextData:
8686
try:
87-
return self.ledger.account_data[account_public_key]
87+
return self.ledger._account_data[account_public_key]
8888
except KeyError:
8989
raise ValueError("Unknown account, check correct testing context is active") from None
9090

src/_algopy_testing/context_helpers/ledger_context.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,25 @@ class LedgerContext:
2323
def __init__(self) -> None:
2424
from _algopy_testing.models.account import AccountContextData, get_empty_account
2525

26-
self.account_data = defaultdict[str, AccountContextData](get_empty_account)
27-
self.app_data: dict[int, ApplicationContextData] = {}
28-
self.asset_data: dict[int, AssetFields] = {}
29-
self.blocks: dict[int, dict[str, int]] = {}
30-
self.global_fields: GlobalFields = get_default_global_fields()
26+
self._account_data = defaultdict[str, AccountContextData](get_empty_account)
27+
self._app_data: dict[int, ApplicationContextData] = {}
28+
self._asset_data: dict[int, AssetFields] = {}
29+
self._blocks: dict[int, dict[str, int]] = {}
30+
self._global_fields: GlobalFields = get_default_global_fields()
3131

3232
self._asset_id = iter(range(1001, 2**64))
3333
self._app_id = iter(range(1001, 2**64))
3434

35-
def get_next_asset_id(self) -> int:
35+
def _get_next_asset_id(self) -> int:
3636
while True:
3737
asset_id = next(self._asset_id)
38-
if asset_id not in self.asset_data:
38+
if asset_id not in self._asset_data:
3939
return asset_id
4040

41-
def get_next_app_id(self) -> int:
41+
def _get_next_app_id(self) -> int:
4242
while True:
4343
app_id = next(self._app_id)
44-
if app_id not in self.app_data:
44+
if app_id not in self._app_data:
4545
return app_id
4646

4747
def get_account(self, address: str) -> algopy.Account:
@@ -68,7 +68,7 @@ def account_exists(self, address: str) -> bool:
6868
bool: True if the account exists, False otherwise.
6969
"""
7070
assert_address_is_valid(address)
71-
return address in self.account_data
71+
return address in self._account_data
7272

7373
def update_account(
7474
self,
@@ -84,11 +84,11 @@ def update_account(
8484
**account_fields: The fields to update.
8585
"""
8686
assert_address_is_valid(address)
87-
self.account_data[address].fields.update(account_fields)
87+
self._account_data[address].fields.update(account_fields)
8888

8989
if opted_asset_balances is not None:
9090
for asset_id, balance in opted_asset_balances.items():
91-
self.account_data[address].opted_asset_balances[UInt64(asset_id)] = balance
91+
self._account_data[address].opted_asset_balances[UInt64(asset_id)] = balance
9292

9393
def get_asset(self, asset_id: algopy.UInt64 | int) -> algopy.Asset:
9494
"""Get an asset by ID.
@@ -105,7 +105,7 @@ def get_asset(self, asset_id: algopy.UInt64 | int) -> algopy.Asset:
105105
import algopy
106106

107107
asset_id = int(asset_id) if isinstance(asset_id, algopy.UInt64) else asset_id
108-
if asset_id not in self.asset_data:
108+
if asset_id not in self._asset_data:
109109
raise ValueError("Asset not found in testing context!")
110110

111111
return algopy.Asset(asset_id)
@@ -122,7 +122,7 @@ def asset_exists(self, asset_id: algopy.UInt64 | int) -> bool:
122122
import algopy
123123

124124
asset_id = int(asset_id) if isinstance(asset_id, algopy.UInt64) else asset_id
125-
return asset_id in self.asset_data
125+
return asset_id in self._asset_data
126126

127127
def update_asset(self, asset_id: int, **asset_fields: typing.Unpack[AssetFields]) -> None:
128128
"""Update asset fields.
@@ -134,9 +134,9 @@ def update_asset(self, asset_id: int, **asset_fields: typing.Unpack[AssetFields]
134134
Raises:
135135
ValueError: If the asset is not found.
136136
"""
137-
if asset_id not in self.asset_data:
137+
if asset_id not in self._asset_data:
138138
raise ValueError("Asset not found in testing context!")
139-
self.asset_data[asset_id].update(asset_fields)
139+
self._asset_data[asset_id].update(asset_fields)
140140

141141
def get_app(
142142
self, app_id: algopy.Contract | algopy.Application | algopy.UInt64 | int
@@ -164,7 +164,7 @@ def app_exists(self, app_id: algopy.UInt64 | int) -> bool:
164164
bool: True if the application exists, False otherwise.
165165
"""
166166
app_id = _get_app_id(app_id)
167-
return app_id in self.app_data
167+
return app_id in self._app_data
168168

169169
def update_app(
170170
self, app_id: int, **application_fields: typing.Unpack[ApplicationFields]
@@ -343,7 +343,7 @@ def set_block(
343343
seed (algopy.UInt64 | int): The block seed.
344344
timestamp (algopy.UInt64 | int): The block timestamp.
345345
"""
346-
self.blocks[index] = {"seed": int(seed), "timestamp": int(timestamp)}
346+
self._blocks[index] = {"seed": int(seed), "timestamp": int(timestamp)}
347347

348348
def get_block_content(self, index: int, key: str) -> int:
349349
"""Get block content.
@@ -358,7 +358,7 @@ def get_block_content(self, index: int, key: str) -> int:
358358
Raises:
359359
ValueError: If the block content is not found.
360360
"""
361-
content = self.blocks.get(index, {}).get(key, None)
361+
content = self._blocks.get(index, {}).get(key, None)
362362
if content is None:
363363
raise KeyError(
364364
f"Block content for index {index} and key {key} not found in testing context!"
@@ -383,7 +383,7 @@ def patch_global_fields(self, **global_fields: typing.Unpack[GlobalFields]) -> N
383383
f"Invalid field(s) found during patch for `Global`: {', '.join(invalid_keys)}"
384384
)
385385

386-
self.global_fields.update(global_fields)
386+
self._global_fields.update(global_fields)
387387

388388
def _get_app_data(
389389
self, app: algopy.UInt64 | algopy.Application | algopy.Contract | int
@@ -401,7 +401,7 @@ def _get_app_data(
401401
"""
402402
app_id = _get_app_id(app)
403403
try:
404-
return self.app_data[app_id]
404+
return self._app_data[app_id]
405405
except KeyError:
406406
raise ValueError("Unknown app id, is there an active transaction?") from None
407407

src/_algopy_testing/context_helpers/txn_context.py

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

33
import contextlib
4+
import time
45
import typing
56

67
import algosdk
@@ -207,6 +208,7 @@ def __init__(
207208
active_txn_index: int | None = None,
208209
active_txn_overrides: dict[str, typing.Any] | None = None,
209210
):
211+
self._latest_timestamp = int(time.time())
210212
self._set_txn_group(txns, active_txn_index)
211213
self._itxn_groups: list[Sequence[InnerTransactionResultType]] = []
212214
self._constructing_itxn_group: list[InnerTransaction] = []

src/_algopy_testing/models/account.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def __init__(self, value: str | Bytes = algosdk.constants.ZERO_ADDRESS, /):
8282
def data(self) -> AccountContextData:
8383
from _algopy_testing.context_helpers import lazy_context
8484

85-
return lazy_context.ledger.account_data[self.public_key]
85+
return lazy_context.get_account_data(self.public_key)
8686

8787
@property
8888
def balance(self) -> algopy.UInt64:

src/_algopy_testing/models/application.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
from __future__ import annotations
22

3-
import inspect
43
import typing
54

65
from _algopy_testing.primitives import UInt64
76
from _algopy_testing.protocols import UInt64Backed
8-
from _algopy_testing.utils import as_int64
7+
from _algopy_testing.utils import as_int64, resolve_app_index
98

109
if typing.TYPE_CHECKING:
1110
from collections.abc import Sequence
@@ -70,22 +69,15 @@ def from_int(cls, value: int, /) -> typing.Self:
7069
def fields(self) -> ApplicationFields:
7170
from _algopy_testing.context_helpers import lazy_context
7271

73-
if self._id == 0:
74-
raise ValueError("cannot access properties of an app with an id of 0") from None
75-
return lazy_context.get_app_data(self._id).fields
72+
return lazy_context.get_app_data(resolve_app_index(self._id)).fields
7673

7774
def __getattr__(self, name: str) -> typing.Any:
78-
if name in inspect.get_annotations(ApplicationFields):
79-
value = self.fields.get(name)
80-
# TODO: 1.0 ensure reasonable default values are present (like account does)
81-
if value is None:
82-
raise ValueError(
83-
f"The Application value '{name}' has not been defined on the test context. "
84-
f"Make sure to patch the field '{name}' using your `AlgopyTestContext` "
85-
"instance."
86-
)
87-
return value
88-
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
75+
try:
76+
return self.fields[name] # type: ignore[literal-required]
77+
except KeyError:
78+
raise AttributeError(
79+
f"'{self.__class__.__name__}' object has no attribute '{name}'"
80+
) from None
8981

9082
def __eq__(self, other: object) -> bool:
9183
if isinstance(other, Application):

src/_algopy_testing/models/asset.py

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import typing
44

55
from _algopy_testing.protocols import UInt64Backed
6+
from _algopy_testing.utils import resolve_asset_index
67

78
if typing.TYPE_CHECKING:
89
import algopy
@@ -65,39 +66,19 @@ def frozen(self, _account: algopy.Account) -> bool:
6566
"Please mock this method using your python testing framework of choice."
6667
)
6768

68-
def __getattr__(self, name: str) -> object:
69+
@property
70+
def fields(self) -> AssetFields:
6971
from _algopy_testing.context_helpers import lazy_context
7072

71-
if int(self.id) not in lazy_context.ledger.asset_data:
72-
# check if its not 0 (which means its not
73-
# instantiated/opted-in yet, and instantiated directly
74-
# without invoking any_asset).
75-
if self.id == 0:
76-
# Handle dunder methods specially
77-
if name.startswith("__") and name.endswith("__"):
78-
return getattr(type(self), name)
79-
# For non-dunder attributes, check in __dict__
80-
if name in self.__dict__:
81-
return self.__dict__[name]
82-
raise AttributeError(
83-
f"'{self.__class__.__name__}' object has no attribute '{name}'"
84-
)
85-
86-
raise ValueError(
87-
"`algopy.Asset` is not present in the test context! "
88-
"Use `context.add_asset()` or `context.any.asset()` to add the asset to "
89-
"your test setup."
90-
)
73+
return lazy_context.get_asset_data(resolve_asset_index(self.id))
9174

92-
return_value = lazy_context.get_asset_data(self.id).get(name)
93-
if return_value is None:
75+
def __getattr__(self, name: str) -> typing.Any:
76+
try:
77+
return self.fields[name] # type: ignore[literal-required]
78+
except KeyError:
9479
raise AttributeError(
95-
f"The value for '{name}' in the test context is None. "
96-
f"Make sure to patch the global field '{name}' using your `AlgopyTestContext` "
97-
"instance."
98-
)
99-
100-
return return_value
80+
f"'{self.__class__.__name__}' object has no attribute '{name}'"
81+
) from None
10182

10283
def __eq__(self, other: object) -> bool:
10384
if isinstance(other, Asset):

src/_algopy_testing/op/global_values.py

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

3-
import time
43
import typing
54
from typing import TypedDict, TypeVar
65

@@ -38,7 +37,7 @@ class GlobalFields(TypedDict, total=False):
3837
class _Global:
3938
@property
4039
def _fields(self) -> GlobalFields:
41-
return lazy_context.ledger.global_fields
40+
return lazy_context.ledger._global_fields
4241

4342
@property
4443
def current_application_address(self) -> algopy.Account:
@@ -60,8 +59,7 @@ def latest_timestamp(self) -> algopy.UInt64:
6059
try:
6160
return self._fields["latest_timestamp"]
6261
except KeyError:
63-
# TODO: 1.0 Construct this while setting default values rather than here.
64-
return UInt64(int(time.time()))
62+
return UInt64(lazy_context.active_group._latest_timestamp)
6563

6664
@property
6765
def group_size(self) -> algopy.UInt64:

src/_algopy_testing/op/misc.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
from _algopy_testing.models import Account, Application, Asset
1111
from _algopy_testing.primitives.bytes import Bytes
1212
from _algopy_testing.primitives.uint64 import UInt64
13-
from _algopy_testing.utils import raise_mocked_function_error
13+
from _algopy_testing.utils import (
14+
raise_mocked_function_error,
15+
resolve_app_index,
16+
resolve_asset_index,
17+
)
1418

1519
if typing.TYPE_CHECKING:
1620
import algopy
@@ -23,10 +27,7 @@ def err() -> None:
2327
def _get_app(app: algopy.Application | algopy.UInt64 | int) -> Application:
2428
if isinstance(app, Application):
2529
return app
26-
if app >= 1001:
27-
return lazy_context.ledger.get_app(app)
28-
txn = lazy_context.active_group.active_txn
29-
return txn.apps(app)
30+
return lazy_context.ledger.get_app(resolve_app_index(app))
3031

3132

3233
def _get_account(acc: algopy.Account | algopy.UInt64 | int) -> Account:
@@ -39,10 +40,7 @@ def _get_account(acc: algopy.Account | algopy.UInt64 | int) -> Account:
3940
def _get_asset(asset: algopy.Asset | algopy.UInt64 | int) -> Asset:
4041
if isinstance(asset, Asset):
4142
return asset
42-
if asset >= 1001:
43-
return lazy_context.ledger.get_asset(asset)
44-
txn = lazy_context.active_group.active_txn
45-
return txn.assets(asset)
43+
return lazy_context.ledger.get_asset(resolve_asset_index(asset))
4644

4745

4846
def _get_bytes(b: algopy.Bytes | bytes) -> bytes:

src/_algopy_testing/utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,28 @@
2929
from _algopy_testing.op.global_values import GlobalFields
3030

3131

32+
def resolve_app_index(app_id_or_index: algopy.UInt64 | int) -> int:
33+
from _algopy_testing.context_helpers import lazy_context
34+
35+
if app_id_or_index >= 1001:
36+
app_id = app_id_or_index
37+
else:
38+
txn = lazy_context.active_group.active_txn
39+
app_id = txn.apps(app_id_or_index).id
40+
return int(app_id)
41+
42+
43+
def resolve_asset_index(asset_id_or_index: algopy.UInt64 | int) -> int:
44+
from _algopy_testing.context_helpers import lazy_context
45+
46+
if asset_id_or_index >= 1001:
47+
asset_id = asset_id_or_index
48+
else:
49+
txn = lazy_context.active_group.active_txn
50+
asset_id = txn.assets(asset_id_or_index).id
51+
return int(asset_id)
52+
53+
3254
def generate_random_int(min_value: int, max_value: int) -> int:
3355
return secrets.randbelow(max_value - min_value + 1) + min_value
3456

0 commit comments

Comments
 (0)