Skip to content

Commit 5eb4708

Browse files
authored
ssz: proper error typing (#241)
* ssz: proper error typing * cleanup * simplify error types
1 parent e167f5a commit 5eb4708

File tree

15 files changed

+269
-186
lines changed

15 files changed

+269
-186
lines changed

src/lean_spec/types/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,18 @@
66
from .byte_arrays import ZERO_HASH, Bytes32, Bytes52, Bytes3116
77
from .collections import SSZList, SSZVector
88
from .container import Container
9+
from .exceptions import (
10+
SSZError,
11+
SSZSerializationError,
12+
SSZTypeError,
13+
SSZValueError,
14+
)
915
from .ssz_base import SSZType
1016
from .uint import Uint64
1117
from .validator import is_proposer
1218

1319
__all__ = [
20+
# Core types
1421
"Uint64",
1522
"BasisPoint",
1623
"Bytes32",
@@ -25,4 +32,9 @@
2532
"SSZType",
2633
"Boolean",
2734
"Container",
35+
# Exceptions
36+
"SSZError",
37+
"SSZTypeError",
38+
"SSZValueError",
39+
"SSZSerializationError",
2840
]

src/lean_spec/types/bitfields.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from typing_extensions import Self
3030

3131
from .boolean import Boolean
32+
from .exceptions import SSZSerializationError, SSZTypeError, SSZValueError
3233
from .ssz_base import SSZModel
3334

3435

@@ -55,13 +56,15 @@ class BaseBitvector(SSZModel):
5556
def _coerce_and_validate(cls, v: Any) -> tuple[Boolean, ...]:
5657
"""Validate and convert input data to typed tuple of Booleans."""
5758
if not hasattr(cls, "LENGTH"):
58-
raise TypeError(f"{cls.__name__} must define LENGTH")
59+
raise SSZTypeError(f"{cls.__name__} must define LENGTH")
5960

6061
if not isinstance(v, (list, tuple)):
6162
v = tuple(v)
6263

6364
if len(v) != cls.LENGTH:
64-
raise ValueError(f"{cls.__name__} requires exactly {cls.LENGTH} bits, got {len(v)}")
65+
raise SSZValueError(
66+
f"{cls.__name__} requires exactly {cls.LENGTH} elements, got {len(v)}"
67+
)
6568

6669
return tuple(Boolean(bit) for bit in v)
6770

@@ -86,10 +89,12 @@ def deserialize(cls, stream: IO[bytes], scope: int) -> Self:
8689
"""Read SSZ bytes from a stream and return an instance."""
8790
expected_len = cls.get_byte_length()
8891
if scope != expected_len:
89-
raise ValueError(f"{cls.__name__}: expected {expected_len} bytes, got {scope}")
92+
raise SSZSerializationError(
93+
f"{cls.__name__}: expected {expected_len} bytes, got {scope}"
94+
)
9095
data = stream.read(scope)
9196
if len(data) != scope:
92-
raise IOError(f"Expected {scope} bytes, got {len(data)}")
97+
raise SSZSerializationError(f"{cls.__name__}: expected {scope} bytes, got {len(data)}")
9398
return cls.decode_bytes(data)
9499

95100
def encode_bytes(self) -> bytes:
@@ -115,7 +120,7 @@ def decode_bytes(cls, data: bytes) -> Self:
115120
"""
116121
expected = cls.get_byte_length()
117122
if len(data) != expected:
118-
raise ValueError(f"{cls.__name__}: expected {expected} bytes, got {len(data)}")
123+
raise SSZValueError(f"{cls.__name__}: expected {expected} bytes, got {len(data)}")
119124

120125
bits = tuple(Boolean((data[i // 8] >> (i % 8)) & 1) for i in range(cls.LENGTH))
121126
return cls(data=bits)
@@ -144,19 +149,19 @@ class BaseBitlist(SSZModel):
144149
def _coerce_and_validate(cls, v: Any) -> tuple[Boolean, ...]:
145150
"""Validate and convert input to a tuple of Boolean elements."""
146151
if not hasattr(cls, "LIMIT"):
147-
raise TypeError(f"{cls.__name__} must define LIMIT")
152+
raise SSZTypeError(f"{cls.__name__} must define LIMIT")
148153

149154
# Handle various input types
150155
if isinstance(v, (list, tuple)):
151156
elements = v
152157
elif hasattr(v, "__iter__") and not isinstance(v, (str, bytes)):
153158
elements = list(v)
154159
else:
155-
raise TypeError(f"Bitlist data must be iterable, got {type(v)}")
160+
raise SSZTypeError(f"Expected iterable, got {type(v).__name__}")
156161

157162
# Check limit
158163
if len(elements) > cls.LIMIT:
159-
raise ValueError(f"{cls.__name__} cannot exceed {cls.LIMIT} bits, got {len(elements)}")
164+
raise SSZValueError(f"{cls.__name__} exceeds limit of {cls.LIMIT}, got {len(elements)}")
160165

161166
return tuple(Boolean(bit) for bit in elements)
162167

@@ -197,8 +202,8 @@ def is_fixed_size(cls) -> bool:
197202

198203
@classmethod
199204
def get_byte_length(cls) -> int:
200-
"""Lists are variable-size, so this raises a TypeError."""
201-
raise TypeError(f"{cls.__name__} is variable-size")
205+
"""Lists are variable-size, so this raises an SSZTypeError."""
206+
raise SSZTypeError(f"{cls.__name__}: variable-size bitlist has no fixed byte length")
202207

203208
def serialize(self, stream: IO[bytes]) -> int:
204209
"""Write SSZ bytes to a binary stream."""
@@ -211,7 +216,7 @@ def deserialize(cls, stream: IO[bytes], scope: int) -> Self:
211216
"""Read SSZ bytes from a stream and return an instance."""
212217
data = stream.read(scope)
213218
if len(data) != scope:
214-
raise IOError(f"Expected {scope} bytes, got {len(data)}")
219+
raise SSZSerializationError(f"{cls.__name__}: expected {scope} bytes, got {len(data)}")
215220
return cls.decode_bytes(data)
216221

217222
def encode_bytes(self) -> bytes:
@@ -254,7 +259,7 @@ def decode_bytes(cls, data: bytes) -> Self:
254259
the last data bit. All bits after the delimiter are assumed to be 0.
255260
"""
256261
if len(data) == 0:
257-
raise ValueError("Cannot decode empty bytes to Bitlist")
262+
raise SSZSerializationError(f"{cls.__name__}: cannot decode empty bytes")
258263

259264
# Find the position of the delimiter bit (rightmost 1).
260265
delimiter_pos = None
@@ -267,12 +272,12 @@ def decode_bytes(cls, data: bytes) -> Self:
267272
break
268273

269274
if delimiter_pos is None:
270-
raise ValueError("No delimiter bit found in Bitlist data")
275+
raise SSZSerializationError(f"{cls.__name__}: no delimiter bit found")
271276

272277
# Extract data bits (everything before the delimiter).
273278
num_bits = delimiter_pos
274279
if num_bits > cls.LIMIT:
275-
raise ValueError(f"{cls.__name__} decoded length {num_bits} exceeds limit {cls.LIMIT}")
280+
raise SSZValueError(f"{cls.__name__} exceeds limit of {cls.LIMIT}, got {num_bits}")
276281

277282
bits = tuple(Boolean((data[i // 8] >> (i % 8)) & 1) for i in range(num_bits))
278283
return cls(data=bits)

src/lean_spec/types/boolean.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pydantic_core import CoreSchema, core_schema
99
from typing_extensions import Self
1010

11+
from .exceptions import SSZSerializationError, SSZTypeError, SSZValueError
1112
from .ssz_base import SSZType
1213

1314

@@ -31,14 +32,14 @@ def __new__(cls, value: bool | int) -> Self:
3132
Accepts only `True`, `False`, `1`, or `0`.
3233
3334
Raises:
34-
TypeError: If `value` is not a bool or int.
35-
ValueError: If `value` is an integer other than 0 or 1.
35+
SSZTypeCoercionError: If `value` is not a bool or int.
36+
SSZDecodeError: If `value` is an integer other than 0 or 1.
3637
"""
3738
if not isinstance(value, int):
38-
raise TypeError(f"Expected bool or int, got {type(value).__name__}")
39+
raise SSZTypeError(f"Expected bool or int, got {type(value).__name__}")
3940

4041
if value not in (0, 1):
41-
raise ValueError(f"Boolean value must be 0 or 1, not {value}")
42+
raise SSZValueError(f"Boolean value must be 0 or 1, not {value}")
4243

4344
return super().__new__(cls, value)
4445

@@ -93,9 +94,9 @@ def encode_bytes(self) -> bytes:
9394
def decode_bytes(cls, data: bytes) -> Self:
9495
"""Deserialize a single byte into a Boolean instance."""
9596
if len(data) != 1:
96-
raise ValueError(f"Expected 1 byte for Boolean, got {len(data)}")
97+
raise SSZSerializationError(f"Boolean: expected 1 byte, got {len(data)}")
9798
if data[0] not in (0, 1):
98-
raise ValueError(f"Boolean byte must be 0x00 or 0x01, got {data[0]:#04x}")
99+
raise SSZSerializationError(f"Boolean: byte must be 0x00 or 0x01, got {data[0]:#04x}")
99100
return cls(data[0])
100101

101102
def serialize(self, stream: IO[bytes]) -> int:
@@ -108,10 +109,10 @@ def serialize(self, stream: IO[bytes]) -> int:
108109
def deserialize(cls, stream: IO[bytes], scope: int) -> Self:
109110
"""Deserialize a boolean from a binary stream."""
110111
if scope != 1:
111-
raise ValueError(f"Invalid scope for Boolean: expected 1, got {scope}")
112+
raise SSZSerializationError(f"Boolean: expected scope of 1, got {scope}")
112113
data = stream.read(1)
113114
if len(data) != 1:
114-
raise IOError("Stream ended prematurely while decoding Boolean")
115+
raise SSZSerializationError(f"Boolean: expected 1 byte, got {len(data)}")
115116
return cls.decode_bytes(data)
116117

117118
def _raise_type_error(self, other: Any, op_symbol: str) -> None:

src/lean_spec/types/byte_arrays.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pydantic_core import core_schema
1717
from typing_extensions import Self
1818

19+
from .exceptions import SSZSerializationError, SSZTypeError, SSZValueError
1920
from .ssz_base import SSZModel, SSZType
2021

2122

@@ -64,14 +65,15 @@ def __new__(cls, value: Any = b"") -> Self:
6465
value: Any value coercible to bytes (see `_coerce_to_bytes`).
6566
6667
Raises:
67-
ValueError: If the resulting byte length differs from `LENGTH`.
68+
SSZTypeDefinitionError: If the class doesn't define LENGTH.
69+
SSZLengthError: If the resulting byte length differs from `LENGTH`.
6870
"""
6971
if not hasattr(cls, "LENGTH"):
70-
raise TypeError(f"{cls.__name__} must define LENGTH")
72+
raise SSZTypeError(f"{cls.__name__} must define LENGTH")
7173

7274
b = _coerce_to_bytes(value)
7375
if len(b) != cls.LENGTH:
74-
raise ValueError(f"{cls.__name__} expects exactly {cls.LENGTH} bytes, got {len(b)}")
76+
raise SSZValueError(f"{cls.__name__} requires exactly {cls.LENGTH} bytes, got {len(b)}")
7577
return super().__new__(cls, b)
7678

7779
@classmethod
@@ -112,16 +114,14 @@ def deserialize(cls, stream: IO[bytes], scope: int) -> Self:
112114
For a fixed-size type, `scope` must match `LENGTH`.
113115
114116
Raises:
115-
ValueError: if `scope` != `LENGTH`.
116-
IOError: if the stream ends prematurely.
117+
SSZDecodeError: if `scope` != `LENGTH`.
118+
SSZStreamError: if the stream ends prematurely.
117119
"""
118120
if scope != cls.LENGTH:
119-
raise ValueError(
120-
f"Invalid scope for ByteVector[{cls.LENGTH}]: expected {cls.LENGTH}, got {scope}"
121-
)
121+
raise SSZSerializationError(f"{cls.__name__}: expected {cls.LENGTH} bytes, got {scope}")
122122
data = stream.read(scope)
123123
if len(data) != scope:
124-
raise IOError("Stream ended prematurely while decoding ByteVector")
124+
raise SSZSerializationError(f"{cls.__name__}: expected {scope} bytes, got {len(data)}")
125125
return cls(data)
126126

127127
def encode_bytes(self) -> bytes:
@@ -136,7 +136,9 @@ def decode_bytes(cls, data: bytes) -> Self:
136136
For a fixed-size type, the data must be exactly `LENGTH` bytes.
137137
"""
138138
if len(data) != cls.LENGTH:
139-
raise ValueError(f"{cls.__name__} expects exactly {cls.LENGTH} bytes, got {len(data)}")
139+
raise SSZValueError(
140+
f"{cls.__name__} requires exactly {cls.LENGTH} bytes, got {len(data)}"
141+
)
140142
return cls(data)
141143

142144
@classmethod
@@ -262,11 +264,11 @@ class BaseByteList(SSZModel):
262264
def _validate_byte_list_data(cls, v: Any) -> bytes:
263265
"""Validate and convert input to bytes with limit checking."""
264266
if not hasattr(cls, "LIMIT"):
265-
raise TypeError(f"{cls.__name__} must define LIMIT")
267+
raise SSZTypeError(f"{cls.__name__} must define LIMIT")
266268

267269
b = _coerce_to_bytes(v)
268270
if len(b) > cls.LIMIT:
269-
raise ValueError(f"ByteList[{cls.LIMIT}] length {len(b)} exceeds limit {cls.LIMIT}")
271+
raise SSZValueError(f"{cls.__name__} exceeds limit of {cls.LIMIT}, got {len(b)}")
270272
return b
271273

272274
@field_serializer("data", when_used="json")
@@ -282,7 +284,7 @@ def is_fixed_size(cls) -> bool:
282284
@classmethod
283285
def get_byte_length(cls) -> int:
284286
"""ByteList is variable-size, so this should not be called."""
285-
raise TypeError(f"{cls.__name__} is variable-size and has no fixed byte length")
287+
raise SSZTypeError(f"{cls.__name__}: variable-size byte list has no fixed byte length")
286288

287289
def serialize(self, stream: IO[bytes]) -> int:
288290
"""
@@ -303,16 +305,17 @@ def deserialize(cls, stream: IO[bytes], scope: int) -> Self:
303305
knows how many bytes belong to this value in its context).
304306
305307
Raises:
306-
ValueError: if the decoded length exceeds `LIMIT`.
307-
IOError: if the stream ends prematurely.
308+
SSZDecodeError: if the scope is negative.
309+
SSZLengthError: if the decoded length exceeds `LIMIT`.
310+
SSZStreamError: if the stream ends prematurely.
308311
"""
309312
if scope < 0:
310-
raise ValueError("Invalid scope for ByteList: negative")
313+
raise SSZSerializationError(f"{cls.__name__}: negative scope")
311314
if scope > cls.LIMIT:
312-
raise ValueError(f"ByteList[{cls.LIMIT}] scope {scope} exceeds limit")
315+
raise SSZValueError(f"{cls.__name__} exceeds limit of {cls.LIMIT}, got {scope}")
313316
data = stream.read(scope)
314317
if len(data) != scope:
315-
raise IOError("Stream ended prematurely while decoding ByteList")
318+
raise SSZSerializationError(f"{cls.__name__}: expected {scope} bytes, got {len(data)}")
316319
return cls(data=data)
317320

318321
def encode_bytes(self) -> bytes:
@@ -327,7 +330,7 @@ def decode_bytes(cls, data: bytes) -> Self:
327330
For variable-size types, the data length must be `<= LIMIT`.
328331
"""
329332
if len(data) > cls.LIMIT:
330-
raise ValueError(f"ByteList[{cls.LIMIT}] length {len(data)} exceeds limit")
333+
raise SSZValueError(f"{cls.__name__} exceeds limit of {cls.LIMIT}, got {len(data)}")
331334
return cls(data=data)
332335

333336
def __bytes__(self) -> bytes:

0 commit comments

Comments
 (0)