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
@@ -0,0 +1,769 @@
1
+ from __future__ import annotations
2
+
3
+ import secrets
4
+ import string
5
+ from contextlib import contextmanager
6
+ from contextvars import ContextVar
7
+ from dataclasses import dataclass
8
+
9
+ # Define the union type
10
+ from typing import TYPE_CHECKING, TypeVar, Unpack, cast
11
+
12
+ import algosdk
13
+
14
+ from algopy_testing.constants import DEFAULT_GLOBAL_GENESIS_HASH, MAX_BYTES_SIZE, MAX_UINT64
15
+ from algopy_testing.models.account import AccountFields
16
+ from algopy_testing.models.application import ApplicationFields
17
+ from algopy_testing.models.asset import AssetFields
18
+ from algopy_testing.models.global_values import GlobalFields
19
+ from algopy_testing.models.txn import TxnFields
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Generator, Sequence
23
+
24
+ import algopy
25
+
26
+ from algopy_testing.gtxn import (
27
+ AssetTransferFields,
28
+ PaymentFields,
29
+ TransactionFields,
30
+ )
31
+ from algopy_testing.models.transactions import _ApplicationCallFields
32
+
33
+ InnerTransactionResultType = (
34
+ algopy.itxn.InnerTransactionResult
35
+ | algopy.itxn.PaymentInnerTransaction
36
+ | algopy.itxn.KeyRegistrationInnerTransaction
37
+ | algopy.itxn.AssetConfigInnerTransaction
38
+ | algopy.itxn.AssetTransferInnerTransaction
39
+ | algopy.itxn.AssetFreezeInnerTransaction
40
+ | algopy.itxn.ApplicationCallInnerTransaction
41
+ )
42
+
43
+
44
+ T = TypeVar("T")
45
+
46
+
47
+ @dataclass
48
+ class AccountContextData:
49
+ """
50
+ Stores account-related information.
51
+
52
+ Attributes:
53
+ opted_asset_balances (dict[int, algopy.UInt64]): Mapping of asset IDs to balances.
54
+ opted_apps (dict[int, algopy.UInt64]): Mapping of application IDs to instances.
55
+ fields (AccountFields): Additional account fields.
56
+ """
57
+
58
+ opted_asset_balances: dict[algopy.UInt64, algopy.UInt64]
59
+ opted_apps: dict[algopy.UInt64, algopy.Application]
60
+ fields: AccountFields
61
+
62
+
63
+ @dataclass
64
+ class ContractContextData:
65
+ """
66
+ Stores contract-related information.
67
+
68
+ Attributes:
69
+ contract (algopy.Contract | algopy.ARC4Contract): Contract instance.
70
+ app_id (algopy.UInt64): Application ID.
71
+ """
72
+
73
+ contract: algopy.Contract | algopy.ARC4Contract
74
+ app_id: algopy.UInt64
75
+
76
+
77
+ class AlgopyTestContext:
78
+ def __init__(
79
+ self,
80
+ *,
81
+ default_creator: algopy.Account | None = None,
82
+ default_application: algopy.Application | None = None,
83
+ ) -> None:
84
+ import algopy
85
+
86
+ self._asset_id = iter(range(1001, 2**64))
87
+ self._app_id = iter(range(1001, 2**64))
88
+
89
+ self._contracts: list[algopy.Contract | algopy.ARC4Contract] = []
90
+ self._txn_fields: TxnFields = {}
91
+ self._gtxns: list[algopy.gtxn.TransactionBase] = []
92
+ self._active_transaction_index: int | None = None
93
+ self._account_data: dict[str, AccountContextData] = {}
94
+ self._application_data: dict[int, ApplicationFields] = {}
95
+ self._asset_data: dict[int, AssetFields] = {}
96
+ self._inner_transaction_groups: list[Sequence[algopy.itxn.InnerTransactionResult]] = []
97
+
98
+ default_application_address = (
99
+ self.any_account() if default_application is None else default_application.address
100
+ )
101
+ default_application_creator = default_creator or algopy.Account(
102
+ algosdk.account.generate_account()[1]
103
+ )
104
+ self._global_fields: GlobalFields = {
105
+ "min_txn_fee": algopy.UInt64(algosdk.constants.MIN_TXN_FEE),
106
+ "min_balance": algopy.UInt64(100_000),
107
+ "max_txn_life": algopy.UInt64(1_000),
108
+ "zero_address": algopy.Account(algosdk.constants.ZERO_ADDRESS),
109
+ "creator_address": default_application_creator,
110
+ "current_application_address": default_application_address,
111
+ "asset_create_min_balance": algopy.UInt64(100_000),
112
+ "asset_opt_in_min_balance": algopy.UInt64(10_000),
113
+ "genesis_hash": algopy.Bytes(DEFAULT_GLOBAL_GENESIS_HASH),
114
+ }
115
+
116
+ self.logs: list[str] = []
117
+ self.default_creator = default_application_creator
118
+ self.default_application = default_application or self.any_application(
119
+ address=default_application_address
120
+ )
121
+
122
+ def patch_global_fields(self, **global_fields: Unpack[GlobalFields]) -> None:
123
+ """
124
+ Patch 'Global' fields in the test context.
125
+
126
+ Args:
127
+ **global_fields: Key-value pairs for global fields.
128
+
129
+ Raises:
130
+ AttributeError: If a key is invalid.
131
+ """
132
+ invalid_keys = global_fields.keys() - GlobalFields.__annotations__.keys()
133
+
134
+ if invalid_keys:
135
+ raise AttributeError(
136
+ f"Invalid field(s) found during patch for `Global`: {', '.join(invalid_keys)}"
137
+ )
138
+
139
+ self._global_fields.update(global_fields)
140
+
141
+ def patch_txn_fields(self, **txn_fields: Unpack[TxnFields]) -> None:
142
+ """
143
+ Patch 'algopy.Txn' fields in the test context.
144
+
145
+ Args:
146
+ **txn_fields: Key-value pairs for transaction fields.
147
+
148
+ Raises:
149
+ AttributeError: If a key is invalid.
150
+ """
151
+ invalid_keys = txn_fields.keys() - TxnFields.__annotations__.keys()
152
+ if invalid_keys:
153
+ raise AttributeError(
154
+ f"Invalid field(s) found during patch for `Txn`: {', '.join(invalid_keys)}"
155
+ )
156
+
157
+ self._txn_fields.update(txn_fields)
158
+
159
+ def get_account(self, address: str) -> algopy.Account:
160
+ """
161
+ Retrieve an account by address.
162
+
163
+ Args:
164
+ address (str): Account address.
165
+
166
+ Returns:
167
+ algopy.Account: The account associated with the address.
168
+ """
169
+ import algopy
170
+
171
+ if address not in self._account_data:
172
+ raise ValueError("Account not found in testing context!")
173
+
174
+ return algopy.Account(address)
175
+
176
+ def get_account_data(self) -> dict[str, AccountContextData]:
177
+ """
178
+ Retrieve all account data.
179
+
180
+ Returns:
181
+ dict[str, AccountContextData]: The account data.
182
+ """
183
+ return self._account_data
184
+
185
+ def get_asset_data(self) -> dict[int, AssetFields]:
186
+ """
187
+ Retrieve all asset data.
188
+
189
+ Returns:
190
+ dict[int, AssetFields]: The asset data.
191
+ """
192
+ return self._asset_data
193
+
194
+ def get_application_data(self) -> dict[int, ApplicationFields]:
195
+ """
196
+ Retrieve all application data.
197
+
198
+ Returns:
199
+ dict[int, ApplicationFields]: The application data.
200
+ """
201
+ return self._application_data
202
+
203
+ def get_contracts(self) -> list[algopy.Contract | algopy.ARC4Contract]:
204
+ """
205
+ Retrieve all contracts.
206
+
207
+ Returns:
208
+ list[algopy.Contract | algopy.ARC4Contract]: The contracts.
209
+ """
210
+ return self._contracts
211
+
212
+ def update_account(self, address: str, **account_fields: Unpack[AccountFields]) -> None:
213
+ """
214
+ Update an existing account.
215
+
216
+ Args:
217
+ address (str): Account address.
218
+ **account_fields: New account data.
219
+
220
+ Raises:
221
+ TypeError: If the provided object is not an instance of `Account`.
222
+ """
223
+
224
+ if address not in self._account_data:
225
+ raise ValueError("Account not found")
226
+
227
+ self._account_data[address].fields.update(account_fields)
228
+
229
+ def get_opted_asset_balance(
230
+ self, account: algopy.Account, asset_id: algopy.UInt64
231
+ ) -> algopy.UInt64 | None:
232
+ """
233
+ Retrieve the opted asset balance for an account and asset ID.
234
+
235
+ Args:
236
+ account (algopy.Account): Account to retrieve the balance for.
237
+ asset_id (algopy.UInt64): Asset ID.
238
+
239
+ Returns:
240
+ algopy.UInt64 | None: The opted asset balance or None if not opted in.
241
+ """
242
+
243
+ response = self._account_data.get(
244
+ str(account),
245
+ AccountContextData(fields=AccountFields(), opted_asset_balances={}, opted_apps={}),
246
+ ).opted_asset_balances.get(asset_id, None)
247
+
248
+ return response
249
+
250
+ def get_asset(self, asset_id: algopy.UInt64 | int) -> algopy.Asset:
251
+ """
252
+ Retrieve an asset by ID.
253
+
254
+ Args:
255
+ asset_id (int): Asset ID.
256
+
257
+ Returns:
258
+ algopy.Asset: The asset associated with the ID.
259
+ """
260
+ import algopy
261
+
262
+ asset_id = int(asset_id) if isinstance(asset_id, algopy.UInt64) else asset_id
263
+
264
+ if asset_id not in self._asset_data:
265
+ raise ValueError("Asset not found in testing context!")
266
+
267
+ return algopy.Asset(asset_id)
268
+
269
+ def update_asset(self, asset_id: int, **asset_fields: Unpack[AssetFields]) -> None:
270
+ """
271
+ Update an existing asset.
272
+
273
+ Args:
274
+ asset_id (int): Asset ID.
275
+ **asset_fields: New asset data.
276
+ """
277
+ if asset_id not in self._asset_data:
278
+ raise ValueError("Asset not found in testing context!")
279
+
280
+ self._asset_data[asset_id].update(asset_fields)
281
+
282
+ def get_application(self, app_id: algopy.UInt64 | int) -> algopy.Application:
283
+ """
284
+ Retrieve an application by ID.
285
+
286
+ Args:
287
+ app_id (int): Application ID.
288
+
289
+ Returns:
290
+ algopy.Application: The application associated with the ID.
291
+ """
292
+ import algopy
293
+
294
+ app_id = int(app_id) if isinstance(app_id, algopy.UInt64) else app_id
295
+
296
+ if app_id not in self._application_data:
297
+ raise ValueError("Application not found in testing context!")
298
+
299
+ return algopy.Application(app_id)
300
+
301
+ def update_application(
302
+ self, app_id: int, **application_fields: Unpack[ApplicationFields]
303
+ ) -> None:
304
+ """
305
+ Update an existing application.
306
+
307
+ Args:
308
+ app_id (int): Application ID.
309
+ **application_fields: New application data.
310
+ """
311
+ if app_id not in self._application_data:
312
+ raise ValueError("Application not found in testing context!")
313
+
314
+ self._application_data[app_id].update(application_fields)
315
+
316
+ def _add_contract(
317
+ self,
318
+ contract: algopy.Contract | algopy.ARC4Contract,
319
+ ) -> None:
320
+ """
321
+ Add a contract to the context.
322
+
323
+ Args:
324
+ contract (algopy.Contract | algopy.ARC4Contract): The contract to add.
325
+ """
326
+ self._contracts.append(contract)
327
+
328
+ def _append_inner_transaction_group(
329
+ self,
330
+ itxn: Sequence[InnerTransactionResultType],
331
+ ) -> None:
332
+ """
333
+ Append a group of inner transactions to the context.
334
+
335
+ Args:
336
+ itxn (Sequence[InnerTransactionResultType]): The group of inner transactions to append.
337
+ """
338
+ import algopy.itxn
339
+
340
+ self._inner_transaction_groups.append(cast(list[algopy.itxn.InnerTransactionResult], itxn))
341
+
342
+ def get_last_inner_transaction_group(self) -> Sequence[algopy.itxn.InnerTransactionResult]:
343
+ """
344
+ Retrieve the last group of inner transactions.
345
+
346
+ Returns:
347
+ Sequence[algopy.itxn.InnerTransactionResult]: The last group of inner transactions.
348
+
349
+ Raises:
350
+ ValueError: If no inner transaction groups have been submitted yet.
351
+ """
352
+
353
+ if not self._inner_transaction_groups:
354
+ raise ValueError("No inner transaction groups submitted yet!")
355
+ return self._inner_transaction_groups[-1]
356
+
357
+ def get_last_submitted_inner_transaction(self) -> algopy.itxn.InnerTransactionResult:
358
+ """
359
+ Retrieve the last submitted inner transaction.
360
+
361
+ Returns:
362
+ algopy.itxn.InnerTransactionResult: The last submitted inner transaction.
363
+
364
+ Raises:
365
+ ValueError: If no inner transactions exist in the last inner transaction group.
366
+ """
367
+
368
+ inner_transaction_group = self.get_last_inner_transaction_group()
369
+ if not inner_transaction_group:
370
+ raise ValueError("No inner transactions in the last inner transaction group!")
371
+ return inner_transaction_group[-1]
372
+
373
+ def any_account(
374
+ self,
375
+ address: str | None = None,
376
+ opted_asset_balances: dict[algopy.UInt64, algopy.UInt64] | None = None,
377
+ opted_apps: dict[algopy.UInt64, algopy.Application] | None = None,
378
+ **account_fields: Unpack[AccountFields],
379
+ ) -> algopy.Account:
380
+ """
381
+ Generate and add a new account with a random address.
382
+
383
+ Returns:
384
+ algopy.Account: The newly generated account.
385
+ """
386
+ import algopy
387
+
388
+ if address and not algosdk.encoding.is_valid_address(address):
389
+ raise ValueError("Invalid Algorand address supplied!")
390
+
391
+ if address in self._account_data:
392
+ raise ValueError(
393
+ "Account with such address already exists in testing context! "
394
+ "Use `context.get_account(address)` to retrieve the existing account."
395
+ )
396
+
397
+ for key in account_fields:
398
+ if key not in AccountFields.__annotations__:
399
+ raise AttributeError(f"Invalid field '{key}' for Account")
400
+
401
+ new_account_address = address or algosdk.account.generate_account()[1]
402
+ new_account = algopy.Account(new_account_address)
403
+ new_account_fields = AccountFields(**account_fields)
404
+ new_account_data = AccountContextData(
405
+ fields=new_account_fields,
406
+ opted_asset_balances=opted_asset_balances or {},
407
+ opted_apps=opted_apps or {},
408
+ )
409
+
410
+ self._account_data[new_account_address] = new_account_data
411
+
412
+ return new_account
413
+
414
+ def any_asset(
415
+ self, asset_id: int | None = None, **asset_fields: Unpack[AssetFields]
416
+ ) -> algopy.Asset:
417
+ """
418
+ Generate and add a new asset with a unique ID.
419
+
420
+ Returns:
421
+ algopy.Asset: The newly generated asset.
422
+ """
423
+ import algopy
424
+
425
+ if asset_id and asset_id in self._asset_data:
426
+ raise ValueError("Asset with such ID already exists in testing context!")
427
+
428
+ new_asset = algopy.Asset(asset_id or next(self._asset_id))
429
+ self._asset_data[int(new_asset.id)] = AssetFields(**asset_fields)
430
+ return new_asset
431
+
432
+ def any_application(
433
+ self, **application_fields: Unpack[ApplicationFields]
434
+ ) -> algopy.Application:
435
+ """
436
+ Generate and add a new application with a unique ID.
437
+
438
+ Returns:
439
+ Application: The newly generated application.
440
+ """
441
+ import algopy
442
+
443
+ new_app = algopy.Application(next(self._app_id))
444
+ self._application_data[int(new_app.id)] = ApplicationFields(**application_fields)
445
+ return new_app
446
+
447
+ def set_transaction_group(
448
+ self, gtxn: list[algopy.gtxn.TransactionBase], active_transaction_index: int | None = None
449
+ ) -> None:
450
+ """
451
+ Set the transaction group using a list of transactions.
452
+
453
+ Args:
454
+ gtxn (list[algopy.gtxn.TransactionBase]): List of transactions.
455
+ active_transaction_index (int, optional): Index of the active transaction.
456
+ Defaults to None.
457
+ """
458
+ self._gtxns = gtxn
459
+
460
+ if active_transaction_index is not None:
461
+ self.set_active_transaction_index(active_transaction_index)
462
+
463
+ def add_transactions(
464
+ self,
465
+ gtxns: list[algopy.gtxn.TransactionBase],
466
+ ) -> None:
467
+ """
468
+ Add transactions to the current transaction group.
469
+
470
+ Args:
471
+ gtxns (list[algopy.gtxn.TransactionBase]): List of transactions to add.
472
+
473
+ Raises:
474
+ ValueError: If any transaction is not an instance of TransactionBase or if the total
475
+ number of transactions exceeds the group limit.
476
+ """
477
+ # ensure that new len after append is at most 16 txns in a group
478
+ import algopy.gtxn
479
+
480
+ if not all(isinstance(txn, algopy.gtxn.TransactionBase) for txn in gtxns): # type: ignore[arg-type, unused-ignore]
481
+ raise ValueError("All transactions must be instances of TransactionBase")
482
+
483
+ if len(self._gtxns) + len(gtxns) > algosdk.constants.TX_GROUP_LIMIT:
484
+ raise ValueError(
485
+ f"Transaction group can have at most {algosdk.constants.TX_GROUP_LIMIT} "
486
+ "transactions, as per AVM limits."
487
+ )
488
+
489
+ self._gtxns.extend(gtxns)
490
+
491
+ def get_transaction_group(self) -> list[algopy.gtxn.TransactionBase]:
492
+ """
493
+ Retrieve the current transaction group.
494
+
495
+ Returns:
496
+ list[algopy.gtxn.TransactionBase]: The current transaction group.
497
+ """
498
+ return self._gtxns
499
+
500
+ def set_active_transaction_index(self, index: int) -> None:
501
+ """
502
+ Set the index of the active transaction.
503
+
504
+ Args:
505
+ index (int): The index of the active transaction.
506
+ """
507
+ self._active_transaction_index = index
508
+
509
+ def get_active_transaction(
510
+ self,
511
+ ) -> algopy.gtxn.Transaction | None:
512
+ """
513
+ Retrieve the active transaction of a specified type.
514
+
515
+ Args:
516
+ _txn_type (type[T]): The type of the active transaction.
517
+
518
+ Returns:
519
+ T | None: The active transaction if it exists, otherwise None.
520
+ """
521
+ import algopy
522
+
523
+ if self._active_transaction_index is None:
524
+ return None
525
+ active_txn = self._gtxns[self._active_transaction_index]
526
+ return cast(algopy.gtxn.Transaction, active_txn) if active_txn else None
527
+
528
+ def any_uint64(self, min_value: int = 0, max_value: int = MAX_UINT64) -> algopy.UInt64:
529
+ """
530
+ Generate a random UInt64 value within a specified range.
531
+
532
+ Args:
533
+ min_value (int, optional): Minimum value. Defaults to 0.
534
+ max_value (int, optional): Maximum value. Defaults to MAX_UINT64.
535
+
536
+ Returns:
537
+ algopy.UInt64: The randomly generated UInt64 value.
538
+
539
+ Raises:
540
+ ValueError: If `max_value` exceeds MAX_UINT64 or `min_value` exceeds `max_value`.
541
+ """
542
+ import algopy
543
+
544
+ if max_value > MAX_UINT64:
545
+ raise ValueError("max_value must be less than or equal to MAX_UINT64")
546
+ if min_value > max_value:
547
+ raise ValueError("min_value must be less than or equal to max_value")
548
+
549
+ random_value = secrets.randbelow(max_value - min_value) + min_value
550
+ return algopy.UInt64(random_value)
551
+
552
+ def any_bytes(self, length: int = MAX_BYTES_SIZE) -> algopy.Bytes:
553
+ """
554
+ Generate a random byte sequence of a specified length.
555
+
556
+ Args:
557
+ length (int, optional): Length of the byte sequence. Defaults to MAX_BYTES_SIZE.
558
+
559
+ Returns:
560
+ algopy.Bytes: The randomly generated byte sequence.
561
+ """
562
+ import algopy
563
+
564
+ return algopy.Bytes(secrets.token_bytes(length))
565
+
566
+ def any_string(self, length: int = MAX_BYTES_SIZE) -> algopy.String:
567
+ """
568
+ Generate a random string of a specified length.
569
+ """
570
+ import algopy
571
+
572
+ return algopy.String(
573
+ "".join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(length))
574
+ )
575
+
576
+ def any_app_call_txn( # noqa: PLR0913
577
+ self,
578
+ group_index: int,
579
+ app_args: Sequence[algopy.Bytes] = [],
580
+ accounts: Sequence[algopy.Account] = [],
581
+ assets: Sequence[algopy.Asset] = [],
582
+ apps: Sequence[algopy.Application] = [],
583
+ approval_program_pages: Sequence[algopy.Bytes] = [],
584
+ clear_state_program_pages: Sequence[algopy.Bytes] = [],
585
+ **kwargs: Unpack[_ApplicationCallFields],
586
+ ) -> algopy.gtxn.ApplicationCallTransaction:
587
+ """
588
+ Generate a new application call transaction with specified fields.
589
+
590
+ Args:
591
+ group_index (int): Position index in the transaction group.
592
+ **kwargs (Unpack[ApplicationCallFields]): Fields to be set in the transaction.
593
+
594
+ Returns:
595
+ algopy.gtxn.ApplicationCallTransaction: The newly generated application
596
+ call transaction.
597
+ """
598
+ import algopy.gtxn
599
+
600
+ callable_params = {
601
+ "app_args": lambda index: app_args[index],
602
+ "accounts": lambda index: accounts[index],
603
+ "assets": lambda index: assets[index],
604
+ "apps": lambda index: apps[index],
605
+ "approval_program_pages": lambda index: approval_program_pages[index],
606
+ "clear_state_program_pages": lambda index: clear_state_program_pages[index],
607
+ }
608
+ new_txn = algopy.gtxn.ApplicationCallTransaction(group_index)
609
+
610
+ for key, value in {**kwargs, **callable_params}.items():
611
+ setattr(new_txn, key, value)
612
+
613
+ return new_txn
614
+
615
+ def any_axfer_txn(
616
+ self, group_index: int, **kwargs: Unpack[AssetTransferFields]
617
+ ) -> algopy.gtxn.AssetTransferTransaction:
618
+ """
619
+ Generate a new asset transfer transaction with specified fields.
620
+
621
+ Args:
622
+ group_index (int): Position index in the transaction group.
623
+ **kwargs (Unpack[AssetTransferFields]): Fields to be set in the transaction.
624
+
625
+ Returns:
626
+ algopy.gtxn.AssetTransferTransaction: The newly generated asset transfer transaction.
627
+ """
628
+ import algopy.gtxn
629
+
630
+ new_txn = algopy.gtxn.AssetTransferTransaction(group_index)
631
+
632
+ for key, value in kwargs.items():
633
+ setattr(new_txn, key, value)
634
+
635
+ return new_txn
636
+
637
+ def any_pay_txn(
638
+ self, group_index: int, **kwargs: Unpack[PaymentFields]
639
+ ) -> algopy.gtxn.PaymentTransaction:
640
+ """
641
+ Generate a new payment transaction with specified fields.
642
+
643
+ Args:
644
+ group_index (int): Position index in the transaction group.
645
+ **kwargs (Unpack[PaymentFields]): Fields to be set in the transaction.
646
+
647
+ Returns:
648
+ algopy.gtxn.PaymentTransaction: The newly generated payment transaction.
649
+ """
650
+ import algopy.gtxn
651
+
652
+ new_txn = algopy.gtxn.PaymentTransaction(group_index)
653
+
654
+ for key, value in kwargs.items():
655
+ setattr(new_txn, key, value)
656
+
657
+ return new_txn
658
+
659
+ def any_transaction( # type: ignore[misc]
660
+ self,
661
+ group_index: int,
662
+ type: algopy.TransactionType, # noqa: A002
663
+ **kwargs: Unpack[TransactionFields],
664
+ ) -> algopy.gtxn.Transaction:
665
+ """
666
+ Generate a new transaction with specified fields.
667
+
668
+ Args:
669
+ group_index (int): Position index in the transaction group.
670
+ type (algopy.TransactionType): Transaction type.
671
+ **kwargs (Unpack[TransactionFields]): Fields to be set in the transaction.
672
+
673
+ Returns:
674
+ algopy.gtxn.Transaction: The newly generated transaction.
675
+ """
676
+ import algopy.gtxn
677
+
678
+ new_txn = algopy.gtxn.Transaction(group_index, type=type) # type: ignore[arg-type, unused-ignore]
679
+
680
+ for key, value in kwargs.items():
681
+ setattr(new_txn, key, value)
682
+
683
+ return new_txn
684
+
685
+ def clear_inner_transaction_groups(self) -> None:
686
+ """
687
+ Clear all inner transactions.
688
+ """
689
+ self._inner_transaction_groups.clear()
690
+
691
+ def clear_transaction_group(self) -> None:
692
+ """
693
+ Clear the transaction group.
694
+ """
695
+ self._gtxns.clear()
696
+
697
+ def clear_accounts(self) -> None:
698
+ """
699
+ Clear all accounts.
700
+ """
701
+ self._account_data.clear()
702
+
703
+ def clear_applications(self) -> None:
704
+ """
705
+ Clear all applications.
706
+ """
707
+ self._application_data.clear()
708
+
709
+ def clear_assets(self) -> None:
710
+ """
711
+ Clear all assets.
712
+ """
713
+ self._asset_data.clear()
714
+
715
+ def clear_logs(self) -> None:
716
+ """
717
+ Clear all logs.
718
+ """
719
+ self.logs.clear()
720
+
721
+ def clear(self) -> None:
722
+ """
723
+ Clear all data, including accounts, applications, assets, inner transactions,
724
+ transaction groups, and logs.
725
+ """
726
+ self.clear_accounts()
727
+ self.clear_applications()
728
+ self.clear_assets()
729
+ self.clear_inner_transaction_groups()
730
+ self.clear_transaction_group()
731
+ self.clear_logs()
732
+
733
+ def reset(self) -> None:
734
+ """
735
+ Reset the test context to its initial state, clearing all data and resetting ID counters.
736
+ """
737
+ self._account_data = {}
738
+ self._application_data = {}
739
+ self._asset_data = {}
740
+ self._inner_transaction_groups = []
741
+ self._gtxns = []
742
+ self._global_fields = {}
743
+ self._txn_fields = {}
744
+ self.logs = []
745
+ self._asset_id = iter(range(1, 2**64))
746
+ self._app_id = iter(range(1, 2**64))
747
+
748
+
749
+ _var: ContextVar[AlgopyTestContext] = ContextVar("_var")
750
+
751
+
752
+ def get_test_context() -> AlgopyTestContext:
753
+ return _var.get()
754
+
755
+
756
+ @contextmanager
757
+ def algopy_testing_context(
758
+ *,
759
+ default_creator: algopy.Account | None = None,
760
+ ) -> Generator[AlgopyTestContext, None, None]:
761
+ token = _var.set(
762
+ AlgopyTestContext(
763
+ default_creator=default_creator,
764
+ )
765
+ )
766
+ try:
767
+ yield _var.get()
768
+ finally:
769
+ _var.reset(token)