From b285255d9d4d2bf51ebc01da1bd74e5f628e25f1 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Tue, 8 Nov 2022 14:58:17 -0500 Subject: [PATCH 01/27] Add mypy dependency and include it in CI --- .circleci/config.yml | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index e0e2bfdb..0a42817e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,6 +25,7 @@ jobs: - checkout - run: pip install -r requirements.txt - run: black --check . + - run: mypy algosdk - run: pytest tests/unit_tests integration-test: parameters: diff --git a/requirements.txt b/requirements.txt index 95843578..98581fea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ black==22.3.0 glom==20.11.0 pytest==6.2.5 +mypy==0.990 git+https://github.com/behave/behave From 2bbc8a3ad29d80266fb77b1539cec3d9c494ffe4 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Tue, 8 Nov 2022 14:59:17 -0500 Subject: [PATCH 02/27] Include documentation for type linting --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 9a14cc06..3fe5ab75 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,10 @@ Format code: * `black .` +Lint types: + +* `mypy algosdk` + ## Quick start Here's a simple example you can run without a node. From ea663f16a92cc032bfeff0a5a2304e8f7eb078d1 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Thu, 10 Nov 2022 14:45:20 -0500 Subject: [PATCH 03/27] Fix type errors in `constants.py` --- algosdk/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algosdk/constants.py b/algosdk/constants.py index 6ddba7d4..09228091 100644 --- a/algosdk/constants.py +++ b/algosdk/constants.py @@ -9,7 +9,7 @@ """str: header key for indexer requests""" UNVERSIONED_PATHS = ["/health", "/versions", "/metrics", "/genesis"] """str[]: paths that don't use the version path prefix""" -NO_AUTH = [] +NO_AUTH: list[str] = [] """str[]: requests that don't require authentication""" From 8df4213d85de1a991c079ee64f87567e2052d1bd Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Thu, 10 Nov 2022 14:45:37 -0500 Subject: [PATCH 04/27] Fix type errors in `source_map.py` --- algosdk/source_map.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/algosdk/source_map.py b/algosdk/source_map.py index b459f9a6..0e67cdf9 100644 --- a/algosdk/source_map.py +++ b/algosdk/source_map.py @@ -1,4 +1,4 @@ -from typing import Dict, Any, List, Tuple +from typing import Dict, Any, List, Final, Optional, cast from algosdk.error import SourceMapVersionError @@ -43,14 +43,14 @@ def __init__(self, source_map: Dict[str, Any]): self.line_to_pc[last_line].append(index) self.pc_to_line[index] = last_line - def get_line_for_pc(self, pc: int) -> int: + def get_line_for_pc(self, pc: int) -> Optional[int]: return self.pc_to_line.get(pc, None) - def get_pcs_for_line(self, line: int) -> List[int]: + def get_pcs_for_line(self, line: int) -> Optional[List[int]]: return self.line_to_pc.get(line, None) -def _decode_int_value(value: str) -> int: +def _decode_int_value(value: str) -> Optional[int]: # Mappings may have up to 5 segments: # Third segment represents the zero-based starting line in the original source represented. decoded_value = _base64vlq_decode(value) @@ -62,19 +62,20 @@ def _decode_int_value(value: str) -> int: """ _b64chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -_b64table = [None] * (max(_b64chars) + 1) +_b64table: Final[list[Optional[int]]] = [None] * (max(_b64chars) + 1) for i, b in enumerate(_b64chars): _b64table[b] = i shiftsize, flag, mask = 5, 1 << 5, (1 << 5) - 1 -def _base64vlq_decode(vlqval: str) -> Tuple[int]: +def _base64vlq_decode(vlqval: str) -> tuple[int, ...]: """Decode Base64 VLQ value""" results = [] shift = value = 0 # use byte values and a table to go from base64 characters to integers for v in map(_b64table.__getitem__, vlqval.encode("ascii")): + v = cast(int, v) # force int type given context value += (v & mask) << shift if v & flag: shift += shiftsize @@ -82,4 +83,4 @@ def _base64vlq_decode(vlqval: str) -> Tuple[int]: # determine sign and add to results results.append((value >> 1) * (-1 if value & 1 else 1)) shift = value = 0 - return results + return tuple(results) From 1dac0400964c9bc40f37ac117e72d14e34ea7a2a Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Thu, 10 Nov 2022 15:07:12 -0500 Subject: [PATCH 05/27] Fix type errors in `dryrun_results.py` --- algosdk/dryrun_results.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/algosdk/dryrun_results.py b/algosdk/dryrun_results.py index cc0efdd6..751aa263 100644 --- a/algosdk/dryrun_results.py +++ b/algosdk/dryrun_results.py @@ -1,5 +1,5 @@ import base64 -from typing import List +from typing import List, Optional, cast class StackPrinterConfig: @@ -63,13 +63,13 @@ def attrname(field): def app_call_rejected(self) -> bool: return ( False - if self.app_call_messages is None - else "REJECT" in self.app_call_messages + if self.app_call_messages is None # type: ignore # dynamic attribute + else "REJECT" in self.app_call_messages # type: ignore # dynamic attribute ) def logic_sig_rejected(self) -> bool: - if self.logic_sig_messages is not None: - return "REJECT" in self.logic_sig_messages + if self.logic_sig_messages is not None: # type: ignore # dynamic attribute + return "REJECT" in self.logic_sig_messages # type: ignore # dynamic attribute return False @classmethod @@ -123,16 +123,17 @@ def trace( return "\n".join(trace) + "\n" - def app_trace(self, spc: StackPrinterConfig = None) -> str: + def app_trace(self, spc: Optional[StackPrinterConfig] = None) -> str: if not hasattr(self, "app_call_trace"): return "" if spc == None: spc = StackPrinterConfig(top_of_stack_first=False) + spc = cast(StackPrinterConfig, spc) return self.trace(self.app_call_trace, self.disassembly, spc=spc) - def lsig_trace(self, spc: StackPrinterConfig = None) -> str: + def lsig_trace(self, spc: Optional[StackPrinterConfig] = None) -> str: if not hasattr(self, "logic_sig_trace"): return "" @@ -143,7 +144,7 @@ def lsig_trace(self, spc: StackPrinterConfig = None) -> str: spc = StackPrinterConfig(top_of_stack_first=False) return self.trace( - self.logic_sig_trace, self.logic_sig_disassembly, spc=spc + self.logic_sig_trace, self.logic_sig_disassembly, spc=spc # type: ignore # dynamic attribute ) @@ -151,9 +152,6 @@ class DryrunTrace: def __init__(self, trace: List[dict]): self.trace = [DryrunTraceLine(line) for line in trace] - def get_trace(self) -> List[str]: - return [line.trace_line() for line in self.trace] - class DryrunTraceLine: def __init__(self, tl): @@ -182,10 +180,13 @@ def __str__(self) -> str: return "0x" + base64.b64decode(self.bytes).hex() return str(self.int) - def __eq__(self, other: "DryrunStackValue"): + def __eq__(self, other: object): return ( - self.type == other.type + hasattr(other, "type") + and self.type == other.type + and hasattr(other, "bytes") and self.bytes == other.bytes + and hasattr(other, "int") and self.int == other.int ) @@ -202,7 +203,7 @@ def scratch_to_string( if not curr_scratch: return "" - new_idx = None + new_idx: Optional[int] = None for idx in range(len(curr_scratch)): if idx >= len(prev_scratch): new_idx = idx @@ -214,6 +215,7 @@ def scratch_to_string( if new_idx == None: return "" + new_idx = cast(int, new_idx) # discharge None type return "{} = {}".format(new_idx, curr_scratch[new_idx]) From 80f367f25ebfe7622473c783b255585c3d59fc74 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Thu, 10 Nov 2022 15:31:48 -0500 Subject: [PATCH 06/27] Add msgpack-types package --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 98581fea..c3d13e9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ black==22.3.0 glom==20.11.0 pytest==6.2.5 mypy==0.990 +msgpack-types==0.2.0 git+https://github.com/behave/behave From 217a022ef1e4a890248b5138e0b5793365fc2f16 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Thu, 10 Nov 2022 15:33:51 -0500 Subject: [PATCH 07/27] Fix type errors in `template.py` --- algosdk/template.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/algosdk/template.py b/algosdk/template.py index 59890510..bb4d3de4 100644 --- a/algosdk/template.py +++ b/algosdk/template.py @@ -3,6 +3,7 @@ from . import error, encoding, constants, transaction, logic, account from Cryptodome.Hash import SHA256, keccak import base64 +from typing import Optional class Template: @@ -329,8 +330,8 @@ def __init__( receiver: str, amount: int, first_valid: int, - last_valid: int = None, - close_remainder_address: str = None, + last_valid: Optional[int] = None, + close_remainder_address: Optional[str] = None, ): self.lease_value = bytes( [random.randint(0, 255) for x in range(constants.lease_length)] From ab70c7d6a275a94315f61c5030d762899c99e3bf Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Thu, 10 Nov 2022 15:34:45 -0500 Subject: [PATCH 08/27] Fix type errors in `future/template.py` --- algosdk/future/template.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/algosdk/future/template.py b/algosdk/future/template.py index 77b424c1..845ac5dc 100644 --- a/algosdk/future/template.py +++ b/algosdk/future/template.py @@ -4,6 +4,7 @@ from . import transaction from Cryptodome.Hash import SHA256, keccak import base64 +from typing import Optional class Template: @@ -318,7 +319,7 @@ def __init__( receiver: str, amount: int, sp, - close_remainder_address: str = None, + close_remainder_address: Optional[str] = None, ): self.lease_value = bytes( [random.randint(0, 255) for x in range(constants.lease_length)] From c1e65044cb9bd892dd227b45aff280cdebde88bb Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Thu, 10 Nov 2022 16:01:18 -0500 Subject: [PATCH 09/27] Fix type errors in `future/transaction.py` --- algosdk/future/transaction.py | 51 ++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/algosdk/future/transaction.py b/algosdk/future/transaction.py index 15eab421..ce0401d6 100644 --- a/algosdk/future/transaction.py +++ b/algosdk/future/transaction.py @@ -3,7 +3,7 @@ import warnings import msgpack from enum import IntEnum -from typing import List, Union +from typing import List, Union, Optional, cast from collections import OrderedDict from algosdk import account, constants, encoding, error, logic, transaction @@ -2161,7 +2161,9 @@ class SignedTransaction: authorizing_address (str) """ - def __init__(self, transaction, signature, authorizing_address=None): + def __init__( + self, transaction: Transaction, signature, authorizing_address=None + ): self.signature = signature self.transaction = transaction self.authorizing_address = authorizing_address @@ -2291,7 +2293,9 @@ def undictify(d): return mtx @staticmethod - def merge(part_stxs: List["MultisigTransaction"]) -> "MultisigTransaction": + def merge( + part_stxs: List["MultisigTransaction"], + ) -> Optional["MultisigTransaction"]: """ Merge partially signed multisig transactions. @@ -2733,7 +2737,9 @@ class LogicSigAccount: Represents an account that can sign with a LogicSig program. """ - def __init__(self, program: bytes, args: List[bytes] = None) -> None: + def __init__( + self, program: bytes, args: Optional[list[bytes]] = None + ) -> None: """ Create a new LogicSigAccount. By default this will create an escrow LogicSig account. Call `sign` or `sign_multisig` on the newly created @@ -2746,7 +2752,7 @@ def __init__(self, program: bytes, args: List[bytes] = None) -> None: program. """ self.lsig = LogicSig(program, args) - self.sigkey = None + self.sigkey: Optional[bytes] = None def dictify(self): od = OrderedDict() @@ -2907,7 +2913,7 @@ def __init__( self.lsig = lsig if transaction.sender != lsigAddr: - self.auth_addr = lsigAddr + self.auth_addr: Optional[str] = lsigAddr else: self.auth_addr = None @@ -3276,9 +3282,9 @@ def create_dryrun( accts.append(txn.sender) # Add foreign args if they're set - if txn.accounts: + if hasattr(txn, "accounts"): accts.extend(txn.accounts) - if txn.foreign_apps: + if hasattr(txn, "foreign_apps"): apps.extend(txn.foreign_apps) accts.extend( [ @@ -3286,41 +3292,56 @@ def create_dryrun( for aidx in txn.foreign_apps ] ) - if txn.foreign_assets: + if hasattr(txn, "foreign_assets"): assets.extend(txn.foreign_assets) # For creates, we need to add the source directly from the transaction - if txn.index == 0: + if hasattr(txn, "index") and txn.index == 0: appId = defaultAppId # Make up app id, since tealdbg/dryrun doesnt like 0s # https://github.com/algorand/go-algorand/blob/e466aa18d4d963868d6d15279b1c881977fa603f/libgoal/libgoal.go#L1089-L1090 - ls = txn.local_schema + ls = txn.local_schema if hasattr(txn, "local_schema") else None if ls is not None: ls = models.ApplicationStateSchema( ls.num_uints, ls.num_byte_slices ) - gs = txn.global_schema + gs = ( + txn.global_schema + if hasattr(txn, "global_schema") + else None + ) if gs is not None: gs = models.ApplicationStateSchema( gs.num_uints, gs.num_byte_slices ) + approval_program = ( + txn.approval_program + if hasattr(txn, "approval_program") + else None + ) + clear_program = ( + txn.clear_program + if hasattr(txn, "clear_program") + else None + ) app_infos.append( models.Application( id=appId, params=models.ApplicationParams( creator=txn.sender, - approval_program=txn.approval_program, - clear_state_program=txn.clear_program, + approval_program=approval_program, + clear_state_program=clear_program, local_state_schema=ls, global_state_schema=gs, ), ) ) else: - apps.append(txn.index) + if hasattr(txn, "index"): + apps.append(txn.index) # Dedupe and filter none, reset programs to bytecode instead of b64 apps = [i for i in set(apps) if i] From 6714597cf81c42db6aab2dd4b20f2e950939efe8 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Thu, 10 Nov 2022 16:13:28 -0500 Subject: [PATCH 10/27] Fix type errors in `base_type.py` --- algosdk/abi/base_type.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/algosdk/abi/base_type.py b/algosdk/abi/base_type.py index 0e6ac628..931eb790 100644 --- a/algosdk/abi/base_type.py +++ b/algosdk/abi/base_type.py @@ -77,8 +77,8 @@ def from_string(s: str) -> "ABIType": elif s.endswith("]"): matches = re.search(STATIC_ARRAY_REGEX, s) try: - static_length = int(matches.group(2)) - array_type = ABIType.from_string(matches.group(1)) + static_length = int(matches.group(2)) # type: ignore # we allow attribute errors to be caught + array_type = ABIType.from_string(matches.group(1)) # type: ignore # we allow attribute errors to be caught return ArrayStaticType(array_type, static_length) except Exception as e: raise error.ABITypeError( @@ -103,8 +103,8 @@ def from_string(s: str) -> "ABIType": elif s.startswith("ufixed"): matches = re.search(UFIXED_REGEX, s) try: - bit_size = int(matches.group(1)) - precision = int(matches.group(2)) + bit_size = int(matches.group(1)) # type: ignore # we allow attribute errors to be caught + precision = int(matches.group(2)) # type: ignore # we allow attribute errors to be caught return UfixedType(bit_size, precision) except Exception as e: raise error.ABITypeError( @@ -126,7 +126,7 @@ def from_string(s: str) -> "ABIType": tuple_list.append(tt) elif isinstance(tup, list): tts = [ABIType.from_string(t_) for t_ in tup] - tuple_list.append(tts) + tuple_list.extend(tts) else: raise error.ABITypeError( "cannot convert {} to an ABI type".format(tup) From 1559a62fab97777d265d09a38f39518fe08a26dc Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Tue, 15 Nov 2022 14:40:47 -0500 Subject: [PATCH 11/27] Fix string format errors with bytestrings --- algosdk/abi/address_type.py | 11 +++++------ algosdk/abi/array_dynamic_type.py | 4 +--- algosdk/abi/array_static_type.py | 4 +--- algosdk/abi/bool_type.py | 6 ++---- algosdk/abi/byte_type.py | 6 ++---- algosdk/abi/tuple_type.py | 6 ++---- algosdk/abi/ufixed_type.py | 4 +--- algosdk/abi/uint_type.py | 4 +--- 8 files changed, 15 insertions(+), 30 deletions(-) diff --git a/algosdk/abi/address_type.py b/algosdk/abi/address_type.py index 57050c71..55fd9c85 100644 --- a/algosdk/abi/address_type.py +++ b/algosdk/abi/address_type.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, cast from algosdk.abi.base_type import ABIType from algosdk.abi.byte_type import ByteType @@ -53,15 +53,16 @@ def encode(self, value: Union[str, bytes]) -> bytes: value = encoding.decode_address(value) except Exception as e: raise error.ABIEncodingError( - "cannot encode the following address: {}".format(value) + f"cannot encode the following address: {value!r}" ) from e elif ( not (isinstance(value, bytes) or isinstance(value, bytearray)) or len(value) != 32 ): raise error.ABIEncodingError( - "cannot encode the following public key: {}".format(value) + f"cannot encode the following public key: {value!r}" ) + value = cast(bytes, value) return bytes(value) def decode(self, bytestring: Union[bytearray, bytes]) -> str: @@ -82,9 +83,7 @@ def decode(self, bytestring: Union[bytearray, bytes]) -> str: or len(bytestring) != 32 ): raise error.ABIEncodingError( - "address string must be in bytes and correspond to a byte[32]: {}".format( - bytestring - ) + f"address string must be in bytes and correspond to a byte[32]: {bytestring!r}" ) # Return the base32 encoded address string return encoding.encode_address(bytestring) diff --git a/algosdk/abi/array_dynamic_type.py b/algosdk/abi/array_dynamic_type.py index 97be5b15..ef8adff6 100644 --- a/algosdk/abi/array_dynamic_type.py +++ b/algosdk/abi/array_dynamic_type.py @@ -58,9 +58,7 @@ def encode(self, value_array: Union[List[Any], bytes, bytearray]) -> bytes: or isinstance(value_array, bytearray) ) and not isinstance(self.child_type, ByteType): raise error.ABIEncodingError( - "cannot pass in bytes when the type of the array is not ByteType: {}".format( - value_array - ) + f"cannot pass in bytes when the type of the array is not ByteType: {value_array!r}" ) converted_tuple = self._to_tuple_type(len(value_array)) length_to_encode = len(converted_tuple.child_types).to_bytes( diff --git a/algosdk/abi/array_static_type.py b/algosdk/abi/array_static_type.py index 059de3e7..22fe7ce1 100644 --- a/algosdk/abi/array_static_type.py +++ b/algosdk/abi/array_static_type.py @@ -81,9 +81,7 @@ def encode(self, value_array: Union[List[Any], bytes, bytearray]) -> bytes: or isinstance(value_array, bytearray) ) and not isinstance(self.child_type, ByteType): raise error.ABIEncodingError( - "cannot pass in bytes when the type of the array is not ByteType: {}".format( - value_array - ) + f"cannot pass in bytes when the type of the array is not ByteType: {value_array!r}" ) converted_tuple = self._to_tuple_type() return converted_tuple.encode(value_array) diff --git a/algosdk/abi/bool_type.py b/algosdk/abi/bool_type.py index ee73ae5f..28e3661d 100644 --- a/algosdk/abi/bool_type.py +++ b/algosdk/abi/bool_type.py @@ -60,9 +60,7 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> bool: or len(bytestring) != 1 ): raise error.ABIEncodingError( - "value string must be in bytes and correspond to a bool: {}".format( - bytestring - ) + f"value string must be in bytes and correspond to a bool: {bytestring!r}" ) if bytestring == b"\x80": return True @@ -70,5 +68,5 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> bool: return False else: raise error.ABIEncodingError( - "boolean value could not be decoded: {}".format(bytestring) + f"boolean value could not be decoded: {bytestring!r}" ) diff --git a/algosdk/abi/byte_type.py b/algosdk/abi/byte_type.py index e719e80e..f52ae582 100644 --- a/algosdk/abi/byte_type.py +++ b/algosdk/abi/byte_type.py @@ -42,7 +42,7 @@ def encode(self, value: int) -> bytes: ) return bytes([value]) - def decode(self, bytestring: Union[bytes, bytearray]) -> bytes: + def decode(self, bytestring: Union[bytes, bytearray]) -> int: """ Decodes a bytestring to a single byte. @@ -60,8 +60,6 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> bytes: or len(bytestring) != 1 ): raise error.ABIEncodingError( - "value string must be in bytes and correspond to a byte: {}".format( - bytestring - ) + f"value string must be in bytes and correspond to a byte: {bytestring!r}" ) return bytestring[0] diff --git a/algosdk/abi/tuple_type.py b/algosdk/abi/tuple_type.py index baa0dda6..830cee88 100644 --- a/algosdk/abi/tuple_type.py +++ b/algosdk/abi/tuple_type.py @@ -291,9 +291,7 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> list: array_index += curr_len if array_index >= len(bytestring) and i != len(tuple_elements) - 1: raise error.ABIEncodingError( - "input string is not long enough to be decoded: {}".format( - bytestring - ) + f"input string is not long enough to be decoded: {bytestring!r}" ) i += 1 @@ -302,7 +300,7 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> list: array_index = len(bytestring) if array_index < len(bytestring): raise error.ABIEncodingError( - "input string was not fully consumed: {}".format(bytestring) + f"input string was not fully consumed: {bytestring!r}" ) # Check dynamic element partitions diff --git a/algosdk/abi/ufixed_type.py b/algosdk/abi/ufixed_type.py index 6ab2c778..708dce27 100644 --- a/algosdk/abi/ufixed_type.py +++ b/algosdk/abi/ufixed_type.py @@ -97,9 +97,7 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> int: or len(bytestring) != self.bit_size // 8 ): raise error.ABIEncodingError( - "value string must be in bytes and correspond to a ufixed{}x{}: {}".format( - self.bit_size, self.precision, bytestring - ) + f"value string must be in bytes and correspond to a ufixed{self.bit_size}x{self.precision}: {bytestring!r}" ) # Convert bytes into an unsigned integer numerator return int.from_bytes(bytestring, byteorder="big", signed=False) diff --git a/algosdk/abi/uint_type.py b/algosdk/abi/uint_type.py index 7e10c2ca..aaf65643 100644 --- a/algosdk/abi/uint_type.py +++ b/algosdk/abi/uint_type.py @@ -82,9 +82,7 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> int: or len(bytestring) != self.bit_size // 8 ): raise error.ABIEncodingError( - "value string must be in bytes and correspond to a uint{}: {}".format( - self.bit_size, bytestring - ) + f"value string must be in bytes and correspond to a uint{self.bit_size}: {bytestring!r}" ) # Convert bytes into an unsigned integer return int.from_bytes(bytestring, byteorder="big", signed=False) From 3d1995176c40fcfb57143770ba28fc9a4d0d27f5 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Tue, 15 Nov 2022 14:51:06 -0500 Subject: [PATCH 12/27] Fix type errors in `tuple_type.py` --- algosdk/abi/tuple_type.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/algosdk/abi/tuple_type.py b/algosdk/abi/tuple_type.py index 830cee88..2f46cca5 100644 --- a/algosdk/abi/tuple_type.py +++ b/algosdk/abi/tuple_type.py @@ -1,4 +1,4 @@ -from typing import Any, List, Union +from typing import Any, List, Union, Optional, cast from algosdk.abi.base_type import ABI_LENGTH_SIZE, ABIType from algosdk.abi.bool_type import BoolType @@ -175,8 +175,11 @@ def encode(self, values: Union[List[Any], bytes, bytearray]) -> bytes: "expected before index should have number of bool mod 8 equal 0" ) after = min(7, after) + consecutive_bool_list = cast( + list[bool], values[i : i + after + 1] + ) compressed_int = TupleType._compress_multiple_bool( - values[i : i + after + 1] + consecutive_bool_list ) heads.append(bytes([compressed_int])) i += after @@ -229,10 +232,10 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> list: "value string must be in bytes: {}".format(bytestring) ) tuple_elements = self.child_types - dynamic_segments = ( - list() - ) # Store the start and end of a dynamic element - value_partitions = list() + dynamic_segments: list[ + list[int] + ] = list() # Store the start and end of a dynamic element + value_partitions: list[Optional[bytes | bytearray]] = list() i = 0 array_index = 0 From 748db3db16e5ee68d3682e3f847ad0b060b0d02a Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Tue, 15 Nov 2022 15:10:02 -0500 Subject: [PATCH 13/27] Fix type errors in `method.py` --- algosdk/abi/method.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/algosdk/abi/method.py b/algosdk/abi/method.py index 08397de0..58ef151f 100644 --- a/algosdk/abi/method.py +++ b/algosdk/abi/method.py @@ -1,10 +1,20 @@ import json -from typing import List, Union +from typing import List, Union, Optional, TypedDict from Cryptodome.Hash import SHA512 from algosdk import abi, constants, error +# In Python 3.11+ the following classes should be combined using `NotRequired` +class MethodDict_Optional(TypedDict, total=False): + desc: str + + +class MethodDict(MethodDict_Optional): + name: str + args: list[dict] + returns: dict + class Method: """ @@ -23,7 +33,7 @@ def __init__( name: str, args: List["Argument"], returns: "Returns", - desc: str = None, + desc: Optional[str] = None, ) -> None: self.name = name self.args = args @@ -108,11 +118,12 @@ def from_signature(s: str) -> "Method": return_type = Returns(tokens[-1]) return Method(name=tokens[0], args=argument_list, returns=return_type) - def dictify(self) -> dict: - d = {} - d["name"] = self.name - d["args"] = [arg.dictify() for arg in self.args] - d["returns"] = self.returns.dictify() + def dictify(self) -> MethodDict: + d: MethodDict = { + "name": self.name, + "args": [arg.dictify() for arg in self.args], + "returns": self.returns.dictify(), + } if self.desc: d["desc"] = self.desc return d @@ -156,12 +167,15 @@ class Argument: """ def __init__( - self, arg_type: str, name: str = None, desc: str = None + self, + arg_type: str, + name: Optional[str] = None, + desc: Optional[str] = None, ) -> None: if abi.is_abi_transaction_type(arg_type) or abi.is_abi_reference_type( arg_type ): - self.type = arg_type + self.type: str | abi.ABIType = arg_type else: # If the type cannot be parsed into an ABI type, it will error self.type = abi.ABIType.from_string(arg_type) @@ -208,9 +222,9 @@ class Returns: # Represents a void return. VOID = "void" - def __init__(self, arg_type: str, desc: str = None) -> None: + def __init__(self, arg_type: str, desc: Optional[str] = None) -> None: if arg_type == "void": - self.type = self.VOID + self.type: str | abi.ABIType = self.VOID else: # If the type cannot be parsed into an ABI type, it will error. self.type = abi.ABIType.from_string(arg_type) From 56659b42e18dd2689bc57a17852164dbedada018 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Tue, 15 Nov 2022 15:13:38 -0500 Subject: [PATCH 14/27] Fix type errors in `interface.py` --- algosdk/abi/interface.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/algosdk/abi/interface.py b/algosdk/abi/interface.py index 5bd79aa4..3f539d89 100644 --- a/algosdk/abi/interface.py +++ b/algosdk/abi/interface.py @@ -1,7 +1,16 @@ import json -from typing import List, Union +from typing import List, Union, Optional, TypedDict -from algosdk.abi.method import Method, get_method_by_name +from algosdk.abi.method import Method, MethodDict, get_method_by_name + +# In Python 3.11+ the following classes should be combined using `NotRequired` +class InterfaceDict_Optional(TypedDict, total=False): + desc: str + + +class InterfaceDict(InterfaceDict_Optional): + name: str + methods: list[MethodDict] class Interface: @@ -15,7 +24,7 @@ class Interface: """ def __init__( - self, name: str, methods: List[Method], desc: str = None + self, name: str, methods: List[Method], desc: Optional[str] = None ) -> None: self.name = name self.methods = methods @@ -35,10 +44,11 @@ def from_json(resp: Union[str, bytes, bytearray]) -> "Interface": d = json.loads(resp) return Interface.undictify(d) - def dictify(self) -> dict: - d = {} - d["name"] = self.name - d["methods"] = [m.dictify() for m in self.methods] + def dictify(self) -> InterfaceDict: + d: InterfaceDict = { + "name": self.name, + "methods": [m.dictify() for m in self.methods], + } if self.desc: d["desc"] = self.desc return d From 4f400181e8fd80482ccb64483bd0e3dad306a7c0 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Tue, 15 Nov 2022 15:21:15 -0500 Subject: [PATCH 15/27] Fix type errors in `contract.py` --- algosdk/abi/contract.py | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/algosdk/abi/contract.py b/algosdk/abi/contract.py index cfeb7b11..71f80e2e 100644 --- a/algosdk/abi/contract.py +++ b/algosdk/abi/contract.py @@ -1,7 +1,22 @@ import json -from typing import Dict, List, Union +from typing import Dict, List, Union, Optional, TypedDict -from algosdk.abi.method import Method, get_method_by_name +from algosdk.abi.method import Method, MethodDict, get_method_by_name + + +class NetworkInfoDict(TypedDict): + appID: int + + +# In Python 3.11+ the following classes should be combined using `NotRequired` +class ContractDict_Optional(TypedDict, total=False): + desc: str + + +class ContractDict(ContractDict_Optional): + name: str + methods: list[MethodDict] + networks: dict[str, NetworkInfoDict] class Contract: @@ -20,8 +35,8 @@ def __init__( self, name: str, methods: List[Method], - desc: str = None, - networks: Dict[str, "NetworkInfo"] = None, + desc: Optional[str] = None, + networks: Optional[Dict[str, "NetworkInfo"]] = None, ) -> None: self.name = name self.methods = methods @@ -43,11 +58,12 @@ def from_json(resp: Union[str, bytes, bytearray]) -> "Contract": d = json.loads(resp) return Contract.undictify(d) - def dictify(self) -> dict: - d = {} - d["name"] = self.name - d["methods"] = [m.dictify() for m in self.methods] - d["networks"] = {k: v.dictify() for k, v in self.networks.items()} + def dictify(self) -> ContractDict: + d: ContractDict = { + "name": self.name, + "methods": [m.dictify() for m in self.methods], + "networks": {k: v.dictify() for k, v in self.networks.items()}, + } if self.desc is not None: d["desc"] = self.desc return d @@ -84,7 +100,7 @@ def __eq__(self, o: object) -> bool: return False return self.app_id == o.app_id - def dictify(self) -> dict: + def dictify(self) -> NetworkInfoDict: return {"appID": self.app_id} @staticmethod From 6acce21a348dcbc2cfdb90323022d8e6cc8f21da Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Tue, 15 Nov 2022 15:23:14 -0500 Subject: [PATCH 16/27] Fix type errors in `dryrun.py` --- algosdk/testing/dryrun.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/algosdk/testing/dryrun.py b/algosdk/testing/dryrun.py index bc561e16..c4620e06 100644 --- a/algosdk/testing/dryrun.py +++ b/algosdk/testing/dryrun.py @@ -2,7 +2,7 @@ import binascii import string from dataclasses import dataclass -from typing import List, Union +from typing import Union, Optional from algosdk.constants import payment_txn, appcall_txn, ZERO_ADDRESS from algosdk.future import transaction @@ -25,7 +25,7 @@ class LSig: """Logic Sig program parameters""" - args: List[bytes] = None + args: Optional[list[bytes]] = None @dataclass @@ -33,12 +33,12 @@ class App: """Application program parameters""" creator: str = ZERO_ADDRESS - round: int = None + round: Optional[int] = None app_idx: int = 0 on_complete: int = 0 - args: List[bytes] = None - accounts: List[Union[str, Account]] = None - global_state: List[TealKeyValue] = None + args: Optional[list[bytes]] = None + accounts: Optional[list[Union[str, Account]]] = None + global_state: Optional[list[TealKeyValue]] = None class DryrunTestCaseMixin: From 8cf2b06ccb189b93338297c3fee6af987a3a823a Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Thu, 17 Nov 2022 15:41:56 -0500 Subject: [PATCH 17/27] Fix type errors in `atomic_transaction_composer.py` --- algosdk/atomic_transaction_composer.py | 99 ++++++++++++++++---------- 1 file changed, 63 insertions(+), 36 deletions(-) diff --git a/algosdk/atomic_transaction_composer.py b/algosdk/atomic_transaction_composer.py index 2f556431..ecbf3009 100644 --- a/algosdk/atomic_transaction_composer.py +++ b/algosdk/atomic_transaction_composer.py @@ -2,7 +2,17 @@ import copy from abc import ABC, abstractmethod from enum import IntEnum -from typing import Any, List, Optional, Tuple, TypeVar, Union +from typing import ( + Any, + List, + Dict, + Optional, + Tuple, + TypeVar, + Union, + Generic, + cast, +) from algosdk import abi, error from algosdk.abi.address_type import AddressType @@ -37,7 +47,7 @@ class AtomicTransactionComposerStatus(IntEnum): def populate_foreign_array( - value_to_add: T, foreign_array: List[T], zero_value: T = None + value_to_add: T, foreign_array: List[T], zero_value: Optional[T] = None ) -> int: """ Add a value to an application call's foreign array. The addition will be as @@ -68,6 +78,13 @@ def populate_foreign_array( return offset + len(foreign_array) - 1 +SignedTransaction = Union[ + transaction.SignedTransaction, + transaction.LogicSigTransaction, + transaction.MultisigTransaction, +] + + class AtomicTransactionComposer: """ Constructs an atomic transaction group which may contain a combination of @@ -88,10 +105,10 @@ class AtomicTransactionComposer: def __init__(self) -> None: self.status = AtomicTransactionComposerStatus.BUILDING - self.method_dict = {} - self.txn_list = [] - self.signed_txns = [] - self.tx_ids = [] + self.method_dict: dict[int, abi.Method] = {} + self.txn_list: list[TransactionWithSigner] = [] + self.signed_txns: list[SignedTransaction] = [] + self.tx_ids: list[str] = [] def get_status(self) -> AtomicTransactionComposerStatus: """ @@ -160,20 +177,22 @@ def add_method_call( sender: str, sp: transaction.SuggestedParams, signer: "TransactionSigner", - method_args: List[Union[Any, "TransactionWithSigner"]] = None, + method_args: Optional[ + List[Union[Any, "TransactionWithSigner"]] + ] = None, on_complete: transaction.OnComplete = transaction.OnComplete.NoOpOC, - local_schema: transaction.StateSchema = None, - global_schema: transaction.StateSchema = None, - approval_program: bytes = None, - clear_program: bytes = None, - extra_pages: int = None, - accounts: List[str] = None, - foreign_apps: List[int] = None, - foreign_assets: List[int] = None, - note: bytes = None, - lease: bytes = None, - rekey_to: str = None, - boxes: List[Tuple[int, bytes]] = None, + local_schema: Optional[transaction.StateSchema] = None, + global_schema: Optional[transaction.StateSchema] = None, + approval_program: Optional[bytes] = None, + clear_program: Optional[bytes] = None, + extra_pages: Optional[int] = None, + accounts: Optional[List[str]] = None, + foreign_apps: Optional[List[int]] = None, + foreign_assets: Optional[List[int]] = None, + note: Optional[bytes] = None, + lease: Optional[bytes] = None, + rekey_to: Optional[str] = None, + boxes: Optional[List[Tuple[int, bytes]]] = None, ) -> "AtomicTransactionComposer": """ Add a smart contract method call to this atomic group. @@ -264,7 +283,7 @@ def add_method_call( boxes = boxes[:] if boxes else [] app_args = [] - raw_values = [] + raw_values: List[Any] = [] raw_types = [] txn_list = [] @@ -288,22 +307,24 @@ def add_method_call( txn_list.append(method_args[i]) else: if abi.is_abi_reference_type(arg.type): - current_type = abi.UintType(8) + current_type: str | abi.ABIType = abi.UintType(8) if arg.type == abi.ABIReferenceType.ACCOUNT: address_type = AddressType() account_arg = address_type.decode( - address_type.encode(method_args[i]) + address_type.encode( + cast(Union[str, bytes], method_args[i]) + ) ) - current_arg = populate_foreign_array( + current_arg: Any = populate_foreign_array( account_arg, accounts, sender ) elif arg.type == abi.ABIReferenceType.ASSET: - asset_arg = int(method_args[i]) + asset_arg = int(cast(int, method_args[i])) current_arg = populate_foreign_array( asset_arg, foreign_assets ) elif arg.type == abi.ABIReferenceType.APPLICATION: - app_arg = int(method_args[i]) + app_arg = int(cast(int, method_args[i])) current_arg = populate_foreign_array( app_arg, foreign_apps, app_id ) @@ -404,8 +425,12 @@ def gather_signatures(self) -> list: # Return cached versions of the signatures return self.signed_txns - stxn_list = [None] * len(self.txn_list) - signer_indexes = {} # Map a signer to a list of indices to sign + stxn_list: List[Optional[SignedTransaction]] = [None] * len( + self.txn_list + ) + signer_indexes: Dict[ + TransactionSigner, List[int] + ] = {} # Map a signer to a list of indices to sign txn_list = self.build_group() for i, txn_with_signer in enumerate(txn_list): if txn_with_signer.signer not in signer_indexes: @@ -424,9 +449,10 @@ def gather_signatures(self) -> list: raise error.AtomicTransactionComposerError( "missing signatures, got {}".format(stxn_list) ) + full_stxn_list = cast(List[SignedTransaction], stxn_list) self.status = AtomicTransactionComposerStatus.SIGNED - self.signed_txns = stxn_list + self.signed_txns = full_stxn_list return self.signed_txns def submit(self, client: algod.AlgodClient) -> list: @@ -496,10 +522,10 @@ def execute( method_results = [] for i, tx_id in enumerate(self.tx_ids): - raw_value = None + raw_value: Optional[bytes] = None return_value = None decode_error = None - tx_info = None + tx_info: Optional[Any] = None if i not in self.method_dict: continue @@ -511,7 +537,7 @@ def execute( method_results.append( ABIResult( tx_id=tx_id, - raw_value=raw_value, + raw_value=cast(bytes, raw_value), return_value=return_value, decode_error=decode_error, tx_info=tx_info, @@ -538,18 +564,19 @@ def execute( "app call transaction did not log a return value" ) raw_value = result_bytes[4:] - return_value = self.method_dict[i].returns.type.decode( - raw_value + method_return_type = cast( + abi.ABIType, self.method_dict[i].returns.type ) + return_value = method_return_type.decode(raw_value) except Exception as e: decode_error = e abi_result = ABIResult( tx_id=tx_id, - raw_value=raw_value, + raw_value=cast(bytes, raw_value), return_value=return_value, decode_error=decode_error, - tx_info=tx_info, + tx_info=cast(Any, tx_info), method=self.method_dict[i], ) method_results.append(abi_result) @@ -693,7 +720,7 @@ def __init__( class ABIResult: def __init__( self, - tx_id: int, + tx_id: str, raw_value: bytes, return_value: Any, decode_error: Optional[Exception], From 8dcd5a05428312d36f6cc68f213d0e163fd77aa9 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Thu, 17 Nov 2022 16:02:53 -0500 Subject: [PATCH 18/27] Fix formatter errors --- algosdk/source_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algosdk/source_map.py b/algosdk/source_map.py index 0e67cdf9..47969f45 100644 --- a/algosdk/source_map.py +++ b/algosdk/source_map.py @@ -75,7 +75,7 @@ def _base64vlq_decode(vlqval: str) -> tuple[int, ...]: shift = value = 0 # use byte values and a table to go from base64 characters to integers for v in map(_b64table.__getitem__, vlqval.encode("ascii")): - v = cast(int, v) # force int type given context + v = cast(int, v) # force int type given context value += (v & mask) << shift if v & flag: shift += shiftsize From 34593aa5bc42e1525b2986a83113b124baedc021 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Thu, 17 Nov 2022 16:10:18 -0500 Subject: [PATCH 19/27] Fix unsupported types before Python 3.9 Some types (such as `list` instead of `List`) were introduced in PEP 585 within Python 3.9 but since `py-algorand-sdk`requires Python 3.8 or later, the explicit types (`List`, `Union`, etc) are necessary for compatibility. --- algosdk/abi/contract.py | 4 ++-- algosdk/abi/interface.py | 2 +- algosdk/abi/method.py | 6 +++--- algosdk/abi/tuple_type.py | 8 ++++---- algosdk/atomic_transaction_composer.py | 10 +++++----- algosdk/constants.py | 4 +++- algosdk/future/transaction.py | 2 +- algosdk/source_map.py | 6 +++--- algosdk/testing/dryrun.py | 10 +++++----- 9 files changed, 27 insertions(+), 25 deletions(-) diff --git a/algosdk/abi/contract.py b/algosdk/abi/contract.py index 71f80e2e..9a728c54 100644 --- a/algosdk/abi/contract.py +++ b/algosdk/abi/contract.py @@ -15,8 +15,8 @@ class ContractDict_Optional(TypedDict, total=False): class ContractDict(ContractDict_Optional): name: str - methods: list[MethodDict] - networks: dict[str, NetworkInfoDict] + methods: List[MethodDict] + networks: Dict[str, NetworkInfoDict] class Contract: diff --git a/algosdk/abi/interface.py b/algosdk/abi/interface.py index 3f539d89..aaf07655 100644 --- a/algosdk/abi/interface.py +++ b/algosdk/abi/interface.py @@ -10,7 +10,7 @@ class InterfaceDict_Optional(TypedDict, total=False): class InterfaceDict(InterfaceDict_Optional): name: str - methods: list[MethodDict] + methods: List[MethodDict] class Interface: diff --git a/algosdk/abi/method.py b/algosdk/abi/method.py index 58ef151f..3bd21842 100644 --- a/algosdk/abi/method.py +++ b/algosdk/abi/method.py @@ -12,7 +12,7 @@ class MethodDict_Optional(TypedDict, total=False): class MethodDict(MethodDict_Optional): name: str - args: list[dict] + args: List[dict] returns: dict @@ -175,7 +175,7 @@ def __init__( if abi.is_abi_transaction_type(arg_type) or abi.is_abi_reference_type( arg_type ): - self.type: str | abi.ABIType = arg_type + self.type: Union[str, abi.ABIType] = arg_type else: # If the type cannot be parsed into an ABI type, it will error self.type = abi.ABIType.from_string(arg_type) @@ -224,7 +224,7 @@ class Returns: def __init__(self, arg_type: str, desc: Optional[str] = None) -> None: if arg_type == "void": - self.type: str | abi.ABIType = self.VOID + self.type: Union[str, abi.ABIType] = self.VOID else: # If the type cannot be parsed into an ABI type, it will error. self.type = abi.ABIType.from_string(arg_type) diff --git a/algosdk/abi/tuple_type.py b/algosdk/abi/tuple_type.py index 2f46cca5..854281e2 100644 --- a/algosdk/abi/tuple_type.py +++ b/algosdk/abi/tuple_type.py @@ -176,7 +176,7 @@ def encode(self, values: Union[List[Any], bytes, bytearray]) -> bytes: ) after = min(7, after) consecutive_bool_list = cast( - list[bool], values[i : i + after + 1] + List[bool], values[i : i + after + 1] ) compressed_int = TupleType._compress_multiple_bool( consecutive_bool_list @@ -232,10 +232,10 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> list: "value string must be in bytes: {}".format(bytestring) ) tuple_elements = self.child_types - dynamic_segments: list[ - list[int] + dynamic_segments: List[ + List[int] ] = list() # Store the start and end of a dynamic element - value_partitions: list[Optional[bytes | bytearray]] = list() + value_partitions: List[Optional[Union[bytes, bytearray]]] = list() i = 0 array_index = 0 diff --git a/algosdk/atomic_transaction_composer.py b/algosdk/atomic_transaction_composer.py index ecbf3009..7c97d2f3 100644 --- a/algosdk/atomic_transaction_composer.py +++ b/algosdk/atomic_transaction_composer.py @@ -105,10 +105,10 @@ class AtomicTransactionComposer: def __init__(self) -> None: self.status = AtomicTransactionComposerStatus.BUILDING - self.method_dict: dict[int, abi.Method] = {} - self.txn_list: list[TransactionWithSigner] = [] - self.signed_txns: list[SignedTransaction] = [] - self.tx_ids: list[str] = [] + self.method_dict: Dict[int, abi.Method] = {} + self.txn_list: List[TransactionWithSigner] = [] + self.signed_txns: List[SignedTransaction] = [] + self.tx_ids: List[str] = [] def get_status(self) -> AtomicTransactionComposerStatus: """ @@ -307,7 +307,7 @@ def add_method_call( txn_list.append(method_args[i]) else: if abi.is_abi_reference_type(arg.type): - current_type: str | abi.ABIType = abi.UintType(8) + current_type: Union[str, abi.ABIType] = abi.UintType(8) if arg.type == abi.ABIReferenceType.ACCOUNT: address_type = AddressType() account_arg = address_type.decode( diff --git a/algosdk/constants.py b/algosdk/constants.py index 09228091..cf3c8a21 100644 --- a/algosdk/constants.py +++ b/algosdk/constants.py @@ -1,3 +1,5 @@ +from typing import List + """ Contains useful constants. """ @@ -9,7 +11,7 @@ """str: header key for indexer requests""" UNVERSIONED_PATHS = ["/health", "/versions", "/metrics", "/genesis"] """str[]: paths that don't use the version path prefix""" -NO_AUTH: list[str] = [] +NO_AUTH: List[str] = [] """str[]: requests that don't require authentication""" diff --git a/algosdk/future/transaction.py b/algosdk/future/transaction.py index ce0401d6..385c6f89 100644 --- a/algosdk/future/transaction.py +++ b/algosdk/future/transaction.py @@ -2738,7 +2738,7 @@ class LogicSigAccount: """ def __init__( - self, program: bytes, args: Optional[list[bytes]] = None + self, program: bytes, args: Optional[List[bytes]] = None ) -> None: """ Create a new LogicSigAccount. By default this will create an escrow diff --git a/algosdk/source_map.py b/algosdk/source_map.py index 47969f45..d72a2129 100644 --- a/algosdk/source_map.py +++ b/algosdk/source_map.py @@ -1,4 +1,4 @@ -from typing import Dict, Any, List, Final, Optional, cast +from typing import Dict, Any, List, Tuple, Final, Optional, cast from algosdk.error import SourceMapVersionError @@ -62,14 +62,14 @@ def _decode_int_value(value: str) -> Optional[int]: """ _b64chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -_b64table: Final[list[Optional[int]]] = [None] * (max(_b64chars) + 1) +_b64table: Final[List[Optional[int]]] = [None] * (max(_b64chars) + 1) for i, b in enumerate(_b64chars): _b64table[b] = i shiftsize, flag, mask = 5, 1 << 5, (1 << 5) - 1 -def _base64vlq_decode(vlqval: str) -> tuple[int, ...]: +def _base64vlq_decode(vlqval: str) -> Tuple[int, ...]: """Decode Base64 VLQ value""" results = [] shift = value = 0 diff --git a/algosdk/testing/dryrun.py b/algosdk/testing/dryrun.py index c4620e06..7ac42ef5 100644 --- a/algosdk/testing/dryrun.py +++ b/algosdk/testing/dryrun.py @@ -2,7 +2,7 @@ import binascii import string from dataclasses import dataclass -from typing import Union, Optional +from typing import Union, List, Optional from algosdk.constants import payment_txn, appcall_txn, ZERO_ADDRESS from algosdk.future import transaction @@ -25,7 +25,7 @@ class LSig: """Logic Sig program parameters""" - args: Optional[list[bytes]] = None + args: Optional[List[bytes]] = None @dataclass @@ -36,9 +36,9 @@ class App: round: Optional[int] = None app_idx: int = 0 on_complete: int = 0 - args: Optional[list[bytes]] = None - accounts: Optional[list[Union[str, Account]]] = None - global_state: Optional[list[TealKeyValue]] = None + args: Optional[List[bytes]] = None + accounts: Optional[List[Union[str, Account]]] = None + global_state: Optional[List[TealKeyValue]] = None class DryrunTestCaseMixin: From d82af55cbb4266971815d48b87df01bc6eb9a36f Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Tue, 22 Nov 2022 15:00:25 -0500 Subject: [PATCH 20/27] Use `cast` instead of `hasattr` when inside an `issubclass` conditional --- algosdk/future/transaction.py | 47 +++++++++++++---------------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/algosdk/future/transaction.py b/algosdk/future/transaction.py index 385c6f89..7af48fbd 100644 --- a/algosdk/future/transaction.py +++ b/algosdk/future/transaction.py @@ -3279,69 +3279,56 @@ def create_dryrun( # we only care about app call transactions if issubclass(type(txn), ApplicationCallTxn): - accts.append(txn.sender) + appTxn = cast(ApplicationCallTxn, txn) + accts.append(appTxn.sender) # Add foreign args if they're set - if hasattr(txn, "accounts"): - accts.extend(txn.accounts) - if hasattr(txn, "foreign_apps"): - apps.extend(txn.foreign_apps) + if appTxn.accounts: + accts.extend(appTxn.accounts) + if appTxn.foreign_apps: + apps.extend(appTxn.foreign_apps) accts.extend( [ logic.get_application_address(aidx) - for aidx in txn.foreign_apps + for aidx in appTxn.foreign_apps ] ) - if hasattr(txn, "foreign_assets"): - assets.extend(txn.foreign_assets) + if appTxn.foreign_assets: + assets.extend(appTxn.foreign_assets) # For creates, we need to add the source directly from the transaction - if hasattr(txn, "index") and txn.index == 0: + if appTxn.index == 0: appId = defaultAppId # Make up app id, since tealdbg/dryrun doesnt like 0s # https://github.com/algorand/go-algorand/blob/e466aa18d4d963868d6d15279b1c881977fa603f/libgoal/libgoal.go#L1089-L1090 - ls = txn.local_schema if hasattr(txn, "local_schema") else None + ls = appTxn.local_schema if ls is not None: ls = models.ApplicationStateSchema( ls.num_uints, ls.num_byte_slices ) - gs = ( - txn.global_schema - if hasattr(txn, "global_schema") - else None - ) + gs = appTxn.global_schema if gs is not None: gs = models.ApplicationStateSchema( gs.num_uints, gs.num_byte_slices ) - approval_program = ( - txn.approval_program - if hasattr(txn, "approval_program") - else None - ) - clear_program = ( - txn.clear_program - if hasattr(txn, "clear_program") - else None - ) app_infos.append( models.Application( id=appId, params=models.ApplicationParams( - creator=txn.sender, - approval_program=approval_program, - clear_state_program=clear_program, + creator=appTxn.sender, + approval_program=appTxn.approval_program, + clear_state_program=appTxn.clear_program, local_state_schema=ls, global_state_schema=gs, ), ) ) else: - if hasattr(txn, "index"): - apps.append(txn.index) + if appTxn.index: + apps.append(appTxn.index) # Dedupe and filter none, reset programs to bytecode instead of b64 apps = [i for i in set(apps) if i] From ef693d8f62537d0c25eeabb4246c2400ede83a9e Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Tue, 22 Nov 2022 16:16:42 -0500 Subject: [PATCH 21/27] Specify ignored errors --- algosdk/abi/base_type.py | 8 ++++---- algosdk/dryrun_results.py | 10 +++++----- algosdk/future/transaction.py | 4 +++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/algosdk/abi/base_type.py b/algosdk/abi/base_type.py index 931eb790..de383d5d 100644 --- a/algosdk/abi/base_type.py +++ b/algosdk/abi/base_type.py @@ -77,8 +77,8 @@ def from_string(s: str) -> "ABIType": elif s.endswith("]"): matches = re.search(STATIC_ARRAY_REGEX, s) try: - static_length = int(matches.group(2)) # type: ignore # we allow attribute errors to be caught - array_type = ABIType.from_string(matches.group(1)) # type: ignore # we allow attribute errors to be caught + static_length = int(matches.group(2)) # type: ignore[union-attr] # we allow attribute errors to be caught + array_type = ABIType.from_string(matches.group(1)) # type: ignore[union-attr] # we allow attribute errors to be caught return ArrayStaticType(array_type, static_length) except Exception as e: raise error.ABITypeError( @@ -103,8 +103,8 @@ def from_string(s: str) -> "ABIType": elif s.startswith("ufixed"): matches = re.search(UFIXED_REGEX, s) try: - bit_size = int(matches.group(1)) # type: ignore # we allow attribute errors to be caught - precision = int(matches.group(2)) # type: ignore # we allow attribute errors to be caught + bit_size = int(matches.group(1)) # type: ignore[union-attr] # we allow attribute errors to be caught + precision = int(matches.group(2)) # type: ignore[union-attr] # we allow attribute errors to be caught return UfixedType(bit_size, precision) except Exception as e: raise error.ABITypeError( diff --git a/algosdk/dryrun_results.py b/algosdk/dryrun_results.py index 751aa263..29520784 100644 --- a/algosdk/dryrun_results.py +++ b/algosdk/dryrun_results.py @@ -63,13 +63,13 @@ def attrname(field): def app_call_rejected(self) -> bool: return ( False - if self.app_call_messages is None # type: ignore # dynamic attribute - else "REJECT" in self.app_call_messages # type: ignore # dynamic attribute + if self.app_call_messages is None # type: ignore[attr-defined] # dynamic attribute + else "REJECT" in self.app_call_messages # type: ignore[attr-defined] # dynamic attribute ) def logic_sig_rejected(self) -> bool: - if self.logic_sig_messages is not None: # type: ignore # dynamic attribute - return "REJECT" in self.logic_sig_messages # type: ignore # dynamic attribute + if self.logic_sig_messages is not None: # type: ignore[attr-defined] # dynamic attribute + return "REJECT" in self.logic_sig_messages # type: ignore[attr-defined] # dynamic attribute return False @classmethod @@ -144,7 +144,7 @@ def lsig_trace(self, spc: Optional[StackPrinterConfig] = None) -> str: spc = StackPrinterConfig(top_of_stack_first=False) return self.trace( - self.logic_sig_trace, self.logic_sig_disassembly, spc=spc # type: ignore # dynamic attribute + self.logic_sig_trace, self.logic_sig_disassembly, spc=spc # type: ignore[attr-defined] # dynamic attribute ) diff --git a/algosdk/future/transaction.py b/algosdk/future/transaction.py index 7af48fbd..7e52515c 100644 --- a/algosdk/future/transaction.py +++ b/algosdk/future/transaction.py @@ -1638,7 +1638,9 @@ def __init__( self.foreign_apps = self.int_list(foreign_apps) self.foreign_assets = self.int_list(foreign_assets) self.extra_pages = extra_pages - self.boxes = BoxReference.translate_box_references(boxes, self.foreign_apps, self.index) # type: ignore + self.boxes = BoxReference.translate_box_references( + boxes, self.foreign_apps, self.index + ) if not sp.flat_fee: self.fee = max( self.estimate_size() * self.fee, constants.min_txn_fee From 6225ff705ecfd1fb4295764b14e17ac225bfefc1 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Tue, 22 Nov 2022 16:28:43 -0500 Subject: [PATCH 22/27] Include mypy configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This avoids mypy’s fallback behavior which uses the config located in the user’s home directory. --- mypy.ini | 1 + 1 file changed, 1 insertion(+) create mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..f8b1844b --- /dev/null +++ b/mypy.ini @@ -0,0 +1 @@ +[mypy] From c082b1c1028a91fe9c152c43c1e5d07515745fe5 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Tue, 22 Nov 2022 16:36:26 -0500 Subject: [PATCH 23/27] Add py.typed marker so mypy knows we include types --- MANIFEST.in | 2 ++ algosdk/py.typed | 0 setup.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 algosdk/py.typed diff --git a/MANIFEST.in b/MANIFEST.in index 8bc13cdb..c36a8d5d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,3 @@ include algosdk/data/langspec.json +global-include *.pyi +global-include *.typed diff --git a/algosdk/py.typed b/algosdk/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/setup.py b/setup.py index 83dc6c3d..8cee71b7 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,6 @@ ], packages=setuptools.find_packages(), python_requires=">=3.8", - package_data={"": ["data/langspec.json"]}, + package_data={"": ["data/langspec.json", "*.pyi", "py.typed"]}, include_package_data=True, ) From 41b5a9735e9432fd01568bd0b6ef797a64acdce8 Mon Sep 17 00:00:00 2001 From: Michael Diamant Date: Tue, 29 Nov 2022 14:40:19 -0500 Subject: [PATCH 24/27] Add return type to TransactionSigner.sign_transactions (#3) * Add return type to TransactionSigner.sign_transactions * Switch back to List * Fix docstring typo --- algosdk/atomic_transaction_composer.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/algosdk/atomic_transaction_composer.py b/algosdk/atomic_transaction_composer.py index 7c97d2f3..ce602122 100644 --- a/algosdk/atomic_transaction_composer.py +++ b/algosdk/atomic_transaction_composer.py @@ -10,7 +10,6 @@ Tuple, TypeVar, Union, - Generic, cast, ) @@ -419,7 +418,7 @@ def gather_signatures(self) -> list: An error will be thrown if signing any of the transactions fails. Returns: - list[SignedTransactions]: list of signed transactions + List[SignedTransaction]: list of signed transactions """ if self.status >= AtomicTransactionComposerStatus.SIGNED: # Return cached versions of the signatures @@ -455,7 +454,7 @@ def gather_signatures(self) -> list: self.signed_txns = full_stxn_list return self.signed_txns - def submit(self, client: algod.AlgodClient) -> list: + def submit(self, client: algod.AlgodClient) -> List[str]: """ Send the transaction group to the network, but don't wait for it to be committed to a block. An error will be thrown if submission fails. @@ -468,7 +467,7 @@ def submit(self, client: algod.AlgodClient) -> list: client (AlgodClient): Algod V2 client Returns: - list[Transaction]: list of submitted transactions + List[str]: list of submitted transaction IDs """ if self.status <= AtomicTransactionComposerStatus.SUBMITTED: self.gather_signatures() @@ -599,7 +598,7 @@ def __init__(self) -> None: @abstractmethod def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> list: + ) -> List[SignedTransaction]: pass @@ -618,7 +617,7 @@ def __init__(self, private_key: str) -> None: def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> list: + ) -> List[SignedTransaction]: """ Sign transactions in a transaction group given the indexes. @@ -652,7 +651,7 @@ def __init__(self, lsig: transaction.LogicSigAccount) -> None: def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> list: + ) -> List[SignedTransaction]: """ Sign transactions in a transaction group given the indexes. @@ -664,7 +663,7 @@ def sign_transactions( txn_group (list[Transaction]): atomic group of transactions indexes (list[int]): array of indexes in the atomic transaction group that should be signed """ - stxns = [] + stxns: List[SignedTransaction] = [] for i in indexes: stxn = transaction.LogicSigTransaction(txn_group[i], self.lsig) stxns.append(stxn) @@ -688,7 +687,7 @@ def __init__(self, msig: transaction.Multisig, sks: str) -> None: def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> list: + ) -> List[SignedTransaction]: """ Sign transactions in a transaction group given the indexes. @@ -700,7 +699,7 @@ def sign_transactions( txn_group (list[Transaction]): atomic group of transactions indexes (list[int]): array of indexes in the atomic transaction group that should be signed """ - stxns = [] + stxns: List[SignedTransaction] = [] for i in indexes: mtxn = transaction.MultisigTransaction(txn_group[i], self.msig) for sk in self.sks: From 1b643b0b057ed8f55ff4f3844c47cd58ef141559 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Tue, 29 Nov 2022 14:32:49 -0500 Subject: [PATCH 25/27] Rename `SignedTransaction` to `GenericSignedTransaction` # Conflicts: # algosdk/atomic_transaction_composer.py --- algosdk/atomic_transaction_composer.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/algosdk/atomic_transaction_composer.py b/algosdk/atomic_transaction_composer.py index ce602122..8f4ab521 100644 --- a/algosdk/atomic_transaction_composer.py +++ b/algosdk/atomic_transaction_composer.py @@ -77,7 +77,7 @@ def populate_foreign_array( return offset + len(foreign_array) - 1 -SignedTransaction = Union[ +GenericSignedTransaction = Union[ transaction.SignedTransaction, transaction.LogicSigTransaction, transaction.MultisigTransaction, @@ -93,7 +93,7 @@ class AtomicTransactionComposer: status (AtomicTransactionComposerStatus): IntEnum representing the current state of the composer method_dict (dict): dictionary of an index in the transaction list to a Method object txn_list (list[TransactionWithSigner]): list of transactions with signers - signed_txns (list[SignedTransaction]): list of signed transactions + signed_txns (list[GenericSignedTransaction]): list of signed transactions tx_ids (list[str]): list of individual transaction IDs in this atomic group """ @@ -106,7 +106,7 @@ def __init__(self) -> None: self.status = AtomicTransactionComposerStatus.BUILDING self.method_dict: Dict[int, abi.Method] = {} self.txn_list: List[TransactionWithSigner] = [] - self.signed_txns: List[SignedTransaction] = [] + self.signed_txns: List[GenericSignedTransaction] = [] self.tx_ids: List[str] = [] def get_status(self) -> AtomicTransactionComposerStatus: @@ -418,13 +418,13 @@ def gather_signatures(self) -> list: An error will be thrown if signing any of the transactions fails. Returns: - List[SignedTransaction]: list of signed transactions + List[GenericSignedTransaction]: list of signed transactions """ if self.status >= AtomicTransactionComposerStatus.SIGNED: # Return cached versions of the signatures return self.signed_txns - stxn_list: List[Optional[SignedTransaction]] = [None] * len( + stxn_list: List[Optional[GenericSignedTransaction]] = [None] * len( self.txn_list ) signer_indexes: Dict[ @@ -448,7 +448,7 @@ def gather_signatures(self) -> list: raise error.AtomicTransactionComposerError( "missing signatures, got {}".format(stxn_list) ) - full_stxn_list = cast(List[SignedTransaction], stxn_list) + full_stxn_list = cast(List[GenericSignedTransaction], stxn_list) self.status = AtomicTransactionComposerStatus.SIGNED self.signed_txns = full_stxn_list @@ -598,7 +598,7 @@ def __init__(self) -> None: @abstractmethod def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> List[SignedTransaction]: + ) -> List[GenericSignedTransaction]: pass @@ -617,7 +617,7 @@ def __init__(self, private_key: str) -> None: def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> List[SignedTransaction]: + ) -> List[GenericSignedTransaction]: """ Sign transactions in a transaction group given the indexes. @@ -651,7 +651,7 @@ def __init__(self, lsig: transaction.LogicSigAccount) -> None: def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> List[SignedTransaction]: + ) -> List[GenericSignedTransaction]: """ Sign transactions in a transaction group given the indexes. @@ -663,7 +663,7 @@ def sign_transactions( txn_group (list[Transaction]): atomic group of transactions indexes (list[int]): array of indexes in the atomic transaction group that should be signed """ - stxns: List[SignedTransaction] = [] + stxns: List[GenericSignedTransaction] = [] for i in indexes: stxn = transaction.LogicSigTransaction(txn_group[i], self.lsig) stxns.append(stxn) @@ -687,7 +687,7 @@ def __init__(self, msig: transaction.Multisig, sks: str) -> None: def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> List[SignedTransaction]: + ) -> List[GenericSignedTransaction]: """ Sign transactions in a transaction group given the indexes. @@ -699,7 +699,7 @@ def sign_transactions( txn_group (list[Transaction]): atomic group of transactions indexes (list[int]): array of indexes in the atomic transaction group that should be signed """ - stxns: List[SignedTransaction] = [] + stxns: List[GenericSignedTransaction] = [] for i in indexes: mtxn = transaction.MultisigTransaction(txn_group[i], self.msig) for sk in self.sks: From afe769c8d9ea8571ec2b91af3720e4f6101eaa73 Mon Sep 17 00:00:00 2001 From: algochoi <86622919+algochoi@users.noreply.github.com> Date: Tue, 29 Nov 2022 14:51:03 -0500 Subject: [PATCH 26/27] Some changes based on mypy: (#1) --- algosdk/abi/base_type.py | 3 --- algosdk/abi/byte_type.py | 2 +- algosdk/abi/tuple_type.py | 4 ++-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/algosdk/abi/base_type.py b/algosdk/abi/base_type.py index de383d5d..7b5021ad 100644 --- a/algosdk/abi/base_type.py +++ b/algosdk/abi/base_type.py @@ -124,9 +124,6 @@ def from_string(s: str) -> "ABIType": if isinstance(tup, str): tt = ABIType.from_string(tup) tuple_list.append(tt) - elif isinstance(tup, list): - tts = [ABIType.from_string(t_) for t_ in tup] - tuple_list.extend(tts) else: raise error.ABITypeError( "cannot convert {} to an ABI type".format(tup) diff --git a/algosdk/abi/byte_type.py b/algosdk/abi/byte_type.py index f52ae582..ccaae47c 100644 --- a/algosdk/abi/byte_type.py +++ b/algosdk/abi/byte_type.py @@ -50,7 +50,7 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> int: bytestring (bytes | bytearray): bytestring to be decoded Returns: - bytes: byte of the encoded bytestring + int: byte value of the encoded bytestring """ if ( not ( diff --git a/algosdk/abi/tuple_type.py b/algosdk/abi/tuple_type.py index 854281e2..62fecb46 100644 --- a/algosdk/abi/tuple_type.py +++ b/algosdk/abi/tuple_type.py @@ -73,7 +73,7 @@ def _find_bool(type_list: List[ABIType], index: int, delta: int) -> int: return until @staticmethod - def _parse_tuple(s: str) -> list: + def _parse_tuple(s: str) -> List[str]: """ Given a tuple string, parses one layer of the tuple and returns tokens as a list. i.e. 'x,(y,(z))' -> ['x', '(y,(z))'] @@ -92,7 +92,7 @@ def _parse_tuple(s: str) -> list: "cannot have consecutive commas in {}".format(s) ) - tuple_strs = [] + tuple_strs: List[str] = [] depth = 0 word = "" for char in s: From 24175d35dccd320c1bdf56486a56237594849458 Mon Sep 17 00:00:00 2001 From: Jacob Daitzman Date: Mon, 5 Dec 2022 17:01:22 -0500 Subject: [PATCH 27/27] Add url metadata to `setup.py` --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 8cee71b7..71e30f3c 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ version="v1.20.2", long_description=long_description, long_description_content_type="text/markdown", + url="https://github.com/algorand/py-algorand-sdk", license="MIT", project_urls={ "Source": "https://github.com/algorand/py-algorand-sdk",