From d729bf9b70ef885cd1b2ef705c4f5e2582d853ab Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Tue, 20 Aug 2024 22:32:15 +0200 Subject: [PATCH] docs: integrating pydoclint; formatting docs; removing docs from stub implementation --- .gitignore | 2 + pyproject.toml | 14 +- .../_context_helpers/context_storage.py | 6 +- .../_context_helpers/txn_context.py | 47 ++---- src/algopy_testing/_itxn_loader.py | 101 +++++------ src/algopy_testing/_value_generators/arc4.py | 64 +------ src/algopy_testing/_value_generators/txn.py | 41 ++--- src/algopy_testing/arc4.py | 70 ++------ src/algopy_testing/context.py | 44 ++--- src/algopy_testing/itxn.py | 4 - src/algopy_testing/models/asset.py | 3 +- .../models/template_variable.py | 4 +- src/algopy_testing/op/pure.py | 4 +- src/algopy_testing/primitives/biguint.py | 3 +- src/algopy_testing/primitives/bytes.py | 19 +-- src/algopy_testing/primitives/string.py | 9 +- src/algopy_testing/primitives/uint64.py | 4 +- src/algopy_testing/state/global_state.py | 11 +- tests/models/test_asset.py | 130 ++++++++++++++ tests/state/test_global_state.py | 158 +++++++++++++----- tests/test_op.py | 63 ++++++- 21 files changed, 452 insertions(+), 349 deletions(-) create mode 100644 tests/models/test_asset.py diff --git a/.gitignore b/.gitignore index 28f7c21..a5ffbcd 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ examples/**/*.trace .coverage coverage.xml .venv* + +.cursorignore diff --git a/pyproject.toml b/pyproject.toml index cd4da7d..2006fdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,7 @@ validate_examples = "python scripts/validate_examples.py" check_stubs_cov = "python scripts/check_stubs_cov.py" pre_commit = [ "hatch run lint:fix", + "hatch run lint:check", "mypy_testing", "hatch run examples:pre_commit", ] @@ -97,15 +98,17 @@ dependencies = [ "ruff==0.5.6", "ruff-lsp", "docformatter", + "pydoclint", ] [tool.hatch.envs.lint.scripts] check = [ "black --check .", "ruff check", + "pydoclint --config=pyproject.toml src", ] fix = [ - "docformatter -i -r src", + "docformatter -i -r --black --style sphinx src", "black .", "ruff check --fix", ] @@ -315,3 +318,12 @@ upload_to_vcs_release = true [tool.semantic_release.remote.token] env = "GITHUB_TOKEN" + +[tool.pydoclint] +style = 'sphinx' +check-return-types = 'False' +skip-checking-raises = 'True' +arg-type-hints-in-docstring = 'False' + +[tool.docformatter] +style = "sphinx" diff --git a/src/algopy_testing/_context_helpers/context_storage.py b/src/algopy_testing/_context_helpers/context_storage.py index 11b9937..7d67e28 100644 --- a/src/algopy_testing/_context_helpers/context_storage.py +++ b/src/algopy_testing/_context_helpers/context_storage.py @@ -21,9 +21,9 @@ class _InternalContext: - """For accessing implementation specific functions, with a convenient - single entry point for other modules to import Also allows for a single - place to check and provide.""" + """For accessing implementation specific functions, with a convenient single entry + point for other modules to import Also allows for a single place to check and + provide.""" @property def value(self) -> AlgopyTestContext: diff --git a/src/algopy_testing/_context_helpers/txn_context.py b/src/algopy_testing/_context_helpers/txn_context.py index f4ca53a..b5c9918 100644 --- a/src/algopy_testing/_context_helpers/txn_context.py +++ b/src/algopy_testing/_context_helpers/txn_context.py @@ -87,16 +87,8 @@ def defer_app_call( *args: TParamSpec.args, **kwargs: TParamSpec.kwargs, ) -> DeferredAppCall[TReturn]: - """Prepare an application call transaction group for a contract method - without executing it. - - :param method: The decorated contract method (baremethod or - abimethod). - :param args: Positional arguments for the method. - :param kwargs: Keyword arguments for the method. - :return: A DeferredAppCall object containing the transaction - group and method info. - """ + r"""Prepare an application call transaction group for a contract method without + executing it.""" from algopy_testing.models import ARC4Contract arc4_metadata = get_arc4_metadata(method) @@ -119,7 +111,7 @@ def defer_app_call( app_id = contract.__app_id__ # Handle ABI methods if arc4_metadata.arc4_signature: - ordered_args = get_ordered_args(fn, args, kwargs) # type: ignore[arg-type] + ordered_args = get_ordered_args(fn, args, kwargs) txns = create_abimethod_txns( app_id=app_id, arc4_signature=arc4_metadata.arc4_signature, @@ -140,20 +132,13 @@ def create_group( active_txn_index: int | None = None, txn_op_fields: TransactionBaseFields | None = None, ) -> Iterator[None]: - """Adds a new transaction group using a list of transactions and an - optional index to indicate the active transaction within the group. - - :param gtxns: List of transactions. - :type gtxns: list[algopy.gtxn.TransactionBase] - :param active_txn_index: Index of the active transaction. - :type active_txn_index: int - :param active_txn_index: Index of the active transaction. - Defaults to None. - :type active_txn_index: int - :param gtxn: list[algopy.gtxn.TransactionBase]: - :param active_txn_index: int | None: (Default value = None) - :param txn_op_fields: dict[str, typing.Any] | None: (Default - value = None) + """Adds a new transaction group using a list of transactions and an optional + index to indicate the active transaction within the group. + + :param gtxns: List of transactions + :param active_txn_index: Index of the active transaction + :param txn_op_fields: Additional transaction operation fields + :return: None """ processed_gtxns = [] @@ -291,12 +276,10 @@ def get_scratch_slot( self, index: algopy.UInt64 | int, ) -> algopy.UInt64 | algopy.Bytes: - """Retrieves the scratch values for a specific slot in the active - transaction. + """Retrieves the scratch values for a specific slot in the active transaction. - :param index: algopy.UInt64 | int: Which scratch slot to query - :returns: Scratch slot value for the active transaction. - :rtype: algopy.UInt64 | algopy.Bytes + :param index: Which scratch slot to query + :return: Scratch slot value for the active transaction """ # this wraps an internal method on TransactionBase, so it can be exposed to # consumers of algopy_testing @@ -307,8 +290,8 @@ def get_scratch_space( ) -> Sequence[algopy.Bytes | algopy.UInt64]: """Retrieves scratch space for the active transaction. - :returns: List of scratch space values for the active - transaction. + :return: List of scratch space values for the active transaction + :rtype: Sequence[algopy.Bytes | algopy.UInt64] """ return self._active_txn.get_scratch_space() diff --git a/src/algopy_testing/_itxn_loader.py b/src/algopy_testing/_itxn_loader.py index fae68cb..c0726b8 100644 --- a/src/algopy_testing/_itxn_loader.py +++ b/src/algopy_testing/_itxn_loader.py @@ -24,12 +24,12 @@ class ITxnLoader: - """A helper class for handling access to individual inner transactions in - test context. + """A helper class for handling access to individual inner transactions in test + context. - This class provides methods to access and retrieve specific types of - inner transactions. It performs type checking and conversion for - various transaction types. + This class provides methods to access and retrieve specific types of inner + transactions. It performs type checking and conversion for various transaction + types. """ _TXN_TYPE_MAP: typing.ClassVar = { @@ -57,8 +57,7 @@ def _get_itxn(self, txn_type: type[_T]) -> _T: def payment(self) -> algopy.itxn.PaymentInnerTransaction: """Retrieve the last PaymentInnerTransaction. - :raises ValueError: If the transaction is not found or not of - the expected type. + :raises ValueError: If the transaction is not found or not of the expected type. """ return self._get_itxn(itxn.PaymentInnerTransaction) @@ -66,8 +65,7 @@ def payment(self) -> algopy.itxn.PaymentInnerTransaction: def asset_config(self) -> algopy.itxn.AssetConfigInnerTransaction: """Retrieve the last AssetConfigInnerTransaction. - :raises ValueError: If the transaction is not found or not of - the expected type. + :raises ValueError: If the transaction is not found or not of the expected type. """ return self._get_itxn(itxn.AssetConfigInnerTransaction) @@ -75,8 +73,7 @@ def asset_config(self) -> algopy.itxn.AssetConfigInnerTransaction: def asset_transfer(self) -> algopy.itxn.AssetTransferInnerTransaction: """Retrieve the last AssetTransferInnerTransaction. - :raises ValueError: If the transaction is not found or not of - the expected type. + :raises ValueError: If the transaction is not found or not of the expected type. """ return self._get_itxn(itxn.AssetTransferInnerTransaction) @@ -84,8 +81,7 @@ def asset_transfer(self) -> algopy.itxn.AssetTransferInnerTransaction: def asset_freeze(self) -> algopy.itxn.AssetFreezeInnerTransaction: """Retrieve the last AssetFreezeInnerTransaction. - :raises ValueError: If the transaction is not found or not of - the expected type. + :raises ValueError: If the transaction is not found or not of the expected type. """ return self._get_itxn(itxn.AssetFreezeInnerTransaction) @@ -93,8 +89,7 @@ def asset_freeze(self) -> algopy.itxn.AssetFreezeInnerTransaction: def application_call(self) -> algopy.itxn.ApplicationCallInnerTransaction: """Retrieve the last ApplicationCallInnerTransaction. - :raises ValueError: If the transaction is not found or not of - the expected type. + :raises ValueError: If the transaction is not found or not of the expected type. """ return self._get_itxn(itxn.ApplicationCallInnerTransaction) @@ -102,8 +97,7 @@ def application_call(self) -> algopy.itxn.ApplicationCallInnerTransaction: def key_registration(self) -> algopy.itxn.KeyRegistrationInnerTransaction: """Retrieve the last KeyRegistrationInnerTransaction. - :raises ValueError: If the transaction is not found or not of - the expected type. + :raises ValueError: If the transaction is not found or not of the expected type. """ return self._get_itxn(itxn.KeyRegistrationInnerTransaction) @@ -111,20 +105,18 @@ def key_registration(self) -> algopy.itxn.KeyRegistrationInnerTransaction: def transaction(self) -> algopy.itxn.InnerTransactionResult: """Retrieve the last InnerTransactionResult. - :raises ValueError: If the transaction is not found or not of - the expected type. + :raises ValueError: If the transaction is not found or not of the expected type. """ return self._get_itxn(itxn.InnerTransactionResult) class ITxnGroupLoader: - """A helper class for handling access to groups of inner transactions in - test context. + """A helper class for handling access to groups of inner transactions in test + context. - This class provides methods to access and retrieve inner - transactions from a group, either individually or as slices. It - supports type-specific retrieval of inner transactions and - implements indexing operations. + This class provides methods to access and retrieve inner transactions from a group, + either individually or as slices. It supports type-specific retrieval of inner + transactions and implements indexing operations. """ @typing.overload @@ -157,74 +149,57 @@ def _get_itxn(self, index: int) -> InnerTransactionResultType: def payment(self, index: int) -> algopy.itxn.PaymentInnerTransaction: """Return a PaymentInnerTransaction from the group at the given index. - :param index: int - :param index: int: - :returns: algopy.itxn.PaymentInnerTransaction: The - PaymentInnerTransaction at the given index. + :param index: The index of the transaction in the group. + :returns: The PaymentInnerTransaction at the given index. + :raises TypeError: If the transaction is not found or not of """ return ITxnLoader(self._get_itxn(index)).payment def asset_config(self, index: int) -> algopy.itxn.AssetConfigInnerTransaction: - """Return an AssetConfigInnerTransaction from the group at the given - index. + """Return an AssetConfigInnerTransaction from the group at the given index. - :param index: int - :param index: int: - :returns: algopy.itxn.AssetConfigInnerTransaction: The - AssetConfigInnerTransaction at the given index. + :param index: The index of the transaction in the group. + :returns: The AssetConfigInnerTransaction at the given index. + :raises TypeError: If the transaction is not found or not of the expected type. """ return ITxnLoader(self._get_itxn(index)).asset_config def asset_transfer(self, index: int) -> algopy.itxn.AssetTransferInnerTransaction: - """Return an AssetTransferInnerTransaction from the group at the given - index. + """Return an AssetTransferInnerTransaction from the group at the given index. - :param index: int - :param index: int: - :returns: algopy.itxn.AssetTransferInnerTransaction: The - AssetTransferInnerTransaction at the given index. + :param index: The index of the transaction in the group. + :returns: The AssetTransferInnerTransaction at the given index. """ return ITxnLoader(self._get_itxn(index)).asset_transfer def asset_freeze(self, index: int) -> algopy.itxn.AssetFreezeInnerTransaction: - """Return an AssetFreezeInnerTransaction from the group at the given - index. + """Return an AssetFreezeInnerTransaction from the group at the given index. - :param index: int - :param index: int: - :returns: algopy.itxn.AssetFreezeInnerTransaction: The - AssetFreezeInnerTransaction at the given index. + :param index: The index of the transaction in the group. + :returns: The AssetFreezeInnerTransaction at the given index. """ return ITxnLoader(self._get_itxn(index)).asset_freeze def application_call(self, index: int) -> algopy.itxn.ApplicationCallInnerTransaction: - """Return an ApplicationCallInnerTransaction from the group at the - given index. + """Return an ApplicationCallInnerTransaction from the group at the given index. - :param index: int - :param index: int: - :returns: algopy.itxn.ApplicationCallInnerTransaction: The - ApplicationCallInnerTransaction at the given index. + :param index: The index of the transaction in the group. + :returns: The ApplicationCallInnerTransaction at the given index. """ return ITxnLoader(self._get_itxn(index)).application_call def key_registration(self, index: int) -> algopy.itxn.KeyRegistrationInnerTransaction: - """Return a KeyRegistrationInnerTransaction from the group at the given - index. + """Return a KeyRegistrationInnerTransaction from the group at the given index. - :param index: int - :param index: int: - :returns: algopy.itxn.KeyRegistrationInnerTransaction: The - KeyRegistrationInnerTransaction at the given index. + :param index: The index of the transaction in the group. + :returns: The KeyRegistrationInnerTransaction at the given index. """ return ITxnLoader(self._get_itxn(index)).key_registration def transaction(self, index: int) -> algopy.itxn.InnerTransactionResult: """Return an InnerTransactionResult from the group at the given index. - :param index: int - :param index: int: - :returns: algopy.itxn.InnerTransactionResult: The - InnerTransactionResult at the given index. + :param index: The index of the transaction in the group. + :returns: The InnerTransactionResult at the given index. """ return ITxnLoader(self._get_itxn(index)).transaction diff --git a/src/algopy_testing/_value_generators/arc4.py b/src/algopy_testing/_value_generators/arc4.py index 984e33f..9ced824 100644 --- a/src/algopy_testing/_value_generators/arc4.py +++ b/src/algopy_testing/_value_generators/arc4.py @@ -21,7 +21,6 @@ def address(self) -> algopy.arc4.Address: """Generate a random Algorand address. :returns: A new, random Algorand address. - :rtype: algopy.arc4.Address """ return arc4.Address(algosdk.account.generate_account()[1]) @@ -30,15 +29,8 @@ def uint8(self, min_value: int = 0, max_value: int = MAX_UINT8) -> algopy.arc4.U """Generate a random UInt8 within the specified range. :param min_value: Minimum value (inclusive). Defaults to 0. - :type min_value: int - :param max_value: Maximum value (inclusive). Defaults to - MAX_UINT8. - :type max_value: int - :param min_value: int: (Default value = 0) - :param max_value: int: (Default value = MAX_UINT8) + :param max_value: Maximum value (inclusive). Defaults to MAX_UINT8. :returns: A random UInt8 value. - :rtype: algopy.arc4.UInt8 - :raises AssertionError: If values are out of UInt8 range. """ return arc4.UInt8(generate_random_int(min_value, max_value)) @@ -46,15 +38,8 @@ def uint16(self, min_value: int = 0, max_value: int = MAX_UINT16) -> algopy.arc4 """Generate a random UInt16 within the specified range. :param min_value: Minimum value (inclusive). Defaults to 0. - :type min_value: int - :param max_value: Maximum value (inclusive). Defaults to - MAX_UINT16. - :type max_value: int - :param min_value: int: (Default value = 0) - :param max_value: int: (Default value = MAX_UINT16) + :param max_value: Maximum value (inclusive). Defaults to MAX_UINT16. :returns: A random UInt16 value. - :rtype: algopy.arc4.UInt16 - :raises AssertionError: If values are out of UInt16 range. """ return arc4.UInt16(generate_random_int(min_value, max_value)) @@ -62,15 +47,8 @@ def uint32(self, min_value: int = 0, max_value: int = MAX_UINT32) -> algopy.arc4 """Generate a random UInt32 within the specified range. :param min_value: Minimum value (inclusive). Defaults to 0. - :type min_value: int - :param max_value: Maximum value (inclusive). Defaults to - MAX_UINT32. - :type max_value: int - :param min_value: int: (Default value = 0) - :param max_value: int: (Default value = MAX_UINT32) + :param max_value: Maximum value (inclusive). Defaults to MAX_UINT32. :returns: A random UInt32 value. - :rtype: algopy.arc4.UInt32 - :raises AssertionError: If values are out of UInt32 range. """ return arc4.UInt32(generate_random_int(min_value, max_value)) @@ -78,15 +56,8 @@ def uint64(self, min_value: int = 0, max_value: int = MAX_UINT64) -> algopy.arc4 """Generate a random UInt64 within the specified range. :param min_value: Minimum value (inclusive). Defaults to 0. - :type min_value: int - :param max_value: Maximum value (inclusive). Defaults to - MAX_UINT64. - :type max_value: int - :param min_value: int: (Default value = 0) - :param max_value: int: (Default value = MAX_UINT64) + :param max_value: Maximum value (inclusive). Defaults to MAX_UINT64. :returns: A random UInt64 value. - :rtype: algopy.arc4.UInt64 - :raises AssertionError: If values are out of UInt64 range. """ return arc4.UInt64(generate_random_int(min_value, max_value)) @@ -96,14 +67,8 @@ def biguint128( """Generate a random UInt128 within the specified range. :param min_value: Minimum value (inclusive). Defaults to 0. - :type min_value: int :param max_value: Maximum value (inclusive). Defaults to (2^128 - 1). - :type max_value: int - :param min_value: int: (Default value = 0) - :param max_value: int: (Default value = (1 << 128) - 1) :returns: A random UInt128 value. - :rtype: algopy.arc4.UInt128 - :raises AssertionError: If values are out of UInt128 range. """ return arc4.UInt128(generate_random_int(min_value, max_value)) @@ -113,14 +78,8 @@ def biguint256( """Generate a random UInt256 within the specified range. :param min_value: Minimum value (inclusive). Defaults to 0. - :type min_value: int :param max_value: Maximum value (inclusive). Defaults to (2^256 - 1). - :type max_value: int - :param min_value: int: (Default value = 0) - :param max_value: int: (Default value = (1 << 256) - 1) :returns: A random UInt256 value. - :rtype: algopy.arc4.UInt256 - :raises AssertionError: If values are out of UInt256 range. """ return arc4.UInt256(generate_random_int(min_value, max_value)) @@ -128,15 +87,8 @@ def biguint512(self, min_value: int = 0, max_value: int = MAX_UINT512) -> algopy """Generate a random UInt512 within the specified range. :param min_value: Minimum value (inclusive). Defaults to 0. - :type min_value: int - :param max_value: Maximum value (inclusive). Defaults to - MAX_UINT512. - :type max_value: int - :param min_value: int: (Default value = 0) - :param max_value: int: (Default value = MAX_UINT512) + :param max_value: Maximum value (inclusive). Defaults to MAX_UINT512. :returns: A random UInt512 value. - :rtype: algopy.arc4.UInt512 - :raises AssertionError: If values are out of UInt512 range. """ return arc4.UInt512(generate_random_int(min_value, max_value)) @@ -145,10 +97,7 @@ def dynamic_bytes(self, n: int) -> algopy.arc4.DynamicBytes: :param n: The number of bits for the dynamic bytes. Must be a multiple of 8, otherwise the last byte will be truncated. - :type n: int - :param n: int: :returns: A new, random dynamic bytes of size `n` bits. - :rtype: algopy.arc4.DynamicBytes """ # rounding up num_bytes = (n + 7) // 8 @@ -166,10 +115,7 @@ def string(self, n: int) -> algopy.arc4.String: """Generate a random string of size `n` bits. :param n: The number of bits for the string. - :type n: int - :param n: int: :returns: A new, random string of size `n` bits. - :rtype: algopy.arc4.String """ # Calculate the number of characters needed (rounding up) num_chars = (n + 7) // 8 diff --git a/src/algopy_testing/_value_generators/txn.py b/src/algopy_testing/_value_generators/txn.py index 4f7dadb..f56fe1b 100644 --- a/src/algopy_testing/_value_generators/txn.py +++ b/src/algopy_testing/_value_generators/txn.py @@ -34,13 +34,10 @@ def application_call( ) -> algopy.gtxn.ApplicationCallTransaction: """Generate a new application call transaction. - :param scratch_space: Scratch space data. :param **fields: - Fields to be set in the transaction. :type **fields: - Unpack[ApplicationCallFields] - :param scratch_space: Sequence[algopy.Bytes | algopy.UInt64 | - int | bytes] | None: (Default value = None) :param **fields: - Unpack[ApplicationCallFields]: - :returns: New application call transaction. + :param scratch_space: Scratch space data. + :param **fields: Fields to be set in the transaction. + :return: New application call transaction. + :raises TypeError: If `app_id` is not an instance of algopy.Application """ try: app = fields["app_id"] @@ -62,22 +59,16 @@ def asset_transfer( ) -> algopy.gtxn.AssetTransferTransaction: """Generate a new asset transfer transaction with specified fields. - :param **fields: Fields to be set in the transaction. :type - **fields: Unpack[AssetTransferFields] :param **fields: - Unpack[AssetTransferFields]: - :returns: The newly generated asset transfer transaction. - :rtype: algopy.gtxn.AssetTransferTransaction + :param **fields: Fields to be set in the transaction. + :return: The newly generated asset transfer transaction. """ return self._new_gtxn(gtxn.AssetTransferTransaction, **fields) def payment(self, **fields: typing.Unpack[PaymentFields]) -> algopy.gtxn.PaymentTransaction: """Generate a new payment transaction with specified fields. - :param **fields: Fields to be set in the transaction. :type - **fields: Unpack[PaymentFields] :param **fields: - Unpack[PaymentFields]: - :returns: The newly generated payment transaction. - :rtype: algopy.gtxn.PaymentTransaction + :param **fields: Fields to be set in the transaction. + :return: The newly generated payment transaction. """ return self._new_gtxn(gtxn.PaymentTransaction, **fields) @@ -86,7 +77,8 @@ def asset_config( ) -> algopy.gtxn.AssetConfigTransaction: """Generate a new ACFG transaction with specified fields. - :param **fields: Unpack[AssetConfigFields]: + :param **fields: Fields to be set in the transaction. + :return: The newly generated asset config transaction. """ return self._new_gtxn(gtxn.AssetConfigTransaction, **fields) @@ -95,7 +87,8 @@ def key_registration( ) -> algopy.gtxn.KeyRegistrationTransaction: """Generate a new key registration transaction with specified fields. - :param **fields: Unpack[KeyRegistrationFields]: + :param **fields: Fields to be set in the transaction. + :return: The newly generated key registration transaction. """ return self._new_gtxn(gtxn.KeyRegistrationTransaction, **fields) @@ -104,7 +97,8 @@ def asset_freeze( ) -> algopy.gtxn.AssetFreezeTransaction: """Generate a new asset freeze transaction with specified fields. - :param **fields: Unpack[AssetFreezeFields]: + :param **fields: Fields to be set in the transaction. + :return: The newly generated asset freeze transaction. """ return self._new_gtxn(gtxn.AssetFreezeTransaction, **fields) @@ -114,11 +108,8 @@ def transaction( ) -> algopy.gtxn.Transaction: """Generate a new transaction with specified fields. - :param **fields: Fields to be set in the transaction. :type - **fields: Unpack[TransactionFields] :param **fields: - Unpack[TransactionFields]: - :returns: The newly generated transaction. - :rtype: algopy.gtxn.Transaction + :param **fields: Fields to be set in the transaction. + :return: The newly generated transaction. """ return self._new_gtxn(gtxn.Transaction, **fields) diff --git a/src/algopy_testing/arc4.py b/src/algopy_testing/arc4.py index cfaa316..06571fe 100644 --- a/src/algopy_testing/arc4.py +++ b/src/algopy_testing/arc4.py @@ -139,8 +139,8 @@ def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self: @classmethod def from_log(cls, log: algopy.Bytes, /) -> typing.Self: - """Load an ABI type from application logs, checking for the ABI return - prefix `0x151f7c75`""" + """Load an ABI type from application logs, checking for the ABI return prefix + `0x151f7c75`""" if log[:4] == ARC4_RETURN_PREFIX: return cls.from_bytes(log[4:]) raise ValueError("ABI return prefix not found") @@ -200,8 +200,7 @@ def __init__(self, value: algopy.String | str = "", /) -> None: @property def native(self) -> algopy.String: - """Return the String representation of the UTF8 string after ARC4 - decoding.""" + """Return the String representation of the UTF8 string after ARC4 decoding.""" import algopy return algopy.String.from_bytes(self._value[_ABI_LENGTH_SIZE:]) @@ -323,8 +322,7 @@ class UIntN(_UIntN, typing.Generic[_TBitSize]): # type: ignore[type-arg] @property def native(self) -> algopy.UInt64: - """Return the UInt64 representation of the value after ARC4 - decoding.""" + """Return the UInt64 representation of the value after ARC4 decoding.""" import algopy return algopy.UInt64(int.from_bytes(self._value)) @@ -359,8 +357,7 @@ class BigUIntN(_UIntN, typing.Generic[_TBitSize]): # type: ignore[type-arg] @property def native(self) -> algopy.BigUInt: - """Return the UInt64 representation of the value after ARC4 - decoding.""" + """Return the UInt64 representation of the value after ARC4 decoding.""" import algopy return algopy.BigUInt.from_bytes(self._value) @@ -439,9 +436,6 @@ class _UFixedNxM( _value: bytes # underlying 'bytes' value representing the UFixedNxM def __init__(self, value: str = "0.0", /) -> None: - """Construct an instance of UFixedNxM where value (v) is determined - from the original decimal value (d) by the formula v = round(d * - (10^M))""" value = as_string(value) with decimal.localcontext( decimal.Context( @@ -692,19 +686,9 @@ def arc4_name(self) -> str: class Address(StaticArray[Byte, typing.Literal[32]]): - """An alias for an array containing 32 bytes representing an Algorand - address.""" - type_info = _AddressTypeInfo() def __init__(self, value: Account | str | algopy.Bytes = algosdk.constants.ZERO_ADDRESS): - """If `value` is a string, it should be a 58 character base32 string, - - ie a base32 string-encoded 32 bytes public key + 4 bytes checksum. - If `value` is a Bytes, it's length checked to be 32 bytes - to avoid this - check, use `Address.from_bytes(...)` instead. - Defaults to the zero-address. - """ if isinstance(value, str): try: bytes_value = algosdk.encoding.decode_address(value) @@ -729,8 +713,8 @@ def __bool__(self) -> bool: return self.bytes != zero_bytes def __eq__(self, other: Address | Account | str) -> bool: # type: ignore[override] - """Address equality is determined by the address of another - `arc4.Address`, `Account` or `str`""" + """Address equality is determined by the address of another `arc4.Address`, + `Account` or `str`""" if isinstance(other, Address | Account): return self.bytes == other.bytes other_bytes: bytes = algosdk.encoding.decode_address(other) @@ -982,7 +966,6 @@ def __new__( return instance def __init__(self, _items: tuple[typing.Unpack[_TTuple]] = (), /): # type: ignore[assignment] - """Construct an ARC4 tuple from a python tuple.""" items = _check_is_arc4(_items) if items: for item, expected_type in zip(items, self.type_info.child_types, strict=True): @@ -1039,8 +1022,8 @@ def from_bytes(cls, value: algopy.Bytes | bytes, /) -> typing.Self: @classmethod def from_log(cls, log: algopy.Bytes, /) -> typing.Self: - """Load an ABI type from application logs, checking for the ABI return - prefix `0x151f7c75`""" + """Load an ABI type from application logs, checking for the ABI return prefix + `0x151f7c75`""" if log[:4] == ARC4_RETURN_PREFIX: return cls.from_bytes(log[4:]) raise ValueError("ABI return prefix not found") @@ -1091,35 +1074,6 @@ def __getitem__(self, return_type: type) -> typing.Any: def emit(event: str | Struct, /, *args: object) -> None: - """Emit an ARC-28 event for the provided event signature or name, and - provided args. - - :param event: Either an ARC4 Struct, an event name, or event signature. - * If event is an ARC4 Struct, the event signature will be determined from the Struct name and fields - * If event is a signature, then the following args will be typed checked to ensure they match. - * If event is just a name, the event signature will be inferred from the name and following arguments - - :param args: When event is a signature or name, args will be used as the event data. - They will all be encoded as single ARC4 Tuple - - Example: - ``` - from algopy import ARC4Contract, arc4 - - - class Swapped(arc4.Struct): - a: arc4.UInt64 - b: arc4.UInt64 - - - class EventEmitter(ARC4Contract): - @arc4.abimethod - def emit_swapped(self, a: arc4.UInt64, b: arc4.UInt64) -> None: - arc4.emit(Swapped(b, a)) - arc4.emit("Swapped(uint64,uint64)", b, a) - arc4.emit("Swapped", b, a) - ``` - """ # noqa: E501 from algopy_testing.utilities.log import log if isinstance(event, str): @@ -1184,8 +1138,7 @@ def _find_bool( index: int, delta: int, ) -> int: - """Helper function to find consecutive booleans from current index in a - tuple.""" + """Helper function to find consecutive booleans from current index in a tuple.""" until = 0 values_length = len(values) if isinstance(values, tuple | list) else values.length.value while True: @@ -1202,8 +1155,7 @@ def _find_bool( def _find_bool_types(values: typing.Sequence[_TypeInfo], index: int, delta: int) -> int: - """Helper function to find consecutive booleans from current index in a - tuple.""" + """Helper function to find consecutive booleans from current index in a tuple.""" until = 0 values_length = len(values) while True: diff --git a/src/algopy_testing/context.py b/src/algopy_testing/context.py index f78840e..1e29edb 100644 --- a/src/algopy_testing/context.py +++ b/src/algopy_testing/context.py @@ -14,14 +14,15 @@ class AlgopyTestContext: - """Manages the testing context for Algorand Python SDK (algopy) - applications. - - This class provides methods and properties to simulate various - aspects of the Algorand blockchain environment, including accounts, - assets, applications, transactions, and global state. It allows for - easy setup and manipulation of test scenarios for algopy-based smart - contracts and applications. + """Manages the testing context for Algorand Python SDK (algopy) applications. + + This class provides methods and properties to simulate various aspects of the + Algorand blockchain environment, including accounts, assets, applications, + transactions, and global state. It allows for easy setup and manipulation of test + scenarios for algopy-based smart contracts and applications. + + :param default_sender: The default sender account address, defaults to None + :param template_vars: Dictionary of template variables, defaults to None """ def __init__( @@ -30,15 +31,6 @@ def __init__( default_sender: str | None = None, template_vars: dict[str, typing.Any] | None = None, ) -> None: - """Initialize the AlgopyTestContext. - - :param default_sender: The default sender account address, - defaults to None - :type default_sender: str | None, optional - :param template_vars: Dictionary of template variables, defaults - to None - :type template_vars: dict[str, typing.Any] | None, optional - """ import algopy self._default_sender = algopy.Account( @@ -93,7 +85,6 @@ def get_app_for_contract( """Get the application for a given contract. :param contract: The contract to get the application for - :type contract: algopy.Contract | algopy.ARC4Contract :return: The application associated with the contract :rtype: algopy.Application """ @@ -103,22 +94,13 @@ def set_template_var(self, name: str, value: typing.Any) -> None: """Set a template variable for the current context. :param name: The name of the template variable - :type name: str :param value: The value to assign to the template variable - :type value: Any """ self._template_vars[name] = value def execute_logicsig(self, lsig: algopy.LogicSig, *args: algopy.Bytes) -> bool | algopy.UInt64: - """Execute a logic signature using provided args. - - :param lsig: The logic signature to execute - :type lsig: algopy.LogicSig - :param args: The logic signature arguments to use - :type args: algopy.Bytes - :return: The result of executing the logic signature function - :rtype: bool | algopy.UInt64 - """ + """Execute a logic signature using provided args.""" + self._active_lsig_args = args try: return lsig.func() @@ -130,8 +112,8 @@ def clear_transaction_context(self) -> None: self._txn_context = TransactionContext() def reset(self) -> None: - """Reset the test context to its initial state, clearing all data and - resetting ID counters.""" + """Reset the test context to its initial state, clearing all data and resetting + ID counters.""" self._template_vars.clear() self._txn_context = TransactionContext() self._ledger_context = LedgerContext() diff --git a/src/algopy_testing/itxn.py b/src/algopy_testing/itxn.py index c996983..c2877b4 100644 --- a/src/algopy_testing/itxn.py +++ b/src/algopy_testing/itxn.py @@ -155,10 +155,6 @@ class ApplicationCall(_BaseInnerTransactionFields): def submit_txns( *transactions: _BaseInnerTransactionFields, ) -> tuple[_BaseInnerTransactionResult, ...]: - """Submits a group of up to 16 inner transactions parameters. - - :returns: A tuple of the resulting inner transactions - """ if len(transactions) > algosdk.constants.TX_GROUP_LIMIT: raise ValueError("Cannot submit more than 16 inner transactions at once") diff --git a/src/algopy_testing/models/asset.py b/src/algopy_testing/models/asset.py index c159cb8..77e22ce 100644 --- a/src/algopy_testing/models/asset.py +++ b/src/algopy_testing/models/asset.py @@ -51,7 +51,8 @@ def balance(self, account: algopy.Account) -> algopy.UInt64: if int(self.id) not in account_data.opted_asset_balances: raise ValueError( "The asset is not opted into the account! " - "Use `account.opt_in()` to opt the asset into the account." + "Use `ctx.any.account(opted_asset_balances={{ASSET_ID: VALUE}})` " + "to set emulated opted asset into the account." ) return account_data.opted_asset_balances[self.id] diff --git a/src/algopy_testing/models/template_variable.py b/src/algopy_testing/models/template_variable.py index d8179ae..770ef18 100644 --- a/src/algopy_testing/models/template_variable.py +++ b/src/algopy_testing/models/template_variable.py @@ -21,5 +21,5 @@ def create_template_var(variable_name: str) -> typing.Any: TemplateVar: TemplateVarGeneric = TemplateVarGeneric() -"""Template variables can be used to represent a placeholder for a deploy-time -provided value.""" +"""Template variables can be used to represent a placeholder for a deploy-time provided +value.""" diff --git a/src/algopy_testing/op/pure.py b/src/algopy_testing/op/pure.py index 99154ed..5a41aba 100644 --- a/src/algopy_testing/op/pure.py +++ b/src/algopy_testing/op/pure.py @@ -292,8 +292,8 @@ def _get_bit(v: int, index: int) -> int: def _set_bit(v: int, index: int, x: int) -> int: - """Set the index:th bit of v to 1 if x is truthy, else to 0, and return the - new value.""" + """Set the index:th bit of v to 1 if x is truthy, else to 0, and return the new + value.""" mask = 1 << index # Compute mask, an integer with just bit 'index' set. v &= ~mask # Clear the bit indicated by the mask (if x is False) if x: diff --git a/src/algopy_testing/primitives/biguint.py b/src/algopy_testing/primitives/biguint.py index c5f3add..e8195fa 100644 --- a/src/algopy_testing/primitives/biguint.py +++ b/src/algopy_testing/primitives/biguint.py @@ -14,8 +14,7 @@ @functools.total_ordering class BigUInt(BytesBacked): - """A python implementation of an TEAL bigint type represented by AVM []byte - type.""" + """A python implementation of an TEAL bigint type represented by AVM []byte type.""" __value: bytes # underlying 'bytes' value representing the BigUInt diff --git a/src/algopy_testing/primitives/bytes.py b/src/algopy_testing/primitives/bytes.py index 8b4d4c2..e7e82b1 100644 --- a/src/algopy_testing/primitives/bytes.py +++ b/src/algopy_testing/primitives/bytes.py @@ -37,8 +37,8 @@ def __bool__(self) -> bool: return bool(self.value) def __add__(self, other: Bytes | bytes) -> Bytes: - """Concatenate Bytes with another Bytes or bytes literal e.g. - `Bytes(b"Hello ") + b"World"`.""" + """Concatenate Bytes with another Bytes or bytes literal e.g. `Bytes(b"Hello ") + + b"World"`.""" if isinstance(other, Bytes): return _checked_result(self.value + other.value, "+") else: @@ -46,8 +46,8 @@ def __add__(self, other: Bytes | bytes) -> Bytes: return _checked_result(result, "+") def __radd__(self, other: bytes) -> Bytes: - """Concatenate Bytes with another Bytes or bytes literal e.g. `b"Hello - " + Bytes(b"World")`.""" + """Concatenate Bytes with another Bytes or bytes literal e.g. `b"Hello " + + Bytes(b"World")`.""" return _checked_result(other + self.value, "+") def __len__(self) -> int: @@ -58,15 +58,15 @@ def __iter__(self) -> Iterator[Bytes]: return _BytesIter(self, 1) def __reversed__(self) -> Iterator[Bytes]: - """Bytes can be iterated in reverse, yield each preceding byte starting - at the end.""" + """Bytes can be iterated in reverse, yield each preceding byte starting at the + end.""" return _BytesIter(self, -1) def __getitem__( self, index: UInt64 | int | slice ) -> Bytes: # maps to substring/substring3 if slice, extract/extract3 otherwise? - """Returns a Bytes containing a single byte if indexed with UInt64 or - int otherwise the substring o bytes described by the slice.""" + """Returns a Bytes containing a single byte if indexed with UInt64 or int + otherwise the substring o bytes described by the slice.""" if isinstance(index, slice): return Bytes(self.value[index]) else: @@ -143,8 +143,7 @@ def from_base64(value: str) -> Bytes: @staticmethod def from_hex(value: str) -> Bytes: - """Creates Bytes from a hex/octal encoded string e.g. - `Bytes.from_hex("FF")`""" + """Creates Bytes from a hex/octal encoded string e.g. `Bytes.from_hex("FF")`""" return Bytes(base64.b16decode(value)) diff --git a/src/algopy_testing/primitives/string.py b/src/algopy_testing/primitives/string.py index 2bd3927..1ca869f 100644 --- a/src/algopy_testing/primitives/string.py +++ b/src/algopy_testing/primitives/string.py @@ -6,12 +6,11 @@ class String(BytesBacked): - """Represents a UTF-8 encoded string backed by Bytes, accessible via - .bytes. + """Represents a UTF-8 encoded string backed by Bytes, accessible via .bytes. - Works with str literals instead of bytes literals. Due to lack of - AVM support for unicode, indexing and length operations are not - supported. Use .bytes.length for byte length. + Works with str literals instead of bytes literals. Due to lack of AVM support for + unicode, indexing and length operations are not supported. Use .bytes.length for + byte length. """ _value: bytes # underlying 'bytes' value representing the String diff --git a/src/algopy_testing/primitives/uint64.py b/src/algopy_testing/primitives/uint64.py index 37e40c5..b09834b 100644 --- a/src/algopy_testing/primitives/uint64.py +++ b/src/algopy_testing/primitives/uint64.py @@ -163,8 +163,8 @@ def __invert__(self) -> UInt64: return UInt64(~self.value & MAX_UINT64) def __index__(self) -> int: - """Return the internal integer value of the UInt64 for use in - indexing/slice expressions. + """Return the internal integer value of the UInt64 for use in indexing/slice + expressions. Returns: int: The internal integer value of the UInt64. diff --git a/src/algopy_testing/state/global_state.py b/src/algopy_testing/state/global_state.py index a9601b1..cd679d1 100644 --- a/src/algopy_testing/state/global_state.py +++ b/src/algopy_testing/state/global_state.py @@ -59,9 +59,8 @@ def __init__( def set_key(self, key: Bytes | String | bytes | str) -> None: """Set the key and apply any pending value. - Pending values are used for implicit keys in Contract - subclasses. They're stored until the 'Contract''s initialization - sets the key. + Pending values are used for implicit keys in Contract subclasses. They're stored + until the 'Contract''s initialization sets the key. """ match key: case bytes(bytes_key): @@ -73,7 +72,7 @@ def set_key(self, key: Bytes | String | bytes | str) -> None: case String() as key_str: self._key = key_str.bytes case _: - raise ValueError("Key must be bytes or str") + raise KeyError("Key must be bytes or str") if self._key and self._pending_value is not None: self.value = self._pending_value @@ -83,7 +82,7 @@ def set_key(self, key: Bytes | String | bytes | str) -> None: def key(self) -> algopy.Bytes: """Provides access to the raw storage key.""" if self._key is None: - raise ValueError("Key is not set") + raise KeyError("Key is not set") return self._key @property @@ -91,7 +90,7 @@ def value(self) -> _T: if self._key is None: if self._pending_value is not None: return self._pending_value - raise ValueError("Key is not set") + raise KeyError("Key is not set") try: native = lazy_context.ledger.get_global_state(self.app_id, self._key) except KeyError as e: diff --git a/tests/models/test_asset.py b/tests/models/test_asset.py new file mode 100644 index 0000000..e275ffe --- /dev/null +++ b/tests/models/test_asset.py @@ -0,0 +1,130 @@ +from collections.abc import Generator + +import pytest +from algopy import Account, Bytes, UInt64 +from algopy_testing import AlgopyTestContext, algopy_testing_context +from algopy_testing.models.asset import Asset, AssetFields + + +@pytest.fixture() +def context() -> Generator[AlgopyTestContext, None, None]: + with algopy_testing_context() as context: + yield context + + +def test_asset_initialization() -> None: + asset = Asset() + assert asset.id == UInt64(0) + + asset = Asset(123) + assert asset.id == UInt64(123) + + asset = Asset(UInt64(456)) + assert asset.id == UInt64(456) + + +def test_asset_int_property() -> None: + asset = Asset(789) + assert asset.int_ == 789 + + +def test_asset_from_int() -> None: + asset = Asset.from_int(101112) + assert isinstance(asset, Asset) + assert asset.id == UInt64(101112) + + +def test_asset_balance(context: AlgopyTestContext) -> None: + account = context.any.account() + asset = context.any.asset() + context.ledger.update_account( + account.public_key, opted_asset_balances={asset.id.value: UInt64(1000)} + ) + + assert asset.balance(account) == UInt64(1000) + + +def test_asset_balance_not_opted_in(context: AlgopyTestContext) -> None: + account = context.any.account() + asset = context.any.asset() + + with pytest.raises(ValueError, match="The asset is not opted into the account!"): + asset.balance(account) + + +def test_asset_frozen() -> None: + asset = Asset(1) + account = Account() + + with pytest.raises( + NotImplementedError, + match="The 'frozen' method is being executed in a python testing context", + ): + asset.frozen(account) + + +def test_asset_attributes(context: AlgopyTestContext) -> None: + asset_fields: AssetFields = { + "total": UInt64(1000000), + "decimals": UInt64(6), + "default_frozen": False, + "unit_name": Bytes(b"TEST"), + "name": Bytes(b"Test Asset"), + "url": Bytes(b"https://test.com"), + "metadata_hash": Bytes(b"\x00" * 32), + "manager": Account(), + "reserve": Account(), + "freeze": Account(), + "clawback": Account(), + "creator": Account(), + } + + asset = context.any.asset(**asset_fields) + + for field, value in asset_fields.items(): + assert getattr(asset, field) == value + + +def test_asset_attribute_error(context: AlgopyTestContext) -> None: + asset = context.any.asset() + + with pytest.raises( + AttributeError, match="The value for 'non_existent' in the test context is None" + ): + asset.non_existent # noqa: B018 + + +def test_asset_not_in_context() -> None: + asset = Asset(999) + with pytest.raises(ValueError, match="Test context is not initialized!"): + asset.total # noqa: B018 + + +def test_asset_equality() -> None: + asset1 = Asset(1) + asset2 = Asset(1) + asset3 = Asset(2) + + assert asset1 == asset2 + assert asset1 != asset3 + assert asset1 == 1 + assert asset1 != 2 + + +def test_asset_bool() -> None: + assert bool(Asset(1)) is True + assert bool(Asset(0)) is False + + +def test_asset_hash() -> None: + asset1 = Asset(1) + asset2 = Asset(1) + asset3 = Asset(2) + + assert hash(asset1) == hash(asset2) + assert hash(asset1) != hash(asset3) + + # Test that assets can be used as dictionary keys + asset_dict = {asset1: "Asset 1", asset3: "Asset 3"} + assert asset_dict[asset2] == "Asset 1" + assert asset_dict[asset3] == "Asset 3" diff --git a/tests/state/test_global_state.py b/tests/state/test_global_state.py index 46f9523..9cbbfa2 100644 --- a/tests/state/test_global_state.py +++ b/tests/state/test_global_state.py @@ -1,12 +1,12 @@ from collections.abc import Generator +from typing import Any -import algopy_testing import pytest +from algopy_testing import arc4 from algopy_testing._context_helpers.context_storage import algopy_testing_context from algopy_testing.context import AlgopyTestContext - -from tests.artifacts.StateOps.contract import GlobalStateContract -from tests.common import AVMInvoker +from algopy_testing.primitives.bytes import Bytes +from algopy_testing.state.global_state import GlobalState @pytest.fixture() @@ -17,38 +17,118 @@ def context() -> Generator[AlgopyTestContext, None, None]: @pytest.mark.usefixtures("context") -@pytest.mark.parametrize( - ("method_name", "expected_type"), - [ - ("get_implicit_key_arc4_uint", algopy_testing.arc4.UInt64), - ("get_implicit_key_arc4_string", algopy_testing.arc4.String), - ("get_implicit_key_arc4_byte", algopy_testing.arc4.Byte), - ("get_implicit_key_arc4_bool", algopy_testing.arc4.Bool), - ("get_implicit_key_arc4_address", algopy_testing.arc4.Address), - ("get_implicit_key_arc4_uint128", algopy_testing.arc4.UInt128), - ("get_implicit_key_arc4_dynamic_bytes", algopy_testing.arc4.DynamicBytes), - ("get_arc4_uint", algopy_testing.arc4.UInt64), - ("get_arc4_string", algopy_testing.arc4.String), - ("get_arc4_byte", algopy_testing.arc4.Byte), - ("get_arc4_bool", algopy_testing.arc4.Bool), - ("get_arc4_address", algopy_testing.arc4.Address), - ("get_arc4_uint128", algopy_testing.arc4.UInt128), - ("get_arc4_dynamic_bytes", algopy_testing.arc4.DynamicBytes), - ], -) -def test_get_global_arc4_value( - get_global_state_avm_result: AVMInvoker, - localnet_creator_address: str, - method_name: str, - expected_type: type, -) -> None: - avm_result = get_global_state_avm_result(method_name) - - with algopy_testing_context(default_sender=localnet_creator_address): - contract = GlobalStateContract() - test_result = getattr(contract, method_name)() - assert isinstance(test_result, expected_type) - if isinstance(test_result, algopy_testing.arc4.Address): - assert test_result.native.public_key == avm_result - else: - assert test_result.native == avm_result # type: ignore[attr-defined] +class TestGlobalState: + @pytest.mark.parametrize( + ("type_or_value", "expected_type", "expected_value"), + [ + (arc4.UInt64, arc4.UInt64, None), + (arc4.String("Hello"), arc4.String, "Hello"), + (arc4.Bool(True), arc4.Bool, True), + ( + arc4.Address("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ"), + arc4.Address, + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ", + ), + (Bytes(b"test"), Bytes, b"test"), + ], + ) + def test_initialization( + self, + context: AlgopyTestContext, + type_or_value: Any, + expected_type: type[Any], + expected_value: Any, + ) -> None: + with context.txn.create_group(gtxns=[context.any.txn.application_call()]): + gs = GlobalState(type_or_value, key="test_key") + assert gs.type_ == expected_type + assert gs.key == Bytes(b"test_key") + if expected_value is not None: + response = gs.value.native if hasattr(gs.value, "native") else gs.value + assert response == expected_value + + @pytest.mark.parametrize( + ("key", "expected_bytes"), + [ + (b"bytes_key", b"bytes_key"), + ("str_key", b"str_key"), + (Bytes(b"bytes_obj_key"), b"bytes_obj_key"), + ("", b""), # Test empty string + ], + ) + def test_set_key(self, context: AlgopyTestContext, key: Any, expected_bytes: bytes) -> None: + with context.txn.create_group(gtxns=[context.any.txn.application_call()]): + gs = GlobalState(arc4.UInt64) + gs.set_key(key) + assert gs.key == Bytes(expected_bytes) + + def test_set_key_invalid(self, context: AlgopyTestContext) -> None: + with context.txn.create_group(gtxns=[context.any.txn.application_call()]): + gs = GlobalState(arc4.UInt64) + with pytest.raises(KeyError, match="Key must be bytes or str"): + gs.set_key(123) # type: ignore[arg-type] + + @pytest.mark.parametrize( + ("type_", "value"), + [ + (arc4.UInt64, 42), + (arc4.String, "test"), + (arc4.Bool, True), + (Bytes, b"test"), + ], + ) + def test_value_operations(self, context: AlgopyTestContext, type_: Any, value: Any) -> None: + with context.txn.create_group(gtxns=[context.any.txn.application_call()]): + gs = GlobalState(type_, key="test_key") + + gs.value = type_(value) + response = gs.value.native if hasattr(gs.value, "native") else gs.value + assert response == value + assert isinstance(gs.value, type_) + + del gs.value + with pytest.raises(ValueError, match="Value is not set"): + _ = gs.value + + def test_get_method(self, context: AlgopyTestContext) -> None: + with context.txn.create_group(gtxns=[context.any.txn.application_call()]): + gs = GlobalState(arc4.UInt64, key="test_uint64") + + assert gs.get(default=arc4.UInt64(0)) == 0 + assert gs.get() == 0 # Default initialization + + gs.value = arc4.UInt64(42) + assert gs.get() == 42 + + def test_maybe_method(self, context: AlgopyTestContext) -> None: + with context.txn.create_group(gtxns=[context.any.txn.application_call()]): + gs = GlobalState(arc4.UInt64, key="test_uint64") + + value, exists = gs.maybe() + assert value is None + assert exists is False + + gs.value = arc4.UInt64(42) + value, exists = gs.maybe() + assert value == 42 + assert exists is True + + def test_pending_value(self, context: AlgopyTestContext) -> None: + with context.txn.create_group(gtxns=[context.any.txn.application_call()]): + gs = GlobalState(arc4.UInt64(100)) + assert gs._pending_value == 100 + + gs.set_key("test_key") + assert gs.value == 100 + assert gs._pending_value is None + + def test_description(self, context: AlgopyTestContext) -> None: + with context.txn.create_group(gtxns=[context.any.txn.application_call()]): + gs = GlobalState(arc4.UInt64, key="test_key", description="Test description") + assert gs.description == "Test description" + + def test_app_id(self, context: AlgopyTestContext) -> None: + with context.txn.create_group(gtxns=[context.any.txn.application_call()]): + gs = GlobalState(arc4.UInt64, key="test_key") + + assert gs.app_id == context.txn.last_active.app_id.id diff --git a/tests/test_op.py b/tests/test_op.py index 91d32df..604b2a0 100644 --- a/tests/test_op.py +++ b/tests/test_op.py @@ -13,6 +13,7 @@ from algokit_utils import LogicError, get_localnet_default_account from algopy_testing import algopy_testing_context, op from algopy_testing.context import AlgopyTestContext +from algopy_testing.op.block import Block from algopy_testing.primitives.bytes import Bytes from algopy_testing.primitives.uint64 import UInt64 from algopy_testing.utils import convert_native_to_stack @@ -901,7 +902,7 @@ def test_itxn_ops(context: AlgopyTestContext) -> None: appl_itxn = itxn_group.application_call(0) pay_itxn = itxn_group.payment(1) - # TODO: 1.0 also test other array fields, apps, accounts, applications, assets + # Test application call transaction fields assert appl_itxn.approval_program == algopy.Bytes.from_hex("068101068101") assert appl_itxn.clear_state_program == algopy.Bytes.from_hex("068101") approval_pages = [ @@ -911,9 +912,65 @@ def test_itxn_ops(context: AlgopyTestContext) -> None: assert approval_pages == [appl_itxn.approval_program] assert appl_itxn.on_completion == algopy.OnCompleteAction.DeleteApplication assert appl_itxn.fee == algopy.UInt64(algosdk.constants.MIN_TXN_FEE) - + assert appl_itxn.sender == context.get_app_for_contract(contract).address + # NOTE: would implementing emulation for this behavior be useful + # in unit testing context (vs integration tests)? + # considering we don't emulate balance (transfer, accounting for fees and etc) + assert appl_itxn.app_id == 0 + assert appl_itxn.type == algopy.TransactionType.ApplicationCall + assert appl_itxn.type_bytes == algopy.Bytes(b"appl") + + # Test payment transaction fields assert pay_itxn.receiver == context.default_sender assert pay_itxn.amount == algopy.UInt64(1000) + assert pay_itxn.sender == context.get_app_for_contract(contract).address + assert pay_itxn.type == algopy.TransactionType.Payment + assert pay_itxn.type_bytes == algopy.Bytes(b"pay") + + # Test common fields for both transactions + for itxn in [appl_itxn, pay_itxn]: + assert isinstance(itxn.sender, algopy.Account) + assert isinstance(itxn.fee, algopy.UInt64) + assert isinstance(itxn.first_valid, algopy.UInt64) + assert isinstance(itxn.last_valid, algopy.UInt64) + assert isinstance(itxn.note, algopy.Bytes) + assert isinstance(itxn.lease, algopy.Bytes) + assert isinstance(itxn.txn_id, algopy.Bytes) + + # Test logs (should be empty for newly created transactions as its a void method) + assert context.txn.last_active.num_logs == algopy.UInt64(0) + assert context.txn.last_active.last_log == algopy.Bytes(b"") + + # Test created_app and created_asset (should be created for these transactions) + assert hasattr(appl_itxn, "created_app") + assert hasattr(pay_itxn, "created_asset") + + +def test_blk_seed_existing_block(context: AlgopyTestContext) -> None: + block_index = 42 + block_seed = 123 + context.ledger.set_block(block_index, block_seed, 1234567890) + result = Block.blk_seed(UInt64(block_index)) + assert op.btoi(result) == block_seed -# def test_ +@pytest.mark.usefixtures("context") +def test_blk_seed_missing_block() -> None: + block_index = 42 + with pytest.raises(KeyError, match=f"Block {block_index}*"): + Block.blk_seed(UInt64(block_index)) + + +def test_blk_timestamp_existing_block(context: AlgopyTestContext) -> None: + block_index = 42 + block_timestamp = 1234567890 + context.ledger.set_block(block_index, 123, block_timestamp) + result = Block.blk_timestamp(UInt64(block_index)) + assert result == UInt64(block_timestamp) + + +@pytest.mark.usefixtures("context") +def test_blk_timestamp_missing_block() -> None: + block_index = 42 + with pytest.raises(KeyError, match=f"Block {block_index}*"): + Block.blk_timestamp(UInt64(block_index))