algorand-python-testing 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. algopy/__init__.py +42 -0
  2. algopy/arc4.py +1 -0
  3. algopy/gtxn.py +1 -0
  4. algopy/itxn.py +1 -0
  5. algopy/op.py +1 -0
  6. algopy/py.typed +0 -0
  7. algopy_testing/__init__.py +47 -0
  8. algopy_testing/arc4.py +1222 -0
  9. algopy_testing/constants.py +17 -0
  10. algopy_testing/context.py +769 -0
  11. algopy_testing/decorators/__init__.py +0 -0
  12. algopy_testing/decorators/abimethod.py +146 -0
  13. algopy_testing/decorators/subroutine.py +9 -0
  14. algopy_testing/enums.py +39 -0
  15. algopy_testing/gtxn.py +239 -0
  16. algopy_testing/itxn.py +353 -0
  17. algopy_testing/models/__init__.py +23 -0
  18. algopy_testing/models/account.py +128 -0
  19. algopy_testing/models/application.py +72 -0
  20. algopy_testing/models/asset.py +109 -0
  21. algopy_testing/models/contract.py +69 -0
  22. algopy_testing/models/global_values.py +67 -0
  23. algopy_testing/models/gtxn.py +40 -0
  24. algopy_testing/models/itxn.py +34 -0
  25. algopy_testing/models/transactions.py +158 -0
  26. algopy_testing/models/txn.py +111 -0
  27. algopy_testing/models/unsigned_builtins.py +15 -0
  28. algopy_testing/op.py +639 -0
  29. algopy_testing/primitives/__init__.py +6 -0
  30. algopy_testing/primitives/biguint.py +147 -0
  31. algopy_testing/primitives/bytes.py +173 -0
  32. algopy_testing/primitives/string.py +67 -0
  33. algopy_testing/primitives/uint64.py +210 -0
  34. algopy_testing/py.typed +0 -0
  35. algopy_testing/state/__init__.py +4 -0
  36. algopy_testing/state/global_state.py +73 -0
  37. algopy_testing/state/local_state.py +54 -0
  38. algopy_testing/utils.py +156 -0
  39. algorand_python_testing-0.1.0.dist-info/METADATA +29 -0
  40. algorand_python_testing-0.1.0.dist-info/RECORD +41 -0
  41. algorand_python_testing-0.1.0.dist-info/WHEEL +4 -0
algopy_testing/itxn.py ADDED
@@ -0,0 +1,353 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing
5
+ from copy import deepcopy
6
+
7
+ import algosdk
8
+
9
+ from algopy_testing.constants import MAX_UINT64
10
+ from algopy_testing.context import get_test_context
11
+ from algopy_testing.models.transactions import (
12
+ PaymentFields,
13
+ _ApplicationCallBaseFields,
14
+ _AssetConfigBaseFields,
15
+ _AssetFreezeBaseFields,
16
+ _AssetTransferBaseFields,
17
+ _KeyRegistrationBaseFields,
18
+ _PaymentBaseFields,
19
+ _TransactionCoreFields,
20
+ _TransactionFields,
21
+ )
22
+ from algopy_testing.utils import dummy_transaction_id, txn_type_to_bytes
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ if typing.TYPE_CHECKING:
27
+ import algopy
28
+
29
+
30
+ class _BaseInnerTransaction:
31
+ fields: dict[str, typing.Any]
32
+
33
+ def submit(self) -> object:
34
+ import algopy.itxn
35
+
36
+ context = get_test_context()
37
+
38
+ if not context:
39
+ raise RuntimeError("No test context found")
40
+
41
+ result = algopy.itxn.InnerTransactionResult(**self.fields)
42
+ context._append_inner_transaction_group([result])
43
+ return result
44
+
45
+ def copy(self) -> typing.Self:
46
+ return deepcopy(self)
47
+
48
+ def get_field(self, type_dict: object, name: str) -> typing.Any:
49
+ if name in type_dict.__annotations__:
50
+ return self.fields.get(name)
51
+
52
+ raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")
53
+
54
+ def __eq__(self, other: object) -> bool:
55
+ if isinstance(other, _BaseInnerTransaction):
56
+ return bool(self.fields == other.fields)
57
+ return False
58
+
59
+ def __hash__(self) -> int:
60
+ return hash(self.fields)
61
+
62
+
63
+ class InnerTransaction(_BaseInnerTransaction):
64
+ def __init__(
65
+ self,
66
+ *,
67
+ type: algopy.TransactionType, # noqa: A002
68
+ **kwargs: typing.Unpack[_TransactionCoreFields],
69
+ ):
70
+ self.fields = {
71
+ "type": type,
72
+ "type_bytes": txn_type_to_bytes(int(type)),
73
+ **kwargs,
74
+ }
75
+
76
+ def set(self, **kwargs: typing.Unpack[_TransactionFields]) -> None:
77
+ """Updates inner transaction parameter values"""
78
+ self.fields.update(kwargs)
79
+
80
+ def __getattr__(self, name: str) -> object:
81
+ return self.get_field(_TransactionFields, name)
82
+
83
+
84
+ class Payment(_BaseInnerTransaction):
85
+ def __init__(self, **kwargs: typing.Unpack[_PaymentBaseFields]):
86
+ import algopy
87
+
88
+ self.fields = {**kwargs, "type": algopy.TransactionType.Payment}
89
+
90
+ def set(self, **kwargs: typing.Unpack[_PaymentBaseFields]) -> None:
91
+ """Updates inner transaction parameter values"""
92
+ import algopy
93
+
94
+ self.fields.update({**kwargs, "type": algopy.TransactionType.Payment})
95
+
96
+ def __getattr__(self, name: str) -> object:
97
+ return self.get_field(_PaymentBaseFields, name)
98
+
99
+
100
+ class KeyRegistration(_BaseInnerTransaction):
101
+ def __init__(self, **kwargs: typing.Unpack[_KeyRegistrationBaseFields]):
102
+ import algopy
103
+
104
+ self.fields = {**kwargs, "type": algopy.TransactionType.KeyRegistration}
105
+
106
+ def set(self, **kwargs: typing.Unpack[_KeyRegistrationBaseFields]) -> None:
107
+ """Updates inner transaction parameter values"""
108
+ import algopy
109
+
110
+ self.fields.update({**kwargs, "type": algopy.TransactionType.KeyRegistration})
111
+
112
+ def __getattr__(self, name: str) -> object:
113
+ return self.get_field(_KeyRegistrationBaseFields, name)
114
+
115
+
116
+ class AssetConfig(_BaseInnerTransaction):
117
+ def __init__(self, **kwargs: typing.Unpack[_AssetConfigBaseFields]):
118
+ import algopy
119
+
120
+ self.fields = {**kwargs, "type": algopy.TransactionType.AssetConfig}
121
+
122
+ def set(self, **kwargs: typing.Unpack[_AssetConfigBaseFields]) -> None:
123
+ """Updates inner transaction parameter values"""
124
+ import algopy
125
+
126
+ self.fields.update({**kwargs, "type": algopy.TransactionType.AssetConfig})
127
+
128
+ def __getattr__(self, name: str) -> object:
129
+ return self.get_field(_AssetConfigBaseFields, name)
130
+
131
+
132
+ class AssetTransfer(_BaseInnerTransaction):
133
+ def __init__(self, **kwargs: typing.Unpack[_AssetTransferBaseFields]):
134
+ import algopy
135
+
136
+ from algopy_testing import get_test_context
137
+
138
+ context = get_test_context()
139
+ self.fields = {
140
+ "type": algopy.TransactionType.AssetTransfer,
141
+ "asset_sender": context.default_application.address if context else None,
142
+ "amount": 0,
143
+ **kwargs,
144
+ }
145
+
146
+ def set(self, **kwargs: typing.Unpack[_AssetTransferBaseFields]) -> None:
147
+ """Updates inner transaction parameter values"""
148
+ import algopy
149
+
150
+ self.fields.update({**kwargs, "type": algopy.TransactionType.AssetTransfer})
151
+
152
+ def __getattr__(self, name: str) -> object:
153
+ return self.get_field(_AssetTransferBaseFields, name)
154
+
155
+
156
+ class AssetFreeze(_BaseInnerTransaction):
157
+ def __init__(self, **kwargs: typing.Unpack[_AssetFreezeBaseFields]):
158
+ import algopy
159
+
160
+ self.fields = {**kwargs, "type": algopy.TransactionType.AssetFreeze}
161
+
162
+ def set(self, **kwargs: typing.Unpack[_AssetFreezeBaseFields]) -> None:
163
+ """Updates inner transaction parameter values"""
164
+ import algopy
165
+
166
+ self.fields.update({**kwargs, "type": algopy.TransactionType.AssetFreeze})
167
+
168
+ def __getattr__(self, name: str) -> object:
169
+ return self.get_field(_AssetFreezeBaseFields, name)
170
+
171
+
172
+ class ApplicationCall(_BaseInnerTransaction):
173
+ def __init__(self, **kwargs: typing.Unpack[_ApplicationCallBaseFields]):
174
+ import algopy
175
+
176
+ self.fields = {**kwargs, "type": algopy.TransactionType.ApplicationCall}
177
+
178
+ def set(self, **kwargs: typing.Unpack[_ApplicationCallBaseFields]) -> None:
179
+ """Updates inner transaction parameter values"""
180
+ import algopy
181
+
182
+ self.fields.update({**kwargs, "type": algopy.TransactionType.ApplicationCall})
183
+
184
+ def __getattr__(self, name: str) -> object:
185
+ return self.get_field(_ApplicationCallBaseFields, name)
186
+
187
+
188
+ # ==== Inner Transaction Results ====
189
+ # These are used to represent finalized transactions submitted to the network
190
+ # and are created by the `submit` method of each inner transaction class
191
+
192
+
193
+ class _BaseInnerTransactionResult:
194
+ fields: dict[str, typing.Any]
195
+
196
+ @typing.overload
197
+ def __init__(
198
+ self,
199
+ txn_type: algopy.TransactionType,
200
+ **kwargs: typing.Unpack[_TransactionFields],
201
+ ): ...
202
+
203
+ @typing.overload
204
+ def __init__(
205
+ self,
206
+ **kwargs: typing.Unpack[_TransactionFields],
207
+ ): ...
208
+
209
+ def __init__(
210
+ self,
211
+ txn_type: algopy.TransactionType | None = None,
212
+ **kwargs: typing.Unpack[_TransactionFields],
213
+ ):
214
+ import algopy
215
+
216
+ if txn_type is None and kwargs.get("type") is None:
217
+ raise ValueError("No transaction type provided to `algopy.itxn.InnerTransaction`")
218
+
219
+ txn_type = txn_type if txn_type is not None else kwargs.get("type")
220
+ txn_type_bytes = txn_type_to_bytes(int(txn_type)) # type: ignore[arg-type]
221
+
222
+ self.fields = {
223
+ "type": txn_type,
224
+ "type_bytes": txn_type_bytes,
225
+ "first_valid": algopy.UInt64(0),
226
+ "first_valid_time": algopy.UInt64(0),
227
+ "last_valid": algopy.UInt64(MAX_UINT64),
228
+ "note": algopy.Bytes(b""),
229
+ "lease": algopy.Bytes(bytes(algosdk.constants.ZERO_ADDRESS, encoding="utf-8")),
230
+ "close_remainder_to": algopy.Bytes(
231
+ bytes(algosdk.constants.ZERO_ADDRESS, encoding="utf-8")
232
+ ),
233
+ "txn_id": algopy.Bytes(dummy_transaction_id()),
234
+ **kwargs,
235
+ }
236
+ self._parse_covariant_types()
237
+
238
+ def get_field(self, type_dict: object, name: str) -> typing.Any:
239
+ if name in type_dict.__annotations__:
240
+ return self.fields.get(name)
241
+
242
+ raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")
243
+
244
+ def _parse_covariant_types(
245
+ self,
246
+ ) -> None:
247
+ import algopy
248
+
249
+ for name, value in self.fields.items():
250
+ if isinstance(value, int):
251
+ self.fields[name] = algopy.UInt64(value)
252
+ if isinstance(value, bytes):
253
+ self.fields[name] = algopy.Bytes(value)
254
+ if isinstance(value, str):
255
+ self.fields[name] = algopy.Bytes(value.encode("utf-8"))
256
+
257
+
258
+ class PaymentInnerTransaction(_BaseInnerTransactionResult):
259
+ def __init__(self, **kwargs: typing.Unpack[PaymentFields]):
260
+ import algopy
261
+
262
+ super().__init__(algopy.TransactionType.Payment, **kwargs)
263
+
264
+ def __getattr__(self, name: str) -> object:
265
+ return self.get_field(PaymentFields, name)
266
+
267
+
268
+ class KeyRegistrationInnerTransaction(_BaseInnerTransactionResult):
269
+ def __init__(self, **kwargs: typing.Unpack[_KeyRegistrationBaseFields]):
270
+ import algopy
271
+
272
+ super().__init__(algopy.TransactionType.KeyRegistration, **kwargs)
273
+
274
+ def __getattr__(self, name: str) -> object:
275
+ return self.get_field(_KeyRegistrationBaseFields, name)
276
+
277
+
278
+ class AssetConfigInnerTransaction(_BaseInnerTransactionResult):
279
+ def __init__(self, **kwargs: typing.Unpack[_AssetConfigBaseFields]):
280
+ import algopy
281
+
282
+ super().__init__(algopy.TransactionType.AssetConfig, **kwargs)
283
+
284
+ def __getattr__(self, name: str) -> object:
285
+ return self.get_field(_AssetConfigBaseFields, name)
286
+
287
+
288
+ class AssetTransferInnerTransaction(_BaseInnerTransactionResult):
289
+ def __init__(self, **kwargs: typing.Unpack[_AssetTransferBaseFields]):
290
+ import algopy
291
+
292
+ super().__init__(algopy.TransactionType.AssetTransfer, **kwargs)
293
+
294
+ def __getattr__(self, name: str) -> object:
295
+ return self.get_field(_AssetTransferBaseFields, name)
296
+
297
+
298
+ class AssetFreezeInnerTransaction(_BaseInnerTransactionResult):
299
+ def __init__(self, **kwargs: typing.Unpack[_AssetFreezeBaseFields]):
300
+ import algopy
301
+
302
+ super().__init__(algopy.TransactionType.AssetFreeze, **kwargs)
303
+
304
+ def __getattr__(self, name: str) -> object:
305
+ return self.get_field(_AssetFreezeBaseFields, name)
306
+
307
+
308
+ class ApplicationCallInnerTransaction(_BaseInnerTransactionResult):
309
+ def __init__(self, **kwargs: typing.Unpack[_ApplicationCallBaseFields]):
310
+ import algopy
311
+
312
+ super().__init__(algopy.TransactionType.ApplicationCall, **kwargs)
313
+
314
+ def __getattr__(self, name: str) -> object:
315
+ return self.get_field(_ApplicationCallBaseFields, name)
316
+
317
+
318
+ class InnerTransactionResult(_BaseInnerTransactionResult):
319
+ def __init__(self, **kwargs: typing.Unpack[_TransactionFields]):
320
+ super().__init__(**kwargs)
321
+
322
+ def __getattr__(self, name: str) -> object:
323
+ return self.get_field(_TransactionFields, name)
324
+
325
+
326
+ _InnerTransactionsType = (
327
+ InnerTransactionResult
328
+ | PaymentInnerTransaction
329
+ | KeyRegistrationInnerTransaction
330
+ | AssetConfigInnerTransaction
331
+ | AssetTransferInnerTransaction
332
+ | AssetFreezeInnerTransaction
333
+ | ApplicationCallInnerTransaction
334
+ )
335
+
336
+ __all__ = [
337
+ "_BaseInnerTransaction",
338
+ "_InnerTransactionsType",
339
+ "InnerTransaction",
340
+ "Payment",
341
+ "KeyRegistration",
342
+ "AssetConfig",
343
+ "AssetTransfer",
344
+ "AssetFreeze",
345
+ "ApplicationCall",
346
+ "PaymentInnerTransaction",
347
+ "KeyRegistrationInnerTransaction",
348
+ "AssetConfigInnerTransaction",
349
+ "AssetTransferInnerTransaction",
350
+ "AssetFreezeInnerTransaction",
351
+ "ApplicationCallInnerTransaction",
352
+ "InnerTransactionResult",
353
+ ]
@@ -0,0 +1,23 @@
1
+ from algopy_testing.models.account import Account
2
+ from algopy_testing.models.application import Application
3
+ from algopy_testing.models.asset import Asset
4
+ from algopy_testing.models.contract import ARC4Contract, Contract
5
+ from algopy_testing.models.global_values import Global
6
+ from algopy_testing.models.gtxn import GTxn
7
+ from algopy_testing.models.itxn import ITxn
8
+ from algopy_testing.models.txn import Txn
9
+ from algopy_testing.models.unsigned_builtins import uenumerate, urange
10
+
11
+ __all__ = [
12
+ "ARC4Contract",
13
+ "Account",
14
+ "Application",
15
+ "Asset",
16
+ "Contract",
17
+ "Global",
18
+ "GTxn",
19
+ "ITxn",
20
+ "Txn",
21
+ "uenumerate",
22
+ "urange",
23
+ ]
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Self, TypedDict, TypeVar
5
+
6
+ import algosdk
7
+
8
+ from algopy_testing.primitives.bytes import Bytes
9
+ from algopy_testing.utils import as_bytes
10
+
11
+ if TYPE_CHECKING:
12
+ import algopy
13
+
14
+
15
+ T = TypeVar("T")
16
+
17
+
18
+ class AccountFields(TypedDict, total=False):
19
+ balance: algopy.UInt64
20
+ min_balance: algopy.UInt64
21
+ auth_address: algopy.Account
22
+ total_num_uint: algopy.UInt64
23
+ total_num_byte_slice: algopy.Bytes
24
+ total_extra_app_pages: algopy.UInt64
25
+ total_apps_created: algopy.UInt64
26
+ total_apps_opted_in: algopy.UInt64
27
+ total_assets_created: algopy.UInt64
28
+ total_assets: algopy.UInt64
29
+ total_boxes: algopy.UInt64
30
+ total_box_bytes: algopy.UInt64
31
+
32
+
33
+ @dataclass()
34
+ class Account:
35
+ _public_key: bytes
36
+
37
+ def __init__(self, value: str | Bytes = algosdk.constants.ZERO_ADDRESS, /):
38
+ if not isinstance(value, (str | Bytes)):
39
+ raise TypeError("Invalid value for Account")
40
+
41
+ public_key = (
42
+ algosdk.encoding.decode_address(value) if isinstance(value, str) else value.value
43
+ )
44
+
45
+ self._public_key = public_key
46
+
47
+ def is_opted_in(self, asset_or_app: algopy.Asset | algopy.Application, /) -> bool:
48
+ from algopy import Application, Asset
49
+
50
+ from algopy_testing import get_test_context
51
+
52
+ context = get_test_context()
53
+ opted_apps = context._account_data[str(self)].opted_apps
54
+ opted_asset_balances = context._account_data[str(self)].opted_asset_balances
55
+
56
+ if not context:
57
+ raise ValueError(
58
+ "Test context is not initialized! Use `with algopy_testing_context()` to access "
59
+ "the context manager."
60
+ )
61
+
62
+ if isinstance(asset_or_app, Asset):
63
+ return asset_or_app.id in opted_asset_balances
64
+ elif isinstance(asset_or_app, Application):
65
+ return asset_or_app.id in opted_apps
66
+
67
+ raise TypeError(
68
+ "Invalid `asset_or_app` argument type. Must be an `algopy.Asset` or "
69
+ "`algopy.Application` instance."
70
+ )
71
+
72
+ @classmethod
73
+ def from_bytes(cls, value: algopy.Bytes | bytes) -> Self:
74
+ # NOTE: AVM does not perform any validation beyond type.
75
+ validated_value = as_bytes(value)
76
+ return cls(Bytes(validated_value))
77
+
78
+ @property
79
+ def bytes(self) -> Bytes:
80
+ return Bytes(self._public_key)
81
+
82
+ def __getattr__(self, name: str) -> object:
83
+ from algopy_testing.context import get_test_context
84
+
85
+ context = get_test_context()
86
+ if not context:
87
+ raise ValueError(
88
+ "Test context is not initialized! Use `with algopy_testing_context()` to access "
89
+ "the context manager."
90
+ )
91
+
92
+ if str(self) not in context._account_data:
93
+ raise ValueError(
94
+ "`algopy.Account` is not present in the test context! "
95
+ "Use `context.add_account()` or `context.any_account()` to add the account "
96
+ "to your test setup."
97
+ )
98
+
99
+ return_value = context._account_data[str(self)].fields.get(name)
100
+ if return_value is None:
101
+ raise AttributeError(
102
+ f"The value for '{name}' in the test context is None. "
103
+ f"Make sure to patch the global field '{name}' using your `AlgopyTestContext` "
104
+ "instance."
105
+ )
106
+
107
+ return return_value
108
+
109
+ def __repr__(self) -> str:
110
+ return str(algosdk.encoding.encode_address(self._public_key))
111
+
112
+ def __str__(self) -> str:
113
+ return str(algosdk.encoding.encode_address(self._public_key))
114
+
115
+ def __eq__(self, other: object) -> bool:
116
+ if not isinstance(other, Account | str):
117
+ raise TypeError("Invalid value for Account")
118
+ if isinstance(other, Account):
119
+ return self._public_key == other._public_key
120
+ return self._public_key == as_bytes(other)
121
+
122
+ def __bool__(self) -> bool:
123
+ return bool(self._public_key) and self._public_key != algosdk.encoding.decode_address(
124
+ algosdk.constants.ZERO_ADDRESS
125
+ )
126
+
127
+ def __hash__(self) -> int:
128
+ return hash(self._public_key)
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, TypedDict, TypeVar
6
+
7
+ from algopy_testing.utils import as_string
8
+
9
+ if TYPE_CHECKING:
10
+ import algopy
11
+
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ class ApplicationFields(TypedDict, total=False):
17
+ approval_program: algopy.Bytes
18
+ clear_state_program: algopy.Bytes
19
+ global_num_uint: algopy.UInt64
20
+ global_num_bytes: algopy.UInt64
21
+ local_num_uint: algopy.UInt64
22
+ local_num_bytes: algopy.UInt64
23
+ extra_program_pages: algopy.UInt64
24
+ creator: algopy.Account
25
+ address: algopy.Account
26
+
27
+
28
+ @dataclass()
29
+ class Application:
30
+ id: algopy.UInt64
31
+
32
+ def __init__(self, application_id: algopy.UInt64 | int = 0, /):
33
+ from algopy import UInt64
34
+
35
+ self.id = application_id if isinstance(application_id, UInt64) else UInt64(application_id)
36
+
37
+ def __getattr__(self, name: str) -> typing.Any:
38
+ from algopy_testing.context import get_test_context
39
+
40
+ context = get_test_context()
41
+ if not context:
42
+ raise ValueError(
43
+ "Test context is not initialized! Use `with algopy_testing_context()` to access "
44
+ "the context manager."
45
+ )
46
+ if int(self.id) not in context._application_data:
47
+ raise ValueError(
48
+ "`algopy.Application` is not present in the test context! "
49
+ "Use `context.add_application()` or `context.any_application()` to add the "
50
+ "application to your test setup."
51
+ )
52
+
53
+ return_value = context._application_data[int(self.id)].get(name)
54
+ if return_value is None:
55
+ raise AttributeError(
56
+ f"The value for '{name}' in the test context is None. "
57
+ f"Make sure to patch the global field '{name}' using your `AlgopyTestContext` "
58
+ "instance."
59
+ )
60
+
61
+ return return_value
62
+
63
+ def __eq__(self, other: object) -> bool:
64
+ if isinstance(other, Application):
65
+ return self.id == other.id
66
+ return self.id == as_string(other)
67
+
68
+ def __bool__(self) -> bool:
69
+ return self.id != 0
70
+
71
+ def __hash__(self) -> int:
72
+ return hash(self.id)
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, TypedDict, TypeVar
5
+
6
+ if TYPE_CHECKING:
7
+ import algopy
8
+
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ class AssetFields(TypedDict, total=False):
14
+ total: algopy.UInt64
15
+ decimals: algopy.UInt64
16
+ default_frozen: bool
17
+ unit_name: algopy.Bytes
18
+ name: algopy.Bytes
19
+ url: algopy.Bytes
20
+ metadata_hash: algopy.Bytes
21
+ manager: algopy.Account
22
+ reserve: algopy.Account
23
+ freeze: algopy.Account
24
+ clawback: algopy.Account
25
+ creator: algopy.Account
26
+
27
+
28
+ @dataclass
29
+ class Asset:
30
+ id: algopy.UInt64
31
+
32
+ def __init__(self, asset_id: algopy.UInt64 | int = 0):
33
+ from algopy import UInt64
34
+
35
+ self.id = asset_id if isinstance(asset_id, UInt64) else UInt64(asset_id)
36
+
37
+ def balance(self, account: algopy.Account) -> algopy.UInt64:
38
+ from algopy_testing.context import get_test_context
39
+
40
+ context = get_test_context()
41
+ if not context:
42
+ raise ValueError(
43
+ "Test context is not initialized! Use `with algopy_testing_context()` to access "
44
+ "the context manager."
45
+ )
46
+
47
+ if account not in context._account_data:
48
+ raise ValueError(
49
+ "The account is not present in the test context! "
50
+ "Use `context.add_account()` or `context.any_account()` to add the account to "
51
+ "your test setup."
52
+ )
53
+
54
+ account_data = context._account_data.get(str(account), None)
55
+
56
+ if not account_data:
57
+ raise ValueError("Account not found in testing context!")
58
+
59
+ if int(self.id) not in account_data.opted_asset_balances:
60
+ raise ValueError(
61
+ "The asset is not opted into the account! "
62
+ "Use `account.opt_in()` to opt the asset into the account."
63
+ )
64
+
65
+ return account_data.opted_asset_balances[self.id]
66
+
67
+ def frozen(self, _account: algopy.Account) -> bool:
68
+ raise NotImplementedError(
69
+ "The 'frozen' method is being executed in a python testing context. "
70
+ "Please mock this method using your python testing framework of choice."
71
+ )
72
+
73
+ def __getattr__(self, name: str) -> object:
74
+ from algopy_testing.context import get_test_context
75
+
76
+ context = get_test_context()
77
+ if not context:
78
+ raise ValueError(
79
+ "Test context is not initialized! Use `with algopy_testing_context()` to access "
80
+ "the context manager."
81
+ )
82
+
83
+ if int(self.id) not in context._asset_data:
84
+ raise ValueError(
85
+ "`algopy.Asset` is not present in the test context! "
86
+ "Use `context.add_asset()` or `context.any_asset()` to add the asset to "
87
+ "your test setup."
88
+ )
89
+
90
+ return_value = context._asset_data[int(self.id)].get(name)
91
+ if return_value is None:
92
+ raise AttributeError(
93
+ f"The value for '{name}' in the test context is None. "
94
+ f"Make sure to patch the global field '{name}' using your `AlgopyTestContext` "
95
+ "instance."
96
+ )
97
+
98
+ return return_value
99
+
100
+ def __eq__(self, other: object) -> bool:
101
+ if isinstance(other, Asset):
102
+ return self.id == other.id
103
+ return self.id == other
104
+
105
+ def __bool__(self) -> bool:
106
+ return self.id != 0
107
+
108
+ def __hash__(self) -> int:
109
+ return hash(self.id)