Skip to content

Commit b16899e

Browse files
committed
add more tests
1 parent 7ae33ea commit b16899e

File tree

3 files changed

+237
-113
lines changed

3 files changed

+237
-113
lines changed

src/_algopy_testing/primitives/bytes.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,8 @@ def __bool__(self) -> bool:
4343
def __add__(self, other: Bytes | bytes) -> Bytes:
4444
"""Concatenate Bytes with another Bytes or bytes literal e.g. `Bytes(b"Hello ")
4545
+ b"World"`."""
46-
if isinstance(other, Bytes):
47-
return _checked_result(self.value + other.value, "+")
48-
else:
49-
result = self.value + as_bytes(other)
50-
return _checked_result(result, "+")
46+
result = self.value + as_bytes(other)
47+
return _checked_result(result, "+")
5148

5249
def __radd__(self, other: bytes) -> Bytes:
5350
"""Concatenate Bytes with another Bytes or bytes literal e.g. `b"Hello " +

src/_algopy_testing/primitives/fixed_bytes.py

Lines changed: 92 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,31 @@
2020
_TBytesLength_Arg = typing.TypeVar("_TBytesLength_Arg", bound=int)
2121

2222

23+
def _create_class(cls: type, length: int) -> type:
24+
cls_name = f"{cls.__name__}[{length}]"
25+
return types.new_class(
26+
cls_name,
27+
bases=(cls,),
28+
exec_body=lambda ns: ns.update(
29+
_length=length,
30+
),
31+
)
32+
33+
2334
class _FixedBytesMeta(type):
2435
__concrete__: typing.ClassVar[dict[type, type]] = {}
2536

2637
# get or create a type that is parametrized with element_t and length
2738
def __getitem__(cls, length_t: type) -> type:
39+
if length_t == typing.Any:
40+
return cls
41+
2842
cache = cls.__concrete__
2943
if c := cache.get(length_t, None):
3044
return c
3145

3246
length = get_int_literal_from_type_generic(length_t)
33-
cls_name = f"{cls.__name__}[{length}]"
34-
cache[length_t] = c = types.new_class(
35-
cls_name,
36-
bases=(cls,),
37-
exec_body=lambda ns: ns.update(
38-
_length=length,
39-
),
40-
)
47+
cache[length_t] = c = _create_class(cls, length)
4148

4249
return c
4350

@@ -63,9 +70,13 @@ def __init__(self, value: Bytes | bytes | None = None, /):
6370
if value is None:
6471
self.value = b"\x00" * self._length
6572
return
73+
6674
self.value = as_bytes(value)
75+
if not hasattr(self, "_length"):
76+
self._length = len(self.value)
77+
6778
if len(self.value) != self._length:
68-
raise TypeError(f"expected value of length {self._length}, not {len(self.value)}")
79+
raise ValueError(f"expected value of length {self._length}, not {len(self.value)}")
6980

7081
def __repr__(self) -> str:
7182
return repr(self.value)
@@ -74,10 +85,10 @@ def __str__(self) -> str:
7485
return str(self.value)
7586

7687
def __bool__(self) -> bool:
77-
return bool(self.value)
88+
return bool(self._length)
7889

7990
def __len__(self) -> int:
80-
return len(self.value)
91+
return self._length
8192

8293
# mypy suggests due to Liskov below should be other: object
8394
# need to consider ramifications here, ignoring it for now
@@ -96,25 +107,19 @@ def __hash__(self) -> int:
96107
def __add__(self, other: FixedBytes[_TBytesLength_Arg] | Bytes | bytes) -> Bytes:
97108
"""Concatenate FixedBytes with another Bytes or bytes literal e.g.
98109
`FixedBytes[typing.Literal[5]](b"Hello ") + b"World"`."""
99-
if isinstance(other, (Bytes | FixedBytes)):
100-
return _checked_result(self.value + other.value, "+")
101-
else:
102-
result = self.value + as_bytes(other)
103-
return _checked_result(result, "+")
110+
result = self.value + as_bytes(other)
111+
return _checked_result(result, "+")
104112

105-
def __radd__(self, other: Bytes | bytes) -> Bytes:
113+
def __radd__(self, other: FixedBytes[_TBytesLength_Arg] | Bytes | bytes) -> Bytes:
106114
"""Concatenate FixedBytes with another Bytes or bytes literal e.g. `b"Hello " +
107115
FixedBytes[typing.Literal[5]](b"World")`."""
108-
if isinstance(other, (Bytes | FixedBytes)):
109-
return _checked_result(other.value + self.value, "+")
110-
else:
111-
result = as_bytes(other) + self.value
112-
return _checked_result(result, "+")
116+
result = as_bytes(other) + self.value
117+
return _checked_result(result, "+")
113118

114119
@property
115120
def length(self) -> UInt64:
116121
"""Returns the length of the Bytes."""
117-
return UInt64(len(self.value))
122+
return UInt64(self._length)
118123

119124
def __getitem__(
120125
self, index: UInt64 | int | slice
@@ -126,6 +131,8 @@ def __getitem__(
126131
else:
127132
int_index = index.value if isinstance(index, UInt64) else index
128133
int_index = len(self.value) + int_index if int_index < 0 else int_index
134+
if (int_index >= len(self.value)) or (int_index < 0):
135+
raise ValueError("FixedBytes index out of range")
129136
# my_bytes[0:1] => b'j' whereas my_bytes[0] => 106
130137
return Bytes(self.value[slice(int_index, int_index + 1)])
131138

@@ -139,55 +146,77 @@ def __reversed__(self) -> Iterator[Bytes]:
139146
return _FixedBytesIter(self, -1)
140147

141148
@typing.overload
142-
def __and__(self, other: FixedBytes[_TBytesLength]) -> FixedBytes[_TBytesLength]: # type: ignore[overload-overlap]
143-
...
144-
149+
def __and__(self, other: typing.Self) -> typing.Self: ... # type: ignore[overload-overlap]
145150
@typing.overload
146-
def __and__(self, other: FixedBytes[typing.Any] | bytes | Bytes) -> Bytes: ...
147-
148-
def __and__(
149-
self, other: FixedBytes[typing.Any] | bytes | Bytes
150-
) -> FixedBytes[_TBytesLength] | Bytes:
151+
def __and__(self, other: FixedBytes[_TBytesLength_Arg] | bytes | Bytes) -> Bytes: ...
152+
def __and__(self, other: FixedBytes[_TBytesLength_Arg] | bytes | Bytes) -> typing.Self | Bytes:
151153
"""Compute the bitwise AND of the FixedBytes with another FixedBytes, Bytes, or
152154
bytes.
153155
154156
Returns FixedBytes if other has the same length, otherwise returns Bytes.
155157
"""
156158
return self._operate_bitwise(other, "and_")
157159

158-
def __rand__(self, other: FixedBytes[typing.Any] | bytes | Bytes) -> Bytes:
160+
@typing.overload
161+
def __rand__(self, other: typing.Self) -> typing.Self: ... # type: ignore[overload-overlap]
162+
@typing.overload
163+
def __rand__(self, other: FixedBytes[_TBytesLength_Arg] | bytes | Bytes) -> Bytes: ...
164+
def __rand__(
165+
self, other: FixedBytes[_TBytesLength_Arg] | bytes | Bytes
166+
) -> typing.Self | Bytes:
159167
return self & other
160168

161-
@typing.overload
162-
def __or__(self, other: FixedBytes[_TBytesLength]) -> FixedBytes[_TBytesLength]: # type: ignore[overload-overlap]
163-
...
169+
def __iand__(self, other: Bytes | typing.Self | bytes) -> typing.Self: # type: ignore[misc]
170+
other_bytes = as_bytes(other)
171+
other_fixed_bytes = self.__class__(other_bytes)
172+
result = self._operate_bitwise(other_fixed_bytes, "and_")
173+
assert isinstance(result, self.__class__)
174+
return result
164175

165176
@typing.overload
166-
def __or__(self, other: FixedBytes[typing.Any] | bytes | Bytes) -> Bytes: ...
167-
168-
def __or__(
169-
self, other: FixedBytes[typing.Any] | bytes | Bytes
170-
) -> FixedBytes[_TBytesLength] | Bytes:
177+
def __or__(self, other: typing.Self) -> typing.Self: ... # type: ignore[overload-overlap]
178+
@typing.overload
179+
def __or__(self, other: FixedBytes[_TBytesLength_Arg] | bytes | Bytes) -> Bytes: ...
180+
def __or__(self, other: FixedBytes[_TBytesLength_Arg] | bytes | Bytes) -> typing.Self | Bytes:
171181
return self._operate_bitwise(other, "or_")
172182

173-
def __ror__(self, other: FixedBytes[typing.Any] | bytes | Bytes) -> Bytes:
183+
@typing.overload
184+
def __ror__(self, other: typing.Self) -> typing.Self: ... # type: ignore[overload-overlap]
185+
@typing.overload
186+
def __ror__(self, other: FixedBytes[_TBytesLength_Arg] | bytes | Bytes) -> Bytes: ...
187+
def __ror__(self, other: FixedBytes[_TBytesLength_Arg] | bytes | Bytes) -> typing.Self | Bytes:
174188
return self | other
175189

176-
@typing.overload
177-
def __xor__(self, other: FixedBytes[_TBytesLength]) -> FixedBytes[_TBytesLength]: # type: ignore[overload-overlap]
178-
...
190+
def __ior__(self, other: Bytes | typing.Self | bytes) -> typing.Self: # type: ignore[misc]
191+
other_bytes = as_bytes(other)
192+
other_fixed_bytes = self.__class__(other_bytes)
193+
result = self._operate_bitwise(other_fixed_bytes, "or_")
194+
assert isinstance(result, self.__class__)
195+
return result
179196

180197
@typing.overload
181-
def __xor__(self, other: FixedBytes[typing.Any] | bytes | Bytes) -> Bytes: ...
182-
183-
def __xor__(
184-
self, other: FixedBytes[typing.Any] | bytes | Bytes
185-
) -> FixedBytes[_TBytesLength] | Bytes:
198+
def __xor__(self, other: typing.Self) -> typing.Self: ... # type: ignore[overload-overlap]
199+
@typing.overload
200+
def __xor__(self, other: FixedBytes[_TBytesLength_Arg] | bytes | Bytes) -> Bytes: ...
201+
def __xor__(self, other: FixedBytes[_TBytesLength_Arg] | bytes | Bytes) -> typing.Self | Bytes:
186202
return self._operate_bitwise(other, "xor")
187203

188-
def __rxor__(self, other: FixedBytes[typing.Any] | bytes | Bytes) -> Bytes:
204+
@typing.overload
205+
def __rxor__(self, other: typing.Self) -> typing.Self: ... # type: ignore[overload-overlap]
206+
@typing.overload
207+
def __rxor__(self, other: FixedBytes[_TBytesLength_Arg] | bytes | Bytes) -> Bytes: ...
208+
def __rxor__(
209+
self, other: FixedBytes[_TBytesLength_Arg] | bytes | Bytes
210+
) -> typing.Self | Bytes:
189211
return self ^ other
190212

213+
def __ixor__(self, other: Bytes | typing.Self | bytes) -> typing.Self: # type: ignore[misc]
214+
other_bytes = as_bytes(other)
215+
other_fixed_bytes = self.__class__(other_bytes)
216+
result = self._operate_bitwise(other_fixed_bytes, "xor")
217+
assert isinstance(result, self.__class__)
218+
return result
219+
191220
def __invert__(self) -> typing.Self:
192221
"""Compute the bitwise inversion of the Bytes.
193222
@@ -198,9 +227,9 @@ def __invert__(self) -> typing.Self:
198227

199228
def _operate_bitwise(
200229
self,
201-
other: FixedBytes[typing.Any] | bytes | Bytes,
230+
other: FixedBytes[_TBytesLength_Arg] | bytes | Bytes,
202231
operator_name: str,
203-
) -> FixedBytes[_TBytesLength] | Bytes:
232+
) -> typing.Self | Bytes:
204233
op = getattr(operator, operator_name)
205234
maybe_bytes = as_bytes(other)
206235
# pad the shorter of self.value and other bytes with leading zero
@@ -227,27 +256,32 @@ def from_base32(cls, value: str) -> typing.Self:
227256
"""Creates Bytes from a base32 encoded string e.g.
228257
`Bytes.from_base32("74======")`"""
229258
bytes_value = base64.b32decode(value)
230-
return cls(bytes_value)
259+
c = _create_class(cls, len(bytes_value)) if not hasattr(cls, "_length") else cls
260+
return c(bytes_value) # type: ignore[no-any-return]
231261

232262
@classmethod
233263
def from_base64(cls, value: str) -> typing.Self:
234264
"""Creates Bytes from a base64 encoded string e.g.
235265
`Bytes.from_base64("RkY=")`"""
236266
bytes_value = base64.b64decode(value)
237-
return cls(bytes_value)
267+
c = _create_class(cls, len(bytes_value)) if not hasattr(cls, "_length") else cls
268+
return c(bytes_value) # type: ignore[no-any-return]
238269

239270
@classmethod
240271
def from_hex(cls, value: str) -> typing.Self:
241272
"""Creates Bytes from a hex/octal encoded string e.g. `Bytes.from_hex("FF")`"""
242273
bytes_value = base64.b16decode(value)
243-
return cls(bytes_value)
274+
c = _create_class(cls, len(bytes_value)) if not hasattr(cls, "_length") else cls
275+
return c(bytes_value) # type: ignore[no-any-return]
244276

245277
@classmethod
246278
def from_bytes(cls, value: Bytes | bytes) -> typing.Self:
247279
"""Construct an instance from the underlying bytes (no validation)"""
248-
result = cls()
249-
result.value = as_bytes(value)
250-
return result
280+
bytes_value = as_bytes(value)
281+
c = _create_class(cls, len(bytes_value)) if not hasattr(cls, "_length") else cls
282+
result = c()
283+
result.value = bytes_value
284+
return result # type: ignore[no-any-return]
251285

252286
@property
253287
def bytes(self) -> Bytes:

0 commit comments

Comments
 (0)