Skip to content

Commit 0f521d0

Browse files
authored
Add type hints and clean up ABI code (#253)
* 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
1 parent d8b29d9 commit 0f521d0

17 files changed

+366
-312
lines changed

algosdk/abi/__init__.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
from .util import type_from_string
2-
from .uint_type import UintType
3-
from .ufixed_type import UfixedType
4-
from .base_type import ABIType
5-
from .bool_type import BoolType
6-
from .byte_type import ByteType
7-
from .address_type import AddressType
8-
from .string_type import StringType
9-
from .array_dynamic_type import ArrayDynamicType
10-
from .array_static_type import ArrayStaticType
11-
from .tuple_type import TupleType
1+
from algosdk.abi.uint_type import UintType
2+
from algosdk.abi.ufixed_type import UfixedType
3+
from algosdk.abi.base_type import ABIType
4+
from algosdk.abi.bool_type import BoolType
5+
from algosdk.abi.byte_type import ByteType
6+
from algosdk.abi.address_type import AddressType
7+
from algosdk.abi.string_type import StringType
8+
from algosdk.abi.array_dynamic_type import ArrayDynamicType
9+
from algosdk.abi.array_static_type import ArrayStaticType
10+
from algosdk.abi.tuple_type import TupleType
1211
from .method import Method, Argument, Returns
1312
from .interface import Interface
1413
from .contract import Contract

algosdk/abi/address_type.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
from .base_type import ABIType
2-
from .byte_type import ByteType
3-
from .tuple_type import TupleType
4-
from .. import error
1+
from typing import Union
2+
3+
from algosdk.abi.base_type import ABIType
4+
from algosdk.abi.byte_type import ByteType
5+
from algosdk.abi.tuple_type import TupleType
6+
from algosdk import error
57

68
from algosdk import encoding
79

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

17-
def __eq__(self, other) -> bool:
19+
def __eq__(self, other: object) -> bool:
1820
if not isinstance(other, AddressType):
1921
return False
2022
return True
2123

22-
def __str__(self):
24+
def __str__(self) -> str:
2325
return "address"
2426

25-
def byte_len(self):
27+
def byte_len(self) -> int:
2628
return 32
2729

28-
def is_dynamic(self):
30+
def is_dynamic(self) -> bool:
2931
return False
3032

3133
def _to_tuple_type(self):
@@ -34,7 +36,7 @@ def _to_tuple_type(self):
3436
child_type_array.append(ByteType())
3537
return TupleType(child_type_array)
3638

37-
def encode(self, value):
39+
def encode(self, value: Union[str, bytes]) -> bytes:
3840
"""
3941
Encode an address string or a 32-byte public key into a Address ABI bytestring.
4042
@@ -62,7 +64,7 @@ def encode(self, value):
6264
)
6365
return bytes(value)
6466

65-
def decode(self, bytestring):
67+
def decode(self, bytestring: Union[bytearray, bytes]) -> str:
6668
"""
6769
Decodes a bytestring to a base32 encoded address string.
6870

algosdk/abi/array_dynamic_type.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,47 @@
1-
from .base_type import ABI_LENGTH_SIZE, ABIType
2-
from .byte_type import ByteType
3-
from .tuple_type import TupleType
4-
from .. import error
1+
from typing import Any, List, NoReturn, Union
2+
3+
from algosdk.abi.base_type import ABI_LENGTH_SIZE, ABIType
4+
from algosdk.abi.byte_type import ByteType
5+
from algosdk.abi.tuple_type import TupleType
6+
from algosdk import error
57

68

79
class ArrayDynamicType(ABIType):
810
"""
911
Represents a ArrayDynamic ABI Type for encoding.
1012
1113
Args:
12-
child_type (Type): the type of the dynamic array.
14+
child_type (ABIType): the type of the dynamic array.
1315
1416
Attributes:
15-
child_type (Type)
17+
child_type (ABIType)
1618
"""
1719

18-
def __init__(self, arg_type) -> None:
20+
def __init__(self, arg_type: ABIType) -> None:
1921
super().__init__()
2022
self.child_type = arg_type
2123

22-
def __eq__(self, other) -> bool:
24+
def __eq__(self, other: object) -> bool:
2325
if not isinstance(other, ArrayDynamicType):
2426
return False
2527
return self.child_type == other.child_type
2628

27-
def __str__(self):
29+
def __str__(self) -> str:
2830
return "{}[]".format(self.child_type)
2931

30-
def byte_len(self):
32+
def byte_len(self) -> NoReturn:
3133
raise error.ABITypeError(
3234
"cannot get length of a dynamic type: {}".format(self)
3335
)
3436

35-
def is_dynamic(self):
37+
def is_dynamic(self) -> bool:
3638
return True
3739

38-
def _to_tuple_type(self, length):
40+
def _to_tuple_type(self, length: int) -> TupleType:
3941
child_type_array = [self.child_type] * length
4042
return TupleType(child_type_array)
4143

42-
def encode(self, value_array):
44+
def encode(self, value_array: Union[List[Any], bytes, bytearray]) -> bytes:
4345
"""
4446
Encodes a list of values into a ArrayDynamic ABI bytestring.
4547
@@ -67,7 +69,7 @@ def encode(self, value_array):
6769
encoded = converted_tuple.encode(value_array)
6870
return bytes(length_to_encode) + encoded
6971

70-
def decode(self, array_bytes):
72+
def decode(self, array_bytes: Union[bytes, bytearray]) -> list:
7173
"""
7274
Decodes a bytestring to a dynamic list.
7375

algosdk/abi/array_static_type.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,63 @@
11
import math
2+
from typing import Any, List, Union
23

3-
from .base_type import ABIType
4-
from .bool_type import BoolType
5-
from .byte_type import ByteType
6-
from .tuple_type import TupleType
7-
from .. import error
4+
from algosdk.abi.base_type import ABIType
5+
from algosdk.abi.bool_type import BoolType
6+
from algosdk.abi.byte_type import ByteType
7+
from algosdk.abi.tuple_type import TupleType
8+
from algosdk import error
89

910

1011
class ArrayStaticType(ABIType):
1112
"""
1213
Represents a ArrayStatic ABI Type for encoding.
1314
1415
Args:
15-
child_type (Type): the type of the child_types array.
16-
static_length (int): length of the static array.
16+
child_type (ABIType): the type of the child_types array.
17+
array_len (int): length of the static array.
1718
1819
Attributes:
19-
child_type (Type)
20+
child_type (ABIType)
2021
static_length (int)
2122
"""
2223

23-
def __init__(self, arg_type, array_len) -> None:
24+
def __init__(self, arg_type: ABIType, array_len: int) -> None:
2425
if array_len < 1:
2526
raise error.ABITypeError(
2627
"static array length must be a positive integer: {}".format(
27-
len(array_len)
28+
array_len
2829
)
2930
)
3031
super().__init__()
3132
self.child_type = arg_type
3233
self.static_length = array_len
3334

34-
def __eq__(self, other) -> bool:
35+
def __eq__(self, other: object) -> bool:
3536
if not isinstance(other, ArrayStaticType):
3637
return False
3738
return (
3839
self.child_type == other.child_type
3940
and self.static_length == other.static_length
4041
)
4142

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

45-
def byte_len(self):
46+
def byte_len(self) -> int:
4647
if isinstance(self.child_type, BoolType):
4748
# 8 Boolean values can be encoded into 1 byte
4849
return math.ceil(self.static_length / 8)
4950
element_byte_length = self.child_type.byte_len()
5051
return self.static_length * element_byte_length
5152

52-
def is_dynamic(self):
53+
def is_dynamic(self) -> bool:
5354
return self.child_type.is_dynamic()
5455

55-
def _to_tuple_type(self):
56+
def _to_tuple_type(self) -> TupleType:
5657
child_type_array = [self.child_type] * self.static_length
5758
return TupleType(child_type_array)
5859

59-
def encode(self, value_array):
60+
def encode(self, value_array: Union[List[Any], bytes, bytearray]) -> bytes:
6061
"""
6162
Encodes a list of values into a ArrayStatic ABI bytestring.
6263
@@ -87,7 +88,7 @@ def encode(self, value_array):
8788
converted_tuple = self._to_tuple_type()
8889
return converted_tuple.encode(value_array)
8990

90-
def decode(self, array_bytes):
91+
def decode(self, array_bytes: Union[bytes, bytearray]) -> list:
9192
"""
9293
Decodes a bytestring to a static list.
9394

algosdk/abi/base_type.py

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,139 @@
11
from abc import ABC, abstractmethod
2+
import re
3+
from typing import Any, Union
4+
5+
from algosdk import error
6+
27

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

613

714
class ABIType(ABC):
815
"""
916
Represents an ABI Type for encoding.
1017
"""
1118

12-
def __init__(
13-
self,
14-
) -> None:
19+
def __init__(self) -> None:
1520
pass
1621

1722
@abstractmethod
18-
def __str__(self):
23+
def __str__(self) -> str:
1924
pass
2025

2126
@abstractmethod
22-
def __eq__(self, other) -> bool:
27+
def __eq__(self, other: object) -> bool:
2328
pass
2429

2530
@abstractmethod
26-
def is_dynamic(self):
31+
def is_dynamic(self) -> bool:
2732
"""
2833
Return whether the ABI type is dynamic.
2934
"""
3035
pass
3136

3237
@abstractmethod
33-
def byte_len(self):
38+
def byte_len(self) -> int:
3439
"""
3540
Return the length in bytes of the ABI type.
3641
"""
3742
pass
3843

3944
@abstractmethod
40-
def encode(self, value):
45+
def encode(self, value: Any) -> bytes:
4146
"""
4247
Serialize the ABI value into a byte string using ABI encoding rules.
4348
"""
4449
pass
4550

4651
@abstractmethod
47-
def decode(self, bytestring):
52+
def decode(self, bytestring: bytes) -> Any:
4853
"""
4954
Deserialize the ABI type and value from a byte string using ABI encoding rules.
5055
"""
5156
pass
57+
58+
@staticmethod
59+
def from_string(s: str) -> "ABIType":
60+
"""
61+
Convert a valid ABI string to a corresponding ABI type.
62+
"""
63+
# We define the imports here to avoid circular imports
64+
from algosdk.abi.uint_type import UintType
65+
from algosdk.abi.ufixed_type import UfixedType
66+
from algosdk.abi.byte_type import ByteType
67+
from algosdk.abi.bool_type import BoolType
68+
from algosdk.abi.address_type import AddressType
69+
from algosdk.abi.string_type import StringType
70+
from algosdk.abi.array_dynamic_type import ArrayDynamicType
71+
from algosdk.abi.array_static_type import ArrayStaticType
72+
from algosdk.abi.tuple_type import TupleType
73+
74+
if s.endswith("[]"):
75+
array_arg_type = ABIType.from_string(s[:-2])
76+
return ArrayDynamicType(array_arg_type)
77+
elif s.endswith("]"):
78+
matches = re.search(STATIC_ARRAY_REGEX, s)
79+
try:
80+
static_length = int(matches.group(2))
81+
array_type = ABIType.from_string(matches.group(1))
82+
return ArrayStaticType(array_type, static_length)
83+
except Exception as e:
84+
raise error.ABITypeError(
85+
"malformed static array string: {}".format(s)
86+
) from e
87+
if s.startswith("uint"):
88+
try:
89+
if not s[4:].isdecimal():
90+
raise error.ABITypeError(
91+
"uint string does not contain a valid size: {}".format(
92+
s
93+
)
94+
)
95+
type_size = int(s[4:])
96+
return UintType(type_size)
97+
except Exception as e:
98+
raise error.ABITypeError(
99+
"malformed uint string: {}".format(s)
100+
) from e
101+
elif s == "byte":
102+
return ByteType()
103+
elif s.startswith("ufixed"):
104+
matches = re.search(UFIXED_REGEX, s)
105+
try:
106+
bit_size = int(matches.group(1))
107+
precision = int(matches.group(2))
108+
return UfixedType(bit_size, precision)
109+
except Exception as e:
110+
raise error.ABITypeError(
111+
"malformed ufixed string: {}".format(s)
112+
) from e
113+
elif s == "bool":
114+
return BoolType()
115+
elif s == "address":
116+
return AddressType()
117+
elif s == "string":
118+
return StringType()
119+
elif len(s) >= 2 and s[0] == "(" and s[-1] == ")":
120+
# Recursively parse parentheses from a tuple string
121+
tuples = TupleType._parse_tuple(s[1:-1])
122+
tuple_list = []
123+
for tup in tuples:
124+
if isinstance(tup, str):
125+
tt = ABIType.from_string(tup)
126+
tuple_list.append(tt)
127+
elif isinstance(tup, list):
128+
tts = [ABIType.from_string(t_) for t_ in tup]
129+
tuple_list.append(tts)
130+
else:
131+
raise error.ABITypeError(
132+
"cannot convert {} to an ABI type".format(tup)
133+
)
134+
135+
return TupleType(tuple_list)
136+
else:
137+
raise error.ABITypeError(
138+
"cannot convert {} to an ABI type".format(s)
139+
)

0 commit comments

Comments
 (0)