Skip to content

Commit

Permalink
Add type hints and clean up ABI code (#253)
Browse files Browse the repository at this point in the history
* Add type hints and clean up ABI types

* Change relative imports to absolute imports

* Add type hints for ABI objects

* Refactor composer with type hints

* Address PR comments, change list type hints, and move type_from_string to ABIType
  • Loading branch information
algochoi authored and aldur committed Feb 8, 2022
1 parent bd0093c commit 44d3b94
Show file tree
Hide file tree
Showing 17 changed files with 366 additions and 312 deletions.
21 changes: 10 additions & 11 deletions algosdk/abi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
from .util import type_from_string
from .uint_type import UintType
from .ufixed_type import UfixedType
from .base_type import ABIType
from .bool_type import BoolType
from .byte_type import ByteType
from .address_type import AddressType
from .string_type import StringType
from .array_dynamic_type import ArrayDynamicType
from .array_static_type import ArrayStaticType
from .tuple_type import TupleType
from algosdk.abi.uint_type import UintType
from algosdk.abi.ufixed_type import UfixedType
from algosdk.abi.base_type import ABIType
from algosdk.abi.bool_type import BoolType
from algosdk.abi.byte_type import ByteType
from algosdk.abi.address_type import AddressType
from algosdk.abi.string_type import StringType
from algosdk.abi.array_dynamic_type import ArrayDynamicType
from algosdk.abi.array_static_type import ArrayStaticType
from algosdk.abi.tuple_type import TupleType
from .method import Method, Argument, Returns
from .interface import Interface
from .contract import Contract
Expand Down
22 changes: 12 additions & 10 deletions algosdk/abi/address_type.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from .base_type import ABIType
from .byte_type import ByteType
from .tuple_type import TupleType
from .. import error
from typing import Union

from algosdk.abi.base_type import ABIType
from algosdk.abi.byte_type import ByteType
from algosdk.abi.tuple_type import TupleType
from algosdk import error

from algosdk import encoding

Expand All @@ -14,18 +16,18 @@ class AddressType(ABIType):
def __init__(self) -> None:
super().__init__()

def __eq__(self, other) -> bool:
def __eq__(self, other: object) -> bool:
if not isinstance(other, AddressType):
return False
return True

def __str__(self):
def __str__(self) -> str:
return "address"

def byte_len(self):
def byte_len(self) -> int:
return 32

def is_dynamic(self):
def is_dynamic(self) -> bool:
return False

def _to_tuple_type(self):
Expand All @@ -34,7 +36,7 @@ def _to_tuple_type(self):
child_type_array.append(ByteType())
return TupleType(child_type_array)

def encode(self, value):
def encode(self, value: Union[str, bytes]) -> bytes:
"""
Encode an address string or a 32-byte public key into a Address ABI bytestring.
Expand Down Expand Up @@ -62,7 +64,7 @@ def encode(self, value):
)
return bytes(value)

def decode(self, bytestring):
def decode(self, bytestring: Union[bytearray, bytes]) -> str:
"""
Decodes a bytestring to a base32 encoded address string.
Expand Down
30 changes: 16 additions & 14 deletions algosdk/abi/array_dynamic_type.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,47 @@
from .base_type import ABI_LENGTH_SIZE, ABIType
from .byte_type import ByteType
from .tuple_type import TupleType
from .. import error
from typing import Any, List, NoReturn, Union

from algosdk.abi.base_type import ABI_LENGTH_SIZE, ABIType
from algosdk.abi.byte_type import ByteType
from algosdk.abi.tuple_type import TupleType
from algosdk import error


class ArrayDynamicType(ABIType):
"""
Represents a ArrayDynamic ABI Type for encoding.
Args:
child_type (Type): the type of the dynamic array.
child_type (ABIType): the type of the dynamic array.
Attributes:
child_type (Type)
child_type (ABIType)
"""

def __init__(self, arg_type) -> None:
def __init__(self, arg_type: ABIType) -> None:
super().__init__()
self.child_type = arg_type

def __eq__(self, other) -> bool:
def __eq__(self, other: object) -> bool:
if not isinstance(other, ArrayDynamicType):
return False
return self.child_type == other.child_type

def __str__(self):
def __str__(self) -> str:
return "{}[]".format(self.child_type)

def byte_len(self):
def byte_len(self) -> NoReturn:
raise error.ABITypeError(
"cannot get length of a dynamic type: {}".format(self)
)

def is_dynamic(self):
def is_dynamic(self) -> bool:
return True

def _to_tuple_type(self, length):
def _to_tuple_type(self, length: int) -> TupleType:
child_type_array = [self.child_type] * length
return TupleType(child_type_array)

def encode(self, value_array):
def encode(self, value_array: Union[List[Any], bytes, bytearray]) -> bytes:
"""
Encodes a list of values into a ArrayDynamic ABI bytestring.
Expand Down Expand Up @@ -67,7 +69,7 @@ def encode(self, value_array):
encoded = converted_tuple.encode(value_array)
return bytes(length_to_encode) + encoded

def decode(self, array_bytes):
def decode(self, array_bytes: Union[bytes, bytearray]) -> list:
"""
Decodes a bytestring to a dynamic list.
Expand Down
35 changes: 18 additions & 17 deletions algosdk/abi/array_static_type.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,63 @@
import math
from typing import Any, List, Union

from .base_type import ABIType
from .bool_type import BoolType
from .byte_type import ByteType
from .tuple_type import TupleType
from .. import error
from algosdk.abi.base_type import ABIType
from algosdk.abi.bool_type import BoolType
from algosdk.abi.byte_type import ByteType
from algosdk.abi.tuple_type import TupleType
from algosdk import error


class ArrayStaticType(ABIType):
"""
Represents a ArrayStatic ABI Type for encoding.
Args:
child_type (Type): the type of the child_types array.
static_length (int): length of the static array.
child_type (ABIType): the type of the child_types array.
array_len (int): length of the static array.
Attributes:
child_type (Type)
child_type (ABIType)
static_length (int)
"""

def __init__(self, arg_type, array_len) -> None:
def __init__(self, arg_type: ABIType, array_len: int) -> None:
if array_len < 1:
raise error.ABITypeError(
"static array length must be a positive integer: {}".format(
len(array_len)
array_len
)
)
super().__init__()
self.child_type = arg_type
self.static_length = array_len

def __eq__(self, other) -> bool:
def __eq__(self, other: object) -> bool:
if not isinstance(other, ArrayStaticType):
return False
return (
self.child_type == other.child_type
and self.static_length == other.static_length
)

def __str__(self):
def __str__(self) -> str:
return "{}[{}]".format(self.child_type, self.static_length)

def byte_len(self):
def byte_len(self) -> int:
if isinstance(self.child_type, BoolType):
# 8 Boolean values can be encoded into 1 byte
return math.ceil(self.static_length / 8)
element_byte_length = self.child_type.byte_len()
return self.static_length * element_byte_length

def is_dynamic(self):
def is_dynamic(self) -> bool:
return self.child_type.is_dynamic()

def _to_tuple_type(self):
def _to_tuple_type(self) -> TupleType:
child_type_array = [self.child_type] * self.static_length
return TupleType(child_type_array)

def encode(self, value_array):
def encode(self, value_array: Union[List[Any], bytes, bytearray]) -> bytes:
"""
Encodes a list of values into a ArrayStatic ABI bytestring.
Expand Down Expand Up @@ -87,7 +88,7 @@ def encode(self, value_array):
converted_tuple = self._to_tuple_type()
return converted_tuple.encode(value_array)

def decode(self, array_bytes):
def decode(self, array_bytes: Union[bytes, bytearray]) -> list:
"""
Decodes a bytestring to a static list.
Expand Down
106 changes: 97 additions & 9 deletions algosdk/abi/base_type.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,139 @@
from abc import ABC, abstractmethod
import re
from typing import Any, Union

from algosdk import error


# Globals
ABI_LENGTH_SIZE = 2 # We use 2 bytes to encode the length of a dynamic element
UFIXED_REGEX = r"^ufixed([1-9][\d]*)x([1-9][\d]*)$"
STATIC_ARRAY_REGEX = r"^([a-z\d\[\](),]+)\[([1-9][\d]*)]$"


class ABIType(ABC):
"""
Represents an ABI Type for encoding.
"""

def __init__(
self,
) -> None:
def __init__(self) -> None:
pass

@abstractmethod
def __str__(self):
def __str__(self) -> str:
pass

@abstractmethod
def __eq__(self, other) -> bool:
def __eq__(self, other: object) -> bool:
pass

@abstractmethod
def is_dynamic(self):
def is_dynamic(self) -> bool:
"""
Return whether the ABI type is dynamic.
"""
pass

@abstractmethod
def byte_len(self):
def byte_len(self) -> int:
"""
Return the length in bytes of the ABI type.
"""
pass

@abstractmethod
def encode(self, value):
def encode(self, value: Any) -> bytes:
"""
Serialize the ABI value into a byte string using ABI encoding rules.
"""
pass

@abstractmethod
def decode(self, bytestring):
def decode(self, bytestring: bytes) -> Any:
"""
Deserialize the ABI type and value from a byte string using ABI encoding rules.
"""
pass

@staticmethod
def from_string(s: str) -> "ABIType":
"""
Convert a valid ABI string to a corresponding ABI type.
"""
# We define the imports here to avoid circular imports
from algosdk.abi.uint_type import UintType
from algosdk.abi.ufixed_type import UfixedType
from algosdk.abi.byte_type import ByteType
from algosdk.abi.bool_type import BoolType
from algosdk.abi.address_type import AddressType
from algosdk.abi.string_type import StringType
from algosdk.abi.array_dynamic_type import ArrayDynamicType
from algosdk.abi.array_static_type import ArrayStaticType
from algosdk.abi.tuple_type import TupleType

if s.endswith("[]"):
array_arg_type = ABIType.from_string(s[:-2])
return ArrayDynamicType(array_arg_type)
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))
return ArrayStaticType(array_type, static_length)
except Exception as e:
raise error.ABITypeError(
"malformed static array string: {}".format(s)
) from e
if s.startswith("uint"):
try:
if not s[4:].isdecimal():
raise error.ABITypeError(
"uint string does not contain a valid size: {}".format(
s
)
)
type_size = int(s[4:])
return UintType(type_size)
except Exception as e:
raise error.ABITypeError(
"malformed uint string: {}".format(s)
) from e
elif s == "byte":
return ByteType()
elif s.startswith("ufixed"):
matches = re.search(UFIXED_REGEX, s)
try:
bit_size = int(matches.group(1))
precision = int(matches.group(2))
return UfixedType(bit_size, precision)
except Exception as e:
raise error.ABITypeError(
"malformed ufixed string: {}".format(s)
) from e
elif s == "bool":
return BoolType()
elif s == "address":
return AddressType()
elif s == "string":
return StringType()
elif len(s) >= 2 and s[0] == "(" and s[-1] == ")":
# Recursively parse parentheses from a tuple string
tuples = TupleType._parse_tuple(s[1:-1])
tuple_list = []
for tup in tuples:
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)
)

return TupleType(tuple_list)
else:
raise error.ABITypeError(
"cannot convert {} to an ABI type".format(s)
)
Loading

0 comments on commit 44d3b94

Please sign in to comment.