algorand-python-testing 0.0.0b1__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 (52) hide show
  1. algopy/__init__.py +58 -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 +55 -0
  8. algopy_testing/arc4.py +1533 -0
  9. algopy_testing/constants.py +22 -0
  10. algopy_testing/context.py +1194 -0
  11. algopy_testing/decorators/__init__.py +0 -0
  12. algopy_testing/decorators/abimethod.py +204 -0
  13. algopy_testing/decorators/baremethod.py +83 -0
  14. algopy_testing/decorators/subroutine.py +9 -0
  15. algopy_testing/enums.py +42 -0
  16. algopy_testing/gtxn.py +261 -0
  17. algopy_testing/itxn.py +665 -0
  18. algopy_testing/models/__init__.py +31 -0
  19. algopy_testing/models/account.py +128 -0
  20. algopy_testing/models/application.py +72 -0
  21. algopy_testing/models/asset.py +109 -0
  22. algopy_testing/models/block.py +34 -0
  23. algopy_testing/models/box.py +158 -0
  24. algopy_testing/models/contract.py +82 -0
  25. algopy_testing/models/gitxn.py +42 -0
  26. algopy_testing/models/global_values.py +72 -0
  27. algopy_testing/models/gtxn.py +56 -0
  28. algopy_testing/models/itxn.py +85 -0
  29. algopy_testing/models/logicsig.py +44 -0
  30. algopy_testing/models/template_variable.py +23 -0
  31. algopy_testing/models/transactions.py +158 -0
  32. algopy_testing/models/txn.py +113 -0
  33. algopy_testing/models/unsigned_builtins.py +36 -0
  34. algopy_testing/op.py +1098 -0
  35. algopy_testing/primitives/__init__.py +6 -0
  36. algopy_testing/primitives/biguint.py +148 -0
  37. algopy_testing/primitives/bytes.py +174 -0
  38. algopy_testing/primitives/string.py +68 -0
  39. algopy_testing/primitives/uint64.py +213 -0
  40. algopy_testing/protocols.py +18 -0
  41. algopy_testing/py.typed +0 -0
  42. algopy_testing/state/__init__.py +4 -0
  43. algopy_testing/state/global_state.py +73 -0
  44. algopy_testing/state/local_state.py +54 -0
  45. algopy_testing/utilities/__init__.py +3 -0
  46. algopy_testing/utilities/budget.py +23 -0
  47. algopy_testing/utilities/log.py +55 -0
  48. algopy_testing/utils.py +249 -0
  49. algorand_python_testing-0.0.0b1.dist-info/METADATA +81 -0
  50. algorand_python_testing-0.0.0b1.dist-info/RECORD +52 -0
  51. algorand_python_testing-0.0.0b1.dist-info/WHEEL +4 -0
  52. algorand_python_testing-0.0.0b1.dist-info/licenses/LICENSE +14 -0
@@ -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_int64
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_int64(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)
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ import algopy
7
+
8
+
9
+ class Block:
10
+ @staticmethod
11
+ def blk_seed(a: algopy.UInt64 | int, /) -> algopy.Bytes:
12
+ from algopy_testing import get_test_context, op
13
+
14
+ context = get_test_context()
15
+
16
+ try:
17
+ index = int(a)
18
+ return op.itob(context._blocks[index]["seed"])
19
+ except KeyError as e:
20
+ raise KeyError(f"Block {a} not set") from e
21
+
22
+ @staticmethod
23
+ def blk_timestamp(a: algopy.UInt64 | int, /) -> algopy.UInt64:
24
+ import algopy
25
+
26
+ from algopy_testing import get_test_context
27
+
28
+ context = get_test_context()
29
+
30
+ try:
31
+ index = int(a)
32
+ return algopy.UInt64(context._blocks[index]["timestamp"])
33
+ except KeyError as e:
34
+ raise KeyError(f"Block {a} not set") from e
@@ -0,0 +1,158 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from algopy_testing.context import get_test_context
6
+
7
+ if TYPE_CHECKING:
8
+ import algopy
9
+
10
+
11
+ class Box:
12
+ @staticmethod
13
+ def create(a: algopy.Bytes | bytes, b: algopy.UInt64 | int, /) -> bool:
14
+ import algopy
15
+
16
+ context = get_test_context()
17
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
18
+ size = int(b)
19
+ if not name_bytes or size > 32768:
20
+ raise ValueError("Invalid box name or size")
21
+ if context.get_box(name_bytes):
22
+ return False
23
+ context.set_box(name_bytes, b"\x00" * size)
24
+ return True
25
+
26
+ @staticmethod
27
+ def delete(a: algopy.Bytes | bytes, /) -> bool:
28
+ import algopy
29
+
30
+ context = get_test_context()
31
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
32
+ if context.get_box(name_bytes):
33
+ context.clear_box(name_bytes)
34
+ return True
35
+ return False
36
+
37
+ @staticmethod
38
+ def extract(
39
+ a: algopy.Bytes | bytes, b: algopy.UInt64 | int, c: algopy.UInt64 | int, /
40
+ ) -> algopy.Bytes:
41
+ import algopy
42
+
43
+ context = get_test_context()
44
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
45
+ start = int(b)
46
+ length = int(c)
47
+ box_content = context.get_box(name_bytes)
48
+ if not box_content:
49
+ raise ValueError("Box does not exist")
50
+ return box_content[start : start + length]
51
+
52
+ @staticmethod
53
+ def get(a: algopy.Bytes | bytes, /) -> tuple[algopy.Bytes, bool]:
54
+ import algopy
55
+
56
+ context = get_test_context()
57
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
58
+ box_content = context.get_box(name_bytes)
59
+ return box_content, bool(box_content)
60
+
61
+ @staticmethod
62
+ def length(a: algopy.Bytes | bytes, /) -> tuple[algopy.UInt64, bool]:
63
+ import algopy
64
+
65
+ context = get_test_context()
66
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
67
+ box_content = context.get_box(name_bytes)
68
+ return algopy.UInt64(len(box_content)), bool(box_content)
69
+
70
+ @staticmethod
71
+ def put(a: algopy.Bytes | bytes, b: algopy.Bytes | bytes, /) -> None:
72
+ import algopy
73
+
74
+ context = get_test_context()
75
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
76
+ content = b.value if isinstance(b, algopy.Bytes) else b
77
+ existing_content = context.get_box(name_bytes)
78
+ if existing_content and len(existing_content) != len(content):
79
+ raise ValueError("New content length does not match existing box length")
80
+ context.set_box(name_bytes, content)
81
+
82
+ @staticmethod
83
+ def replace(
84
+ a: algopy.Bytes | bytes, b: algopy.UInt64 | int, c: algopy.Bytes | bytes, /
85
+ ) -> None:
86
+ import algopy
87
+
88
+ context = get_test_context()
89
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
90
+ start = int(b)
91
+ new_content = c.value if isinstance(c, algopy.Bytes) else c
92
+ box_content = context.get_box(name_bytes)
93
+ if not box_content:
94
+ raise ValueError("Box does not exist")
95
+ if start + len(new_content) > len(box_content):
96
+ raise ValueError("Replacement content exceeds box size")
97
+ updated_content = (
98
+ box_content[:start] + new_content + box_content[start + len(new_content) :]
99
+ )
100
+ context.set_box(name_bytes, updated_content)
101
+
102
+ @staticmethod
103
+ def resize(a: algopy.Bytes | bytes, b: algopy.UInt64 | int, /) -> None:
104
+ import algopy
105
+
106
+ context = get_test_context()
107
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
108
+ new_size = int(b)
109
+ if not name_bytes or new_size > 32768:
110
+ raise ValueError("Invalid box name or size")
111
+ box_content = context.get_box(name_bytes)
112
+ if not box_content:
113
+ raise ValueError("Box does not exist")
114
+ if new_size > len(box_content):
115
+ updated_content = box_content + b"\x00" * (new_size - len(box_content))
116
+ else:
117
+ updated_content = box_content[:new_size]
118
+ context.set_box(name_bytes, updated_content)
119
+
120
+ @staticmethod
121
+ def splice(
122
+ a: algopy.Bytes | bytes,
123
+ b: algopy.UInt64 | int,
124
+ c: algopy.UInt64 | int,
125
+ d: algopy.Bytes | bytes,
126
+ /,
127
+ ) -> None:
128
+ import algopy
129
+
130
+ context = get_test_context()
131
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
132
+ start = int(b)
133
+ delete_count = int(c)
134
+ insert_content = d.value if isinstance(d, algopy.Bytes) else d
135
+ box_content = context.get_box(name_bytes)
136
+
137
+ if not box_content:
138
+ raise ValueError("Box does not exist")
139
+
140
+ if start > len(box_content):
141
+ raise ValueError("Start index exceeds box size")
142
+
143
+ # Calculate the end index for deletion
144
+ end = min(start + delete_count, len(box_content))
145
+
146
+ # Construct the new content
147
+ new_content = box_content[:start] + insert_content + box_content[end:]
148
+
149
+ # Adjust the size if necessary
150
+ if len(new_content) > len(box_content):
151
+ # Truncate if the new content is too long
152
+ new_content = new_content[: len(box_content)]
153
+ elif len(new_content) < len(box_content):
154
+ # Pad with zeros if the new content is too short
155
+ new_content += b"\x00" * (len(box_content) - len(new_content))
156
+
157
+ # Update the box with the new content
158
+ context.set_box(name_bytes, new_content)
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Any, final
5
+
6
+ if TYPE_CHECKING:
7
+ import algopy
8
+
9
+
10
+ @dataclass
11
+ class StateTotals:
12
+ global_uints: int | None = None
13
+ global_bytes: int | None = None
14
+ local_uints: int | None = None
15
+ local_bytes: int | None = None
16
+
17
+
18
+ class _ContractMeta(type):
19
+ def __call__(cls, *args: Any, **kwargs: dict[str, Any]) -> object:
20
+ from algopy import Contract
21
+
22
+ from algopy_testing.context import get_test_context
23
+
24
+ context = get_test_context()
25
+ instance = super().__call__(*args, **kwargs)
26
+
27
+ if context and isinstance(instance, Contract):
28
+ context._add_contract(instance)
29
+
30
+ return instance
31
+
32
+
33
+ class Contract(metaclass=_ContractMeta):
34
+ """Base class for an Algorand Smart Contract"""
35
+
36
+ _name: str
37
+ _scratch_slots: Any | None
38
+ _state_totals: StateTotals | None
39
+
40
+ def __init_subclass__(
41
+ cls,
42
+ *,
43
+ name: str | None = None,
44
+ scratch_slots: (
45
+ algopy.UInt64 | tuple[int | algopy.UInt64, ...] | list[int | algopy.UInt64] | None
46
+ ) = None,
47
+ state_totals: StateTotals | None = None,
48
+ ):
49
+ cls._name = name or cls.__name__
50
+ cls._scratch_slots = scratch_slots
51
+ cls._state_totals = state_totals
52
+
53
+ def approval_program(self) -> algopy.UInt64 | bool:
54
+ raise NotImplementedError("`approval_program` is not implemented.")
55
+
56
+ def clear_state_program(self) -> algopy.UInt64 | bool:
57
+ raise NotImplementedError("`clear_state_program` is not implemented.")
58
+
59
+ def __hash__(self) -> int:
60
+ return hash(self._name)
61
+
62
+ def __getattribute__(self, name: str) -> Any:
63
+ attr = super().__getattribute__(name)
64
+ if callable(attr):
65
+
66
+ def wrapper(*args: Any, **kwargs: dict[str, Any]) -> Any:
67
+ return attr(*args, **kwargs)
68
+
69
+ return wrapper
70
+ return attr
71
+
72
+
73
+ class ARC4Contract(Contract):
74
+ @final
75
+ def approval_program(self) -> algopy.UInt64 | bool:
76
+ raise NotImplementedError(
77
+ "`approval_program` is not implemented. To test ARC4 specific logic, "
78
+ "refer to direct calls to ARC4 methods."
79
+ )
80
+
81
+ def clear_state_program(self) -> algopy.UInt64 | bool:
82
+ return True
@@ -0,0 +1,42 @@
1
+ import typing
2
+ from collections.abc import Callable, Sequence
3
+
4
+
5
+ class _GITxn:
6
+ def __getattr__(self, name: str) -> Callable[[int], typing.Any]:
7
+ from algopy_testing.context import get_test_context
8
+
9
+ context = get_test_context()
10
+ if not context:
11
+ raise ValueError(
12
+ "Test context is not initialized! Use `with algopy_testing_context()` to access "
13
+ "the context manager."
14
+ )
15
+ if not context._inner_transaction_groups:
16
+ raise ValueError(
17
+ "No inner transaction found in the context! Use `with algopy_testing_context()` "
18
+ "to access the context manager."
19
+ )
20
+ last_itxn_group = context._inner_transaction_groups[-1]
21
+
22
+ if not last_itxn_group:
23
+ raise ValueError("No inner transaction found in the testing context!")
24
+
25
+ return lambda index: self._get_value(last_itxn_group, name, index)
26
+
27
+ # TODO: refine mapping
28
+ def _map_fields(self, name: str) -> str:
29
+ field_mapping = {"type": "type_bytes", "type_enum": "type", "application_args": "app_args"}
30
+ return field_mapping.get(name, name)
31
+
32
+ def _get_value(self, itxn_group: Sequence[typing.Any], name: str, index: int) -> object:
33
+ if index >= len(itxn_group):
34
+ raise IndexError("Transaction index out of range")
35
+ itxn = itxn_group[index]
36
+ value = getattr(itxn, self._map_fields(name))
37
+ if value is None:
38
+ raise ValueError(f"'{name}' is not defined for {type(itxn).__name__}")
39
+ return value
40
+
41
+
42
+ GITxn = _GITxn()
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import typing
5
+ from dataclasses import dataclass
6
+ from typing import TypedDict, TypeVar
7
+
8
+ import algosdk
9
+
10
+ if typing.TYPE_CHECKING:
11
+ from collections.abc import Callable
12
+
13
+ import algopy
14
+
15
+ T = TypeVar("T")
16
+
17
+
18
+ class GlobalFields(TypedDict, total=False):
19
+ min_txn_fee: algopy.UInt64
20
+ min_balance: algopy.UInt64
21
+ max_txn_life: algopy.UInt64
22
+ zero_address: algopy.Account
23
+ group_size: algopy.UInt64
24
+ logic_sig_version: algopy.UInt64
25
+ round: algopy.UInt64
26
+ latest_timestamp: algopy.UInt64
27
+ current_application_id: algopy.Application
28
+ creator_address: algopy.Account
29
+ current_application_address: algopy.Account
30
+ group_id: algopy.Bytes
31
+ caller_application_id: algopy.Application
32
+ caller_application_address: algopy.Account
33
+ asset_create_min_balance: algopy.UInt64
34
+ asset_opt_in_min_balance: algopy.UInt64
35
+ genesis_hash: algopy.Bytes
36
+ opcode_budget: Callable[[], int]
37
+
38
+
39
+ @dataclass
40
+ class _Global:
41
+ def __getattr__(self, name: str) -> typing.Any:
42
+ import algopy
43
+
44
+ from algopy_testing.context import get_test_context
45
+
46
+ context = get_test_context()
47
+ if not context:
48
+ raise ValueError(
49
+ "Test context is not initialized! Use `with algopy_testing_context()` to access "
50
+ "the context manager."
51
+ )
52
+
53
+ if name == "latest_timestamp" and context._global_fields.get(name) is None:
54
+ return algopy.UInt64(int(time.time()))
55
+
56
+ if name == "group_size" and context._global_fields.get(name) is None:
57
+ return algopy.UInt64(len(context.get_transaction_group()))
58
+
59
+ if name == "zero_address" and context._global_fields.get(name) is None:
60
+ return algopy.Account(algosdk.constants.ZERO_ADDRESS)
61
+
62
+ if name not in context._global_fields:
63
+ raise AttributeError(
64
+ f"'algopy.Global' object has no value set for attribute named '{name}'. "
65
+ f"Use `context.patch_global_fields({name}=your_value)` to set the value "
66
+ "in your test setup."
67
+ )
68
+
69
+ return context._global_fields[name] # type: ignore[literal-required]
70
+
71
+
72
+ Global = _Global()