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/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/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. 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/base_type.py b/algosdk/abi/base_type.py index 0e6ac628..7b5021ad 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[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)) - precision = int(matches.group(2)) + 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( @@ -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.append(tts) else: raise error.ABITypeError( "cannot convert {} to an ABI type".format(tup) 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..ccaae47c 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. @@ -50,7 +50,7 @@ def decode(self, bytestring: Union[bytes, bytearray]) -> bytes: bytestring (bytes | bytearray): bytestring to be decoded Returns: - bytes: byte of the encoded bytestring + int: byte value of the encoded bytestring """ if ( not ( @@ -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/contract.py b/algosdk/abi/contract.py index cfeb7b11..9a728c54 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 diff --git a/algosdk/abi/interface.py b/algosdk/abi/interface.py index 5bd79aa4..aaf07655 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 diff --git a/algosdk/abi/method.py b/algosdk/abi/method.py index 08397de0..3bd21842 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: 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) @@ -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: 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 baa0dda6..62fecb46 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 @@ -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: @@ -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[Union[bytes, bytearray]]] = list() i = 0 array_index = 0 @@ -291,9 +294,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 +303,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) diff --git a/algosdk/atomic_transaction_composer.py b/algosdk/atomic_transaction_composer.py index 2f556431..8f4ab521 100644 --- a/algosdk/atomic_transaction_composer.py +++ b/algosdk/atomic_transaction_composer.py @@ -2,7 +2,16 @@ 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, + cast, +) from algosdk import abi, error from algosdk.abi.address_type import AddressType @@ -37,7 +46,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 +77,13 @@ def populate_foreign_array( return offset + len(foreign_array) - 1 +GenericSignedTransaction = Union[ + transaction.SignedTransaction, + transaction.LogicSigTransaction, + transaction.MultisigTransaction, +] + + class AtomicTransactionComposer: """ Constructs an atomic transaction group which may contain a combination of @@ -77,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 """ @@ -88,10 +104,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[GenericSignedTransaction] = [] + self.tx_ids: List[str] = [] def get_status(self) -> AtomicTransactionComposerStatus: """ @@ -160,20 +176,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 +282,7 @@ def add_method_call( boxes = boxes[:] if boxes else [] app_args = [] - raw_values = [] + raw_values: List[Any] = [] raw_types = [] txn_list = [] @@ -288,22 +306,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: Union[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 ) @@ -398,14 +418,18 @@ 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[GenericSignedTransaction]: list of signed transactions """ if self.status >= AtomicTransactionComposerStatus.SIGNED: # 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[GenericSignedTransaction]] = [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,12 +448,13 @@ def gather_signatures(self) -> list: raise error.AtomicTransactionComposerError( "missing signatures, got {}".format(stxn_list) ) + full_stxn_list = cast(List[GenericSignedTransaction], 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: + 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. @@ -442,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() @@ -496,10 +521,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 +536,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 +563,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) @@ -572,7 +598,7 @@ def __init__(self) -> None: @abstractmethod def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> list: + ) -> List[GenericSignedTransaction]: pass @@ -591,7 +617,7 @@ def __init__(self, private_key: str) -> None: def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> list: + ) -> List[GenericSignedTransaction]: """ Sign transactions in a transaction group given the indexes. @@ -625,7 +651,7 @@ def __init__(self, lsig: transaction.LogicSigAccount) -> None: def sign_transactions( self, txn_group: List[transaction.Transaction], indexes: List[int] - ) -> list: + ) -> List[GenericSignedTransaction]: """ Sign transactions in a transaction group given the indexes. @@ -637,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[GenericSignedTransaction] = [] for i in indexes: stxn = transaction.LogicSigTransaction(txn_group[i], self.lsig) stxns.append(stxn) @@ -661,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[GenericSignedTransaction]: """ Sign transactions in a transaction group given the indexes. @@ -673,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[GenericSignedTransaction] = [] for i in indexes: mtxn = transaction.MultisigTransaction(txn_group[i], self.msig) for sk in self.sks: @@ -693,7 +719,7 @@ def __init__( class ABIResult: def __init__( self, - tx_id: int, + tx_id: str, raw_value: bytes, return_value: Any, decode_error: Optional[Exception], diff --git a/algosdk/constants.py b/algosdk/constants.py index 6ddba7d4..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 = [] +NO_AUTH: List[str] = [] """str[]: requests that don't require authentication""" diff --git a/algosdk/dryrun_results.py b/algosdk/dryrun_results.py index cc0efdd6..29520784 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[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: - return "REJECT" in self.logic_sig_messages + 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 @@ -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[attr-defined] # 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]) 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)] diff --git a/algosdk/future/transaction.py b/algosdk/future/transaction.py index 15eab421..7e52515c 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 @@ -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 @@ -2161,7 +2163,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 +2295,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 +2739,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 +2754,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 +2915,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 @@ -3273,35 +3281,36 @@ 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 txn.accounts: - accts.extend(txn.accounts) - if 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 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 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 + ls = appTxn.local_schema if ls is not None: ls = models.ApplicationStateSchema( ls.num_uints, ls.num_byte_slices ) - gs = txn.global_schema + gs = appTxn.global_schema if gs is not None: gs = models.ApplicationStateSchema( gs.num_uints, gs.num_byte_slices @@ -3311,16 +3320,17 @@ def create_dryrun( models.Application( id=appId, params=models.ApplicationParams( - creator=txn.sender, - approval_program=txn.approval_program, - clear_state_program=txn.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: - 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] diff --git a/algosdk/py.typed b/algosdk/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/algosdk/source_map.py b/algosdk/source_map.py index b459f9a6..d72a2129 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, Tuple, 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) 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)] diff --git a/algosdk/testing/dryrun.py b/algosdk/testing/dryrun.py index bc561e16..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 List, Union +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: 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: diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..f8b1844b --- /dev/null +++ b/mypy.ini @@ -0,0 +1 @@ +[mypy] diff --git a/requirements.txt b/requirements.txt index 95843578..c3d13e9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ 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 diff --git a/setup.py b/setup.py index 83dc6c3d..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", @@ -23,6 +24,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, )