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.
- algopy/__init__.py +58 -0
- algopy/arc4.py +1 -0
- algopy/gtxn.py +1 -0
- algopy/itxn.py +1 -0
- algopy/op.py +1 -0
- algopy/py.typed +0 -0
- algopy_testing/__init__.py +55 -0
- algopy_testing/arc4.py +1533 -0
- algopy_testing/constants.py +22 -0
- algopy_testing/context.py +1194 -0
- algopy_testing/decorators/__init__.py +0 -0
- algopy_testing/decorators/abimethod.py +204 -0
- algopy_testing/decorators/baremethod.py +83 -0
- algopy_testing/decorators/subroutine.py +9 -0
- algopy_testing/enums.py +42 -0
- algopy_testing/gtxn.py +261 -0
- algopy_testing/itxn.py +665 -0
- algopy_testing/models/__init__.py +31 -0
- algopy_testing/models/account.py +128 -0
- algopy_testing/models/application.py +72 -0
- algopy_testing/models/asset.py +109 -0
- algopy_testing/models/block.py +34 -0
- algopy_testing/models/box.py +158 -0
- algopy_testing/models/contract.py +82 -0
- algopy_testing/models/gitxn.py +42 -0
- algopy_testing/models/global_values.py +72 -0
- algopy_testing/models/gtxn.py +56 -0
- algopy_testing/models/itxn.py +85 -0
- algopy_testing/models/logicsig.py +44 -0
- algopy_testing/models/template_variable.py +23 -0
- algopy_testing/models/transactions.py +158 -0
- algopy_testing/models/txn.py +113 -0
- algopy_testing/models/unsigned_builtins.py +36 -0
- algopy_testing/op.py +1098 -0
- algopy_testing/primitives/__init__.py +6 -0
- algopy_testing/primitives/biguint.py +148 -0
- algopy_testing/primitives/bytes.py +174 -0
- algopy_testing/primitives/string.py +68 -0
- algopy_testing/primitives/uint64.py +213 -0
- algopy_testing/protocols.py +18 -0
- algopy_testing/py.typed +0 -0
- algopy_testing/state/__init__.py +4 -0
- algopy_testing/state/global_state.py +73 -0
- algopy_testing/state/local_state.py +54 -0
- algopy_testing/utilities/__init__.py +3 -0
- algopy_testing/utilities/budget.py +23 -0
- algopy_testing/utilities/log.py +55 -0
- algopy_testing/utils.py +249 -0
- algorand_python_testing-0.0.0b1.dist-info/METADATA +81 -0
- algorand_python_testing-0.0.0b1.dist-info/RECORD +52 -0
- algorand_python_testing-0.0.0b1.dist-info/WHEEL +4 -0
- algorand_python_testing-0.0.0b1.dist-info/licenses/LICENSE +14 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
from typing import cast, overload
|
|
5
|
+
|
|
6
|
+
_T = typing.TypeVar("_T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GlobalState(typing.Generic[_T]):
|
|
10
|
+
@overload
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
type_: type[_T],
|
|
14
|
+
/,
|
|
15
|
+
*,
|
|
16
|
+
key: bytes | str = "",
|
|
17
|
+
description: str = "",
|
|
18
|
+
) -> None: ...
|
|
19
|
+
|
|
20
|
+
@overload
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
initial_value: _T,
|
|
24
|
+
/,
|
|
25
|
+
*,
|
|
26
|
+
key: bytes | str = "",
|
|
27
|
+
description: str = "",
|
|
28
|
+
) -> None: ...
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
type_or_value: type[_T] | _T,
|
|
33
|
+
/,
|
|
34
|
+
*,
|
|
35
|
+
key: bytes | str = "",
|
|
36
|
+
description: str = "",
|
|
37
|
+
) -> None:
|
|
38
|
+
if isinstance(type_or_value, type):
|
|
39
|
+
self.type_ = type_or_value
|
|
40
|
+
self._value: _T | None = None
|
|
41
|
+
else:
|
|
42
|
+
self.type_ = type(type_or_value)
|
|
43
|
+
self._value = type_or_value
|
|
44
|
+
|
|
45
|
+
self.key = key
|
|
46
|
+
self.description = description
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def value(self) -> _T:
|
|
50
|
+
if self._value is None:
|
|
51
|
+
raise ValueError("Value is not set")
|
|
52
|
+
return self._value
|
|
53
|
+
|
|
54
|
+
@value.setter
|
|
55
|
+
def value(self, value: _T) -> None:
|
|
56
|
+
self._value = value
|
|
57
|
+
|
|
58
|
+
@value.deleter
|
|
59
|
+
def value(self) -> None:
|
|
60
|
+
self._value = None
|
|
61
|
+
|
|
62
|
+
def __bool__(self) -> bool:
|
|
63
|
+
return self._value is not None
|
|
64
|
+
|
|
65
|
+
def get(self, default: _T | None = None) -> _T:
|
|
66
|
+
if self._value is not None:
|
|
67
|
+
return self._value
|
|
68
|
+
if default is not None:
|
|
69
|
+
return default
|
|
70
|
+
return cast(_T, self.type_())
|
|
71
|
+
|
|
72
|
+
def maybe(self) -> tuple[_T | None, bool]:
|
|
73
|
+
return self._value, self._value is not None
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import algopy
|
|
8
|
+
|
|
9
|
+
_T = typing.TypeVar("_T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LocalState(typing.Generic[_T]):
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
type_: type[_T],
|
|
16
|
+
/,
|
|
17
|
+
*,
|
|
18
|
+
key: bytes | str = "",
|
|
19
|
+
description: str = "",
|
|
20
|
+
) -> None:
|
|
21
|
+
self.type_ = type_
|
|
22
|
+
self.key = key
|
|
23
|
+
self.description = description
|
|
24
|
+
self._state: dict[object, _T] = {}
|
|
25
|
+
|
|
26
|
+
def _validate_local_state_key(self, key: algopy.Account | algopy.UInt64 | int) -> None:
|
|
27
|
+
from algopy import Account, UInt64
|
|
28
|
+
|
|
29
|
+
if not isinstance(key, Account | UInt64 | int):
|
|
30
|
+
raise TypeError(f"Invalid key type {type(key)} for LocalState")
|
|
31
|
+
|
|
32
|
+
def __setitem__(self, key: algopy.Account | algopy.UInt64 | int, value: _T) -> None:
|
|
33
|
+
self._validate_local_state_key(key)
|
|
34
|
+
self._state[key] = value
|
|
35
|
+
|
|
36
|
+
def __getitem__(self, key: algopy.Account | algopy.UInt64 | int) -> _T:
|
|
37
|
+
self._validate_local_state_key(key)
|
|
38
|
+
return self._state[key]
|
|
39
|
+
|
|
40
|
+
def __delitem__(self, key: algopy.Account | algopy.UInt64 | int) -> None:
|
|
41
|
+
self._validate_local_state_key(key)
|
|
42
|
+
del self._state[key]
|
|
43
|
+
|
|
44
|
+
def __contains__(self, key: algopy.Account | algopy.UInt64 | int) -> bool:
|
|
45
|
+
self._validate_local_state_key(key)
|
|
46
|
+
return key in self._state
|
|
47
|
+
|
|
48
|
+
def get(self, key: algopy.Account | algopy.UInt64 | int, default: _T | None = None) -> _T:
|
|
49
|
+
self._validate_local_state_key(key)
|
|
50
|
+
return self._state.get(key, default if default is not None else self.type_())
|
|
51
|
+
|
|
52
|
+
def maybe(self, key: algopy.Account | algopy.UInt64 | int) -> tuple[object, bool]:
|
|
53
|
+
self._validate_local_state_key(key)
|
|
54
|
+
return self._state.get(key), key in self._state
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from algopy_testing import UInt64
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OpUpFeeSource(UInt64):
|
|
7
|
+
"""Defines the source of fees for the OpUp utility."""
|
|
8
|
+
|
|
9
|
+
GroupCredit: OpUpFeeSource
|
|
10
|
+
AppAccount: OpUpFeeSource
|
|
11
|
+
Any: OpUpFeeSource
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
OpUpFeeSource.GroupCredit = OpUpFeeSource(0)
|
|
15
|
+
OpUpFeeSource.AppAccount = OpUpFeeSource(1)
|
|
16
|
+
OpUpFeeSource.Any = OpUpFeeSource(2)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def ensure_budget(
|
|
20
|
+
required_budget: UInt64 | int, # noqa: ARG001
|
|
21
|
+
fee_source: OpUpFeeSource = OpUpFeeSource.GroupCredit, # noqa: ARG001
|
|
22
|
+
) -> None:
|
|
23
|
+
pass
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from algopy_testing.primitives.bytes import Bytes
|
|
2
|
+
from algopy_testing.primitives.uint64 import UInt64
|
|
3
|
+
from algopy_testing.protocols import BytesBacked
|
|
4
|
+
from algopy_testing.utils import int_to_bytes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def log( # noqa: C901, PLR0912
|
|
8
|
+
*args: UInt64 | Bytes | BytesBacked | str | bytes | int,
|
|
9
|
+
sep: Bytes | bytes | str = b"",
|
|
10
|
+
) -> None:
|
|
11
|
+
"""Concatenates and logs supplied args as a single bytes value.
|
|
12
|
+
|
|
13
|
+
UInt64 args are converted to bytes and each argument is separated by `sep`.
|
|
14
|
+
Literal `str` values will be encoded as UTF8.
|
|
15
|
+
"""
|
|
16
|
+
import algopy
|
|
17
|
+
|
|
18
|
+
from algopy_testing.context import get_test_context
|
|
19
|
+
|
|
20
|
+
context = get_test_context()
|
|
21
|
+
logs: list[bytes] = []
|
|
22
|
+
|
|
23
|
+
for arg in args:
|
|
24
|
+
if isinstance(arg, UInt64):
|
|
25
|
+
logs.append(int_to_bytes(arg.value))
|
|
26
|
+
elif isinstance(arg, Bytes):
|
|
27
|
+
logs.append(arg.value)
|
|
28
|
+
elif isinstance(arg, str):
|
|
29
|
+
logs.append(arg.encode("utf8"))
|
|
30
|
+
elif isinstance(arg, bytes):
|
|
31
|
+
logs.append(arg)
|
|
32
|
+
elif isinstance(arg, int):
|
|
33
|
+
logs.append(int_to_bytes(arg))
|
|
34
|
+
else:
|
|
35
|
+
logs.append(arg.bytes.value)
|
|
36
|
+
|
|
37
|
+
separator = b""
|
|
38
|
+
if isinstance(sep, Bytes):
|
|
39
|
+
separator = sep.value
|
|
40
|
+
elif isinstance(sep, str):
|
|
41
|
+
separator = sep.encode("utf8")
|
|
42
|
+
else:
|
|
43
|
+
separator = sep
|
|
44
|
+
|
|
45
|
+
active_txn = context.get_active_transaction()
|
|
46
|
+
if not active_txn:
|
|
47
|
+
raise ValueError("Cannot emit events outside of application call context!")
|
|
48
|
+
if active_txn.type != algopy.TransactionType.ApplicationCall:
|
|
49
|
+
raise ValueError("Cannot emit events outside of application call context!")
|
|
50
|
+
if not active_txn.app_id:
|
|
51
|
+
raise ValueError("Cannot emit event: missing `app_id` in associated call transaction!")
|
|
52
|
+
context.add_application_logs(
|
|
53
|
+
app_id=active_txn.app_id(),
|
|
54
|
+
logs=separator.join(logs),
|
|
55
|
+
)
|
algopy_testing/utils.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import functools
|
|
5
|
+
import secrets
|
|
6
|
+
import typing
|
|
7
|
+
from types import UnionType
|
|
8
|
+
from typing import TYPE_CHECKING, get_args
|
|
9
|
+
|
|
10
|
+
import algosdk
|
|
11
|
+
import algosdk.transaction
|
|
12
|
+
|
|
13
|
+
from algopy_testing import arc4
|
|
14
|
+
from algopy_testing.constants import MAX_BYTES_SIZE, MAX_UINT8, MAX_UINT16, MAX_UINT64, MAX_UINT512
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
import algopy
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def as_int(value: object, *, max: int | None) -> int: # noqa: A002
|
|
21
|
+
"""
|
|
22
|
+
Returns the underlying int value for any numeric type up to UInt512
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
TypeError: If `value` is not a numeric type
|
|
26
|
+
ValueError: If not 0 <= `value` <= max
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from algopy_testing.primitives.biguint import BigUInt
|
|
30
|
+
from algopy_testing.primitives.uint64 import UInt64
|
|
31
|
+
|
|
32
|
+
match value:
|
|
33
|
+
case int(int_value):
|
|
34
|
+
pass
|
|
35
|
+
case UInt64(value=int_value):
|
|
36
|
+
pass
|
|
37
|
+
case BigUInt(value=int_value):
|
|
38
|
+
pass
|
|
39
|
+
case arc4.UIntN():
|
|
40
|
+
int_value = value.native.value
|
|
41
|
+
case arc4.BigUIntN():
|
|
42
|
+
int_value = value.native.value
|
|
43
|
+
# TODO: add arc4 numerics
|
|
44
|
+
case _:
|
|
45
|
+
raise TypeError(f"value must be a numeric type, not {type(value).__name__!r}")
|
|
46
|
+
if int_value < 0:
|
|
47
|
+
raise ValueError(f"expected positive value, got {int_value}")
|
|
48
|
+
if max is not None and int_value > max:
|
|
49
|
+
raise ValueError(f"expected value <= {max}, got: {int_value}")
|
|
50
|
+
return int_value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def as_int8(value: object) -> int:
|
|
54
|
+
return as_int(value, max=MAX_UINT8)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def as_int16(value: object) -> int:
|
|
58
|
+
return as_int(value, max=MAX_UINT16)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def as_int64(value: object) -> int:
|
|
62
|
+
return as_int(value, max=MAX_UINT64)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def as_int512(value: object) -> int:
|
|
66
|
+
return as_int(value, max=MAX_UINT512)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def as_bytes(value: object, *, max_size: int = MAX_BYTES_SIZE) -> bytes:
|
|
70
|
+
"""
|
|
71
|
+
Returns the underlying bytes value for bytes or Bytes type up to 4096
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
TypeError: If `value` is not a bytes type
|
|
75
|
+
ValueError: If not 0 <= `len(value)` <= max_size
|
|
76
|
+
"""
|
|
77
|
+
from algopy_testing.primitives.bytes import Bytes
|
|
78
|
+
|
|
79
|
+
match value:
|
|
80
|
+
case bytes(bytes_value):
|
|
81
|
+
pass
|
|
82
|
+
case Bytes(value=bytes_value):
|
|
83
|
+
pass
|
|
84
|
+
case _:
|
|
85
|
+
raise TypeError(f"value must be a bytes or Bytes type, not {type(value).__name__!r}")
|
|
86
|
+
if len(bytes_value) > max_size:
|
|
87
|
+
raise ValueError(f"expected value length <= {max_size}, got: {len(bytes_value)}")
|
|
88
|
+
return bytes_value
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def as_string(value: object) -> str:
|
|
92
|
+
from algopy_testing.primitives.string import String
|
|
93
|
+
|
|
94
|
+
match value:
|
|
95
|
+
case str(string_value) | String(value=string_value):
|
|
96
|
+
return string_value
|
|
97
|
+
case arc4.String():
|
|
98
|
+
return value.native.value
|
|
99
|
+
case _:
|
|
100
|
+
raise TypeError(f"value must be a string or String type, not {type(value).__name__!r}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def int_to_bytes(x: int, pad_to: int | None = None) -> bytes:
|
|
104
|
+
result = x.to_bytes((x.bit_length() + 7) // 8, "big")
|
|
105
|
+
result = (
|
|
106
|
+
b"\x00" * (pad_to - len(result)) if pad_to is not None and len(result) < pad_to else b""
|
|
107
|
+
) + result
|
|
108
|
+
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def dummy_transaction_id() -> bytes:
|
|
113
|
+
private_key, address = algosdk.account.generate_account()
|
|
114
|
+
|
|
115
|
+
suggested_params = algosdk.transaction.SuggestedParams(fee=1000, first=0, last=1, gh="")
|
|
116
|
+
txn = algosdk.transaction.PaymentTxn(
|
|
117
|
+
sender=address,
|
|
118
|
+
receiver=address,
|
|
119
|
+
amt=1000,
|
|
120
|
+
sp=suggested_params,
|
|
121
|
+
note=secrets.token_bytes(8),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
signed_txn = txn.sign(private_key)
|
|
125
|
+
txn_id = str(signed_txn.transaction.get_txid()).encode("utf-8")
|
|
126
|
+
return txn_id
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class _TransactionStrType(enum.StrEnum):
|
|
130
|
+
PAYMENT = algosdk.constants.PAYMENT_TXN
|
|
131
|
+
KEYREG = algosdk.constants.KEYREG_TXN
|
|
132
|
+
ASSETCONFIG = algosdk.constants.ASSETCONFIG_TXN
|
|
133
|
+
ASSETTRANSFER = algosdk.constants.ASSETTRANSFER_TXN
|
|
134
|
+
ASSETFREEZE = algosdk.constants.ASSETFREEZE_TXN
|
|
135
|
+
APPCALL = algosdk.constants.APPCALL_TXN
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@functools.cache
|
|
139
|
+
def txn_type_to_bytes(txn_type: int) -> algopy.Bytes:
|
|
140
|
+
import algopy
|
|
141
|
+
|
|
142
|
+
match txn_type:
|
|
143
|
+
case algopy.TransactionType.Payment:
|
|
144
|
+
result = _TransactionStrType.PAYMENT
|
|
145
|
+
case algopy.TransactionType.KeyRegistration:
|
|
146
|
+
result = _TransactionStrType.KEYREG
|
|
147
|
+
case algopy.TransactionType.AssetConfig:
|
|
148
|
+
result = _TransactionStrType.ASSETCONFIG
|
|
149
|
+
case algopy.TransactionType.AssetTransfer:
|
|
150
|
+
result = _TransactionStrType.ASSETTRANSFER
|
|
151
|
+
case algopy.TransactionType.AssetFreeze:
|
|
152
|
+
result = _TransactionStrType.ASSETFREEZE
|
|
153
|
+
case algopy.TransactionType.ApplicationCall:
|
|
154
|
+
result = _TransactionStrType.APPCALL
|
|
155
|
+
case _:
|
|
156
|
+
raise ValueError(f"invalid transaction type: {txn_type}")
|
|
157
|
+
|
|
158
|
+
return algopy.Bytes(bytes(result, encoding="utf-8"))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def is_instance(obj: object, class_or_tuple: type | UnionType) -> bool:
|
|
162
|
+
if isinstance(class_or_tuple, UnionType):
|
|
163
|
+
return any(is_instance(obj, arg) for arg in get_args(class_or_tuple))
|
|
164
|
+
|
|
165
|
+
if isinstance(obj, typing._ProtocolMeta): # type: ignore[type-check, unused-ignore]
|
|
166
|
+
return (
|
|
167
|
+
f"{obj.__module__}.{obj.__name__}"
|
|
168
|
+
== f"{class_or_tuple.__module__}.{class_or_tuple.__name__}" # type: ignore[union-attr, unused-ignore]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Manual comparison by module and name
|
|
172
|
+
if (
|
|
173
|
+
hasattr(obj, "__module__")
|
|
174
|
+
and hasattr(obj, "__name__")
|
|
175
|
+
and (
|
|
176
|
+
obj.__module__,
|
|
177
|
+
obj.__name__, # type: ignore[attr-defined, unused-ignore]
|
|
178
|
+
)
|
|
179
|
+
== (
|
|
180
|
+
class_or_tuple.__module__,
|
|
181
|
+
class_or_tuple.__name__,
|
|
182
|
+
)
|
|
183
|
+
):
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
return isinstance(obj, class_or_tuple)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def abi_type_name_for_arg( # noqa: PLR0912, C901, PLR0911
|
|
190
|
+
*, arg: object, is_return_type: bool = False
|
|
191
|
+
) -> str:
|
|
192
|
+
"""
|
|
193
|
+
Returns the ABI type name for the given argument. Especially convenient for use with
|
|
194
|
+
algosdk to generate method signatures
|
|
195
|
+
"""
|
|
196
|
+
import algopy
|
|
197
|
+
|
|
198
|
+
if is_instance(arg, algopy.arc4.String | algopy.String | str):
|
|
199
|
+
return "string"
|
|
200
|
+
if is_instance(arg, algopy.arc4.Bool | bool):
|
|
201
|
+
return "bool"
|
|
202
|
+
if is_instance(arg, algopy.BigUInt):
|
|
203
|
+
return "uint512"
|
|
204
|
+
if is_instance(arg, algopy.UInt64):
|
|
205
|
+
return "uint64"
|
|
206
|
+
if isinstance(arg, int):
|
|
207
|
+
return "uint64" if arg <= MAX_UINT64 else "uint512"
|
|
208
|
+
if is_instance(arg, algopy.Bytes | bytes):
|
|
209
|
+
return "byte[]"
|
|
210
|
+
if is_instance(arg, algopy.arc4.Address):
|
|
211
|
+
return "address"
|
|
212
|
+
if is_instance(arg, algopy.Asset):
|
|
213
|
+
return "uint64" if is_return_type else "asset"
|
|
214
|
+
if is_instance(arg, algopy.Account):
|
|
215
|
+
return "uint64" if is_return_type else "account"
|
|
216
|
+
if is_instance(arg, algopy.Application):
|
|
217
|
+
return "uint64" if is_return_type else "application"
|
|
218
|
+
if is_instance(arg, algopy.arc4.UIntN):
|
|
219
|
+
return "uint" + str(arg._bit_size) # type: ignore[attr-defined]
|
|
220
|
+
if is_instance(arg, algopy.arc4.BigUIntN):
|
|
221
|
+
return "uint" + str(arg._bit_size) # type: ignore[attr-defined]
|
|
222
|
+
if is_instance(arg, algopy.arc4.UFixedNxM):
|
|
223
|
+
return f"ufixed{arg._n}x{arg._m}" # type: ignore[attr-defined]
|
|
224
|
+
if is_instance(arg, algopy.arc4.BigUFixedNxM):
|
|
225
|
+
return f"ufixed{arg._n}x{arg._m}" # type: ignore[attr-defined]
|
|
226
|
+
if is_instance(arg, algopy.arc4.StaticArray):
|
|
227
|
+
return f"{abi_type_name_for_arg(arg=arg[0], # type: ignore[index]
|
|
228
|
+
is_return_type=is_return_type)}[{arg.length.value}]" # type: ignore[attr-defined]
|
|
229
|
+
if is_instance(arg, algopy.arc4.DynamicArray):
|
|
230
|
+
return f"{abi_type_name_for_arg(arg=arg[0], # type: ignore[index]
|
|
231
|
+
is_return_type=is_return_type)}[]"
|
|
232
|
+
if isinstance(arg, tuple):
|
|
233
|
+
return f"({','.join(abi_type_name_for_arg(arg=a,
|
|
234
|
+
is_return_type=is_return_type) for a in arg)})"
|
|
235
|
+
raise ValueError(f"Unsupported type {type(arg)}")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def abi_return_type_annotation_for_arg(arg: object) -> str:
|
|
239
|
+
"""
|
|
240
|
+
Returns the ABI type name for the given argument. Especially convenient for use with
|
|
241
|
+
algosdk to generate method signatures
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
return abi_type_name_for_arg(arg=arg, is_return_type=True)
|
|
246
|
+
except ValueError:
|
|
247
|
+
if arg is None:
|
|
248
|
+
return "void"
|
|
249
|
+
raise
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: algorand-python-testing
|
|
3
|
+
Version: 0.0.0b1
|
|
4
|
+
Summary: Algorand Python testing library
|
|
5
|
+
Project-URL: Documentation, https://github.com/algorandfoundation/puya/tree/main/algopy_testing#README.md
|
|
6
|
+
Project-URL: Issues, https://github.com/algorandfoundation/puya/issues
|
|
7
|
+
Project-URL: Source, https://github.com/algorandfoundation/puya/tree/main/algopy_testing
|
|
8
|
+
Author-email: Algorand Foundation <contact@algorand.foundation>
|
|
9
|
+
License-Expression: AGPL-3.0-or-later
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Testing
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Requires-Dist: algorand-python>=1.2
|
|
18
|
+
Requires-Dist: coincurve>=19.0.1
|
|
19
|
+
Requires-Dist: ecdsa>=0.17.0
|
|
20
|
+
Requires-Dist: pycryptodomex<4,>=3.6.0
|
|
21
|
+
Requires-Dist: pynacl<2,>=1.4.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
<div align="center">
|
|
25
|
+
<a href="https://github.com/algorandfoundation/algorand-python-testing"><img src="https://bafybeiaibjaf6zy6hvef2rrysaacsfsyb3hw4qqtgn657gw7k5tdzqdxzi.ipfs.nftstorage.link/" width=60%></a>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<a target="_blank" href="https://algorandfoundation.github.io/algorand-python-testing/"><img src="https://img.shields.io/badge/docs-repository-74dfdc?logo=github&style=flat.svg" /></a>
|
|
30
|
+
<a target="_blank" href="https://developer.algorand.org/algokit/"><img src="https://img.shields.io/badge/learn-AlgoKit-74dfdc?logo=algorand&mac=flat.svg" /></a>
|
|
31
|
+
<a target="_blank" href="https://github.com/algorandfoundation/algorand-python-testing"><img src="https://img.shields.io/github/stars/algorandfoundation/algorand-python-testing?color=74dfdc&logo=star&style=flat" /></a>
|
|
32
|
+
<a target="_blank" href="https://developer.algorand.org/algokit/"><img src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fgithub.com%2Falgorandfoundation%2Falgorand-python-testing&countColor=%2374dfdc&style=flat" /></a>
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
Algorand Python Testing is a companion package to [Algorand Python](https://github.com/algorandfoundation/puya) that enables efficient unit testing of Algorand Python smart contracts in an offline environment. It emulates key AVM behaviors without requiring a network connection, offering fast and reliable testing capabilities with a familiar Pythonic interface.
|
|
38
|
+
|
|
39
|
+
[Documentation](https://algorandfoundation.github.io/algopy_testing/index.html) | [Algorand Python Documentation](https://algorandfoundation.github.io/puya/)
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
The easiest way to use Algorand Python Testing is to instantiate a template with AlgoKit via `algokit init -t python`. This will give you a full development environment with testing capabilities built-in.
|
|
44
|
+
|
|
45
|
+
Alternatively, if you want to start from scratch:
|
|
46
|
+
|
|
47
|
+
1. Ensure you have Python 3.12+
|
|
48
|
+
2. Install [AlgoKit CLI](https://github.com/algorandfoundation/algokit-cli?tab=readme-ov-file#install)
|
|
49
|
+
3. Install Algorand Python Testing into your project:
|
|
50
|
+
```bash
|
|
51
|
+
pip install algopy-testing-python
|
|
52
|
+
```
|
|
53
|
+
4. Create a test file (e.g., `test_contract.py`):
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from algopy_testing import algopy_testing_context
|
|
57
|
+
from your_contract import YourContract
|
|
58
|
+
|
|
59
|
+
def test_your_contract():
|
|
60
|
+
with algopy_testing_context() as ctx:
|
|
61
|
+
contract = YourContract() # Your test code here
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
5. Run your tests using your preferred Python testing framework (e.g., pytest, unittest)
|
|
65
|
+
|
|
66
|
+
For more detailed information, check out the [full documentation](https://algorandfoundation.github.io/algopy_testing).
|
|
67
|
+
|
|
68
|
+
## Features
|
|
69
|
+
|
|
70
|
+
- Offline testing environment simulating core AVM functionality
|
|
71
|
+
- Compatible with popular Python testing frameworks
|
|
72
|
+
- Supports testing of ARC4 contracts, smart signatures, and more
|
|
73
|
+
- Provides tools for mocking blockchain state and transactions
|
|
74
|
+
|
|
75
|
+
## Examples
|
|
76
|
+
|
|
77
|
+
For detailed examples showcasing various testing scenarios, refer to the [examples section](https://algorandfoundation.github.io/algopy_testing/examples.html) in the documentation.
|
|
78
|
+
|
|
79
|
+
## Contributing
|
|
80
|
+
|
|
81
|
+
We welcome contributions to this project! Please read our [contributing guide](CONTRIBUTING.md) to get started.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
algopy/__init__.py,sha256=nd5hpdHpkfb5srMM6vHKfbOmEIxTutr4Dh0b7wwFq8Y,1143
|
|
2
|
+
algopy/arc4.py,sha256=zE-cwwmeEBA0m22QiHIujeY0S5rdyf3gDqTyGgROAqI,48
|
|
3
|
+
algopy/gtxn.py,sha256=tS2waKIr3g79T1xcmE2iVcvu524xANsR8awXduUVoT0,48
|
|
4
|
+
algopy/itxn.py,sha256=gXida1ee8xMfRufZ24G4xPGGSe26Iwm9nqfHpQCxbpE,48
|
|
5
|
+
algopy/op.py,sha256=JmF0QiVmqF1Oqhj9VypCl7ef-r2r-dHPI0hxIS3LQD8,46
|
|
6
|
+
algopy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
algopy_testing/__init__.py,sha256=x7WpDet7utqy6M9MdE5OJeKLqNd1uAIPg_U2ZpDOenI,1150
|
|
8
|
+
algopy_testing/arc4.py,sha256=3rCZGKs4-mH3SymHOrp190edGS9MV_v3L5oP_5_GBsA,52015
|
|
9
|
+
algopy_testing/constants.py,sha256=L1-Vod3_k6NAmeHJ9d8qIIovSMlakudvd4VeR6BjPeo,733
|
|
10
|
+
algopy_testing/context.py,sha256=TswTYyLMw1_novoEnoLoIIGrkN-c2xBCWoWFW7AOvs8,39371
|
|
11
|
+
algopy_testing/enums.py,sha256=MJE7g_5wiI94a5oAaEdPGGaO7QS1wm5GgZlYtADwB1U,690
|
|
12
|
+
algopy_testing/gtxn.py,sha256=ul1iBYt8Kwc9Xob_pWVwa6dj9ueQ0iIz2DHkC72Aga4,7954
|
|
13
|
+
algopy_testing/itxn.py,sha256=lLz6dPBwTSvU5zmuKPQd0LkCLG1VJN2Q8Dmb6HRNb3s,23363
|
|
14
|
+
algopy_testing/op.py,sha256=e_VY3pvgUuW_y64yolzzuxeN2Gpg4XkDcJM5fNK-iOg,33046
|
|
15
|
+
algopy_testing/protocols.py,sha256=fPvuYAWKVVYM8oZE9hJtmRKW24Ba8VSzrBGz-7mDnIw,448
|
|
16
|
+
algopy_testing/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
algopy_testing/utils.py,sha256=PkDkHPAKJxyq6Jc0kWSVhF9MtIyNZmNv-hY2bPtwQ40,8393
|
|
18
|
+
algopy_testing/decorators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
algopy_testing/decorators/abimethod.py,sha256=8pRNP8EEExCkBk9i3mQukRrMngL6FnViJQKqWQjET1E,6756
|
|
20
|
+
algopy_testing/decorators/baremethod.py,sha256=g-HJl49g5q5T40QHXLNZnVKob4XtGaPcWWX_F3Z7HKY,2324
|
|
21
|
+
algopy_testing/decorators/subroutine.py,sha256=7bQt-RBdqmbzosMTIllBh1KCEDUSyZwNSxXc-1guflw,192
|
|
22
|
+
algopy_testing/models/__init__.py,sha256=yQdq6-32r3NfVZWn-n4ILdLOiFDf8oxrKfyHRQPnv-I,923
|
|
23
|
+
algopy_testing/models/account.py,sha256=vR_furAVjNGkT1x8vJ8BQeOuCyjg3Cwc1pIGUkwX-R0,4215
|
|
24
|
+
algopy_testing/models/application.py,sha256=leMvgKkYt7Kct4ayUwkL7rQy65mpqp52oGnuoNO-1xA,2203
|
|
25
|
+
algopy_testing/models/asset.py,sha256=qsvC7vGDKTH_PHqybc0SAgzPLAjPgzgwK0aDliUxoLk,3471
|
|
26
|
+
algopy_testing/models/block.py,sha256=JwzoQ3Z7EIvOaSjbMlzQTQ7DztYejw43rQeRptolADs,876
|
|
27
|
+
algopy_testing/models/box.py,sha256=CJrj9v0SwoJztyMVbe0Xu9ZHsAhWUdurAXjWo9lped8,5548
|
|
28
|
+
algopy_testing/models/contract.py,sha256=9TwDC-5Ncxor2wn4_EVQ8ku6RpRkTNDTeuU64RKAmP0,2307
|
|
29
|
+
algopy_testing/models/gitxn.py,sha256=MEWkVoK81Hoqz6Bb7uFAuGCZTb_G5lH9Vl1Dx_2nO7w,1594
|
|
30
|
+
algopy_testing/models/global_values.py,sha256=cT5JKbcSY2_VenF6Pn3BkSj_iaV2D5o4uJZYyKXs2xQ,2215
|
|
31
|
+
algopy_testing/models/gtxn.py,sha256=RJCwFyJvIbxwbF27RpWJaS04x7ZACKr1IPP3iXRS6Rk,1993
|
|
32
|
+
algopy_testing/models/itxn.py,sha256=sgN4wvzolwYmdDJC-4hh6PXDWSBU6-FCjm4tuLIb0Y4,2995
|
|
33
|
+
algopy_testing/models/logicsig.py,sha256=5l_7EL2_bVKZ_myc07La2G2_H8aYZF--Hd0tFMic2t0,1151
|
|
34
|
+
algopy_testing/models/template_variable.py,sha256=FbyP9JniPasbgqYtPRfSOF2u6LWkc45suRSmTfURi5g,759
|
|
35
|
+
algopy_testing/models/transactions.py,sha256=QATadTBJIaHERWuhXOsXEeSjVq-9QyHjJSAQrLOz5pc,4313
|
|
36
|
+
algopy_testing/models/txn.py,sha256=chTwHOVpy9elV4ZVTIzVXS_JKTmc1OF6vhmzdyXEC_A,3912
|
|
37
|
+
algopy_testing/models/unsigned_builtins.py,sha256=-6zoPjn1MTh0wzacsMhhLhAEy4DWdWq7TVFtHWP-QZA,826
|
|
38
|
+
algopy_testing/primitives/__init__.py,sha256=xERIGR6lbEN-cPTtR5eH0yeJIawvjg2XQnmIeiwICKQ,260
|
|
39
|
+
algopy_testing/primitives/biguint.py,sha256=Ss_wE9DCqweydiOFTQ3nCm4xZVTciD4b9qkFF0OVrNw,5199
|
|
40
|
+
algopy_testing/primitives/bytes.py,sha256=TI-M5hA4NNTdiNwZg_70EH08VFHOW0f9Wm7RwVeCItk,5634
|
|
41
|
+
algopy_testing/primitives/string.py,sha256=7eWrbFxu74nQ7ZdDg6GVsZ73VF_p24os7SnIoXKZuFk,2189
|
|
42
|
+
algopy_testing/primitives/uint64.py,sha256=-vQHLYza-K7Y_Pms-49pQQ44B78CNLEYBb6X6Ny-G6c,7110
|
|
43
|
+
algopy_testing/state/__init__.py,sha256=uRFuGSn7RMnhjVKWWdHnVtPNhNfTlQIoQaDPbcmkr3U,155
|
|
44
|
+
algopy_testing/state/global_state.py,sha256=1eJRY9SYzOdj_dnNUqF_vghh0t9Q6N1B1MzLX0hKPxU,1662
|
|
45
|
+
algopy_testing/state/local_state.py,sha256=7m98XD5Afbn72yvgw9Q_VLAEhObLBv_5trp11ZkGwuE,1788
|
|
46
|
+
algopy_testing/utilities/__init__.py,sha256=y43zYxwO37xO9CodAENU9y5p6DBVrkDOHnoZZO77nxc,126
|
|
47
|
+
algopy_testing/utilities/budget.py,sha256=P1pOKWCyya4wsqAwKgvp7y0p97hn2Kr2qFVRKm-sJZo,540
|
|
48
|
+
algopy_testing/utilities/log.py,sha256=5MDJY8Oh_COt5OLZXWqkvAJk2qdfBpfSGIpidV-HJ4k,1870
|
|
49
|
+
algorand_python_testing-0.0.0b1.dist-info/METADATA,sha256=zZpOXQVULRPTrkvGyeFCowcvmdZvIQkncTZEwLGz5-E,4172
|
|
50
|
+
algorand_python_testing-0.0.0b1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
51
|
+
algorand_python_testing-0.0.0b1.dist-info/licenses/LICENSE,sha256=jRyAzzkz3HPr-knS8XWyDY8CyuU-L4RtydPP8uGWsUw,657
|
|
52
|
+
algorand_python_testing-0.0.0b1.dist-info/RECORD,,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Copyright (C) 2024 Algorand Foundation
|
|
2
|
+
|
|
3
|
+
This program is free software: you can redistribute it and/or modify
|
|
4
|
+
it under the terms of the GNU Affero General Public License as
|
|
5
|
+
published by the Free Software Foundation, either version 3 of the
|
|
6
|
+
License, or any later version.
|
|
7
|
+
|
|
8
|
+
This program is distributed in the hope that it will be useful,
|
|
9
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
GNU Affero General Public License for more details.
|
|
12
|
+
|
|
13
|
+
You should have received a copy of the GNU Affero General Public License
|
|
14
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|