diff --git a/CLAUDE.md b/CLAUDE.md index 5e943c53..ca5bb736 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,11 +28,11 @@ uv run fill --fork=devnet --clean -n auto # Generate test vectors ### Code Quality ```bash -uv run ruff format src tests packages # Format code -uv run ruff check --fix src tests packages # Lint and fix -uvx tox -e typecheck # Type check -uvx tox -e all-checks # All quality checks -uvx tox # Everything (checks + tests + docs) +uv run ruff format # Format code +uv run ruff check --fix # Lint and fix +uvx tox -e typecheck # Type check +uvx tox -e all-checks # All quality checks +uvx tox # Everything (checks + tests + docs) ``` ### Common Tasks diff --git a/README.md b/README.md index 1bac1330..f716d447 100644 --- a/README.md +++ b/README.md @@ -97,16 +97,16 @@ uv run fill --clean --fork=devnet ```bash # Check code style and errors -uv run ruff check src tests packages +uv run ruff check # Auto-fix issues -uv run ruff check --fix src tests packages +uv run ruff check --fix # Format code -uv run ruff format src tests packages +uv run ruff format # Type checking -uv run mypy src tests packages +uv run ty check ``` ### Using Tox for Comprehensive Checks @@ -179,24 +179,24 @@ def test_withdrawal_amount_above_uint64_max(): - **pytest**: Testing framework - just name test files `test_*.py` and functions `test_*` - **uv**: Fast Python package manager - like npm/yarn but for Python - **ruff**: Linter and formatter -- **mypy**: Type checker that works with Pydantic models +- **ty**: Type checker - **tox**: Automation tool for running tests across multiple environments (used via `uvx`) - **mkdocs**: Documentation generator - write docs in Markdown, serve them locally ## Common Commands Reference -| Task | Command | -|-----------------------------------------------|----------------------------------------------| -| Install and sync project and dev dependencies | `uv sync` | -| Run tests | `uv run pytest ...` | -| Format code | `uv run ruff format src tests packages` | -| Lint code | `uv run ruff check src tests packages` | -| Fix lint errors | `uv run ruff check --fix src tests packages` | -| Type check | `uv run mypy src tests packages` | -| Build docs | `uv run mkdocs build` | -| Serve docs | `uv run mkdocs serve` | -| Run everything (checks + tests + docs) | `uvx tox` | -| Run all quality checks (no tests/docs) | `uvx tox -e all-checks` | +| Task | Command | +|-----------------------------------------------|---------------------------| +| Install and sync project and dev dependencies | `uv sync` | +| Run tests | `uv run pytest ...` | +| Format code | `uv run ruff format` | +| Lint code | `uv run ruff check` | +| Fix lint errors | `uv run ruff check --fix` | +| Type check | `uv run ty check` | +| Build docs | `uv run mkdocs build` | +| Serve docs | `uv run mkdocs serve` | +| Run everything (checks + tests + docs) | `uvx tox` | +| Run all quality checks (no tests/docs) | `uvx tox -e all-checks` | ## Contributing diff --git a/packages/testing/src/framework/forks/base.py b/packages/testing/src/framework/forks/base.py index 113b1c6c..4c10792a 100644 --- a/packages/testing/src/framework/forks/base.py +++ b/packages/testing/src/framework/forks/base.py @@ -97,9 +97,9 @@ def __init_subclass__( cls._children = set() # Track parent-child relationships - base_class = cls.__bases__[0] - if base_class != BaseFork and hasattr(base_class, "_children"): - base_class._children.add(cls) + for base in cls.__bases__: + if base is not BaseFork and issubclass(base, BaseFork): + base._children.add(cls) @classmethod @abstractmethod diff --git a/pyproject.toml b/pyproject.toml index 54cf9f26..ec08c110 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,16 +66,11 @@ known-first-party = ["lean_spec"] [tool.ruff.lint.per-file-ignores] "tests/**" = ["D", "F401", "F403"] -[tool.mypy] -python_version = "3.12" -plugins = ["pydantic.mypy"] -strict = true -warn_return_any = true -warn_unused_configs = true -warn_unused_ignores = true -no_implicit_reexport = true -namespace_packages = false -explicit_package_bases = false +[tool.ty.environment] +python-version = "3.12" + +[tool.ty.terminal] +error-on-warning = true [tool.pytest.ini_options] minversion = "8.3.3" @@ -125,7 +120,7 @@ test = [ "lean-ethereum-testing", ] lint = [ - "mypy>=1.17.0,<2", + "ty>=0.0.1a34", "ruff>=0.13.2,<1", "codespell>=2.4.1,<3", ] diff --git a/src/lean_spec/subspecs/containers/attestation/types.py b/src/lean_spec/subspecs/containers/attestation/types.py index 9dec9bae..8304db5f 100644 --- a/src/lean_spec/subspecs/containers/attestation/types.py +++ b/src/lean_spec/subspecs/containers/attestation/types.py @@ -59,7 +59,7 @@ def to_validator_indices(self) -> list[Uint64]: return indices -class NaiveAggregatedSignature(SSZList): +class NaiveAggregatedSignature(SSZList[Signature]): """Naive list of validator signatures used for aggregation placeholders.""" ELEMENT_TYPE = Signature diff --git a/src/lean_spec/subspecs/containers/block/types.py b/src/lean_spec/subspecs/containers/block/types.py index 6135e770..d939f562 100644 --- a/src/lean_spec/subspecs/containers/block/types.py +++ b/src/lean_spec/subspecs/containers/block/types.py @@ -6,18 +6,12 @@ from ..attestation import AggregatedAttestation, AttestationData, NaiveAggregatedSignature -class AggregatedAttestations(SSZList): +class AggregatedAttestations(SSZList[AggregatedAttestation]): """List of aggregated attestations included in a block.""" ELEMENT_TYPE = AggregatedAttestation LIMIT = int(VALIDATOR_REGISTRY_LIMIT) - def __getitem__(self, index: int) -> AggregatedAttestation: - """Access an aggregated attestation by index with proper typing.""" - item = self.data[index] - assert isinstance(item, AggregatedAttestation) - return item - def has_duplicate_data(self) -> bool: """Check if any two attestations share the same AttestationData.""" seen: set[AttestationData] = set() @@ -28,7 +22,7 @@ def has_duplicate_data(self) -> bool: return False -class AttestationSignatures(SSZList): +class AttestationSignatures(SSZList[NaiveAggregatedSignature]): """List of per-attestation naive signature lists aligned with block body attestations.""" ELEMENT_TYPE = NaiveAggregatedSignature diff --git a/src/lean_spec/subspecs/containers/state/state.py b/src/lean_spec/subspecs/containers/state/state.py index e1654a59..28f0f13c 100644 --- a/src/lean_spec/subspecs/containers/state/state.py +++ b/src/lean_spec/subspecs/containers/state/state.py @@ -333,8 +333,8 @@ def process_block_header(self, block: Block) -> "State": update={ "latest_justified": new_latest_justified, "latest_finalized": new_latest_finalized, - "historical_block_hashes": HistoricalBlockHashes(data=new_historical_hashes_data), - "justified_slots": JustifiedSlots(data=new_justified_slots_data), + "historical_block_hashes": new_historical_hashes_data, + "justified_slots": new_justified_slots_data, "latest_block_header": new_header, } ) @@ -570,7 +570,7 @@ def process_attestations( "justifications_validators": JustificationValidators( data=[vote for root in sorted_roots for vote in justifications[root]] ), - "justified_slots": JustifiedSlots(data=justified_slots), + "justified_slots": justified_slots, "latest_justified": latest_justified, "latest_finalized": latest_finalized, } diff --git a/src/lean_spec/subspecs/containers/state/types.py b/src/lean_spec/subspecs/containers/state/types.py index af2f28c0..a4395cad 100644 --- a/src/lean_spec/subspecs/containers/state/types.py +++ b/src/lean_spec/subspecs/containers/state/types.py @@ -7,14 +7,14 @@ from ..validator import Validator -class HistoricalBlockHashes(SSZList): +class HistoricalBlockHashes(SSZList[Bytes32]): """List of historical block root hashes up to historical_roots_limit.""" ELEMENT_TYPE = Bytes32 LIMIT = int(DEVNET_CONFIG.historical_roots_limit) -class JustificationRoots(SSZList): +class JustificationRoots(SSZList[Bytes32]): """List of justified block roots up to historical_roots_limit.""" ELEMENT_TYPE = Bytes32 @@ -33,14 +33,8 @@ class JustificationValidators(BaseBitlist): LIMIT = int(DEVNET_CONFIG.historical_roots_limit) * int(DEVNET_CONFIG.validator_registry_limit) -class Validators(SSZList): +class Validators(SSZList[Validator]): """Validator registry tracked in the state.""" ELEMENT_TYPE = Validator LIMIT = int(DEVNET_CONFIG.validator_registry_limit) - - def __getitem__(self, index: int) -> Validator: - """Access a validator by index with proper typing.""" - item = self.data[index] - assert isinstance(item, Validator) - return item diff --git a/src/lean_spec/subspecs/networking/config.py b/src/lean_spec/subspecs/networking/config.py index f81db33d..d2011e4f 100644 --- a/src/lean_spec/subspecs/networking/config.py +++ b/src/lean_spec/subspecs/networking/config.py @@ -7,8 +7,8 @@ MAX_REQUEST_BLOCKS: Final = 2**10 """Maximum number of blocks in a single request.""" -MESSAGE_DOMAIN_INVALID_SNAPPY: Final = DomainType(b"\x00\x00\x00\x00") +MESSAGE_DOMAIN_INVALID_SNAPPY: Final[DomainType] = b"\x00\x00\x00\x00" """4-byte domain for gossip message-id isolation of invalid snappy messages.""" -MESSAGE_DOMAIN_VALID_SNAPPY: Final = DomainType(b"\x01\x00\x00\x00") +MESSAGE_DOMAIN_VALID_SNAPPY: Final[DomainType] = b"\x01\x00\x00\x00" """4-byte domain for gossip message-id isolation of valid snappy messages.""" diff --git a/src/lean_spec/subspecs/networking/gossipsub/message.py b/src/lean_spec/subspecs/networking/gossipsub/message.py index 5ee0c735..fde3fec1 100644 --- a/src/lean_spec/subspecs/networking/gossipsub/message.py +++ b/src/lean_spec/subspecs/networking/gossipsub/message.py @@ -83,8 +83,8 @@ def id(self) -> MessageId: self.raw_data, ) - # Compute the raw ID bytes and cast to our strict type before caching - self._id = MessageId(self._compute_raw_id(domain, data_for_hash)) + # Compute the raw ID bytes and assign with proper type annotation + self._id: MessageId = self._compute_raw_id(domain, data_for_hash) return self._id def _compute_raw_id(self, domain: bytes, message_data: bytes) -> bytes: diff --git a/src/lean_spec/subspecs/xmss/message_hash.py b/src/lean_spec/subspecs/xmss/message_hash.py index e8d4fcaa..2a26ae2e 100644 --- a/src/lean_spec/subspecs/xmss/message_hash.py +++ b/src/lean_spec/subspecs/xmss/message_hash.py @@ -29,7 +29,7 @@ from __future__ import annotations -from typing import List, cast +from typing import List from pydantic import model_validator @@ -191,11 +191,7 @@ def apply( # The input is: rho || P || epoch || message || iteration. combined_input = ( - cast(List[Fp], list(rho.data)) - + cast(List[Fp], list(parameter.data)) - + epoch_fe - + message_fe - + iteration_separator + list(rho.data) + list(parameter.data) + epoch_fe + message_fe + iteration_separator ) # Hash the combined input using Poseidon2 compression mode. diff --git a/src/lean_spec/subspecs/xmss/subtree.py b/src/lean_spec/subspecs/xmss/subtree.py index b0de2712..34c3650a 100644 --- a/src/lean_spec/subspecs/xmss/subtree.py +++ b/src/lean_spec/subspecs/xmss/subtree.py @@ -166,7 +166,7 @@ def new( parents = [ hasher.apply( parameter, - TreeTweak(level=level + 1, index=int(parent_start) + i), + TreeTweak(level=level + 1, index=Uint64(int(parent_start) + i)), [current.nodes[2 * i], current.nodes[2 * i + 1]], ) for i in range(len(current.nodes) // 2) @@ -394,7 +394,7 @@ def from_prf_key( chain_ends.append(end_digest) # Hash the chain ends to get the leaf for this epoch. - leaf_tweak = TreeTweak(level=0, index=epoch) + leaf_tweak = TreeTweak(level=0, index=Uint64(epoch)) leaf_hash = hasher.apply(parameter, leaf_tweak, chain_ends) leaf_hashes.append(leaf_hash) @@ -537,7 +537,7 @@ def combined_path( # Concatenate: bottom path + top path. bottom_path = bottom_tree.path(position) top_path = top_tree.path(position // leafs_per_tree) - combined = bottom_path.siblings.data + top_path.siblings.data + combined = tuple(bottom_path.siblings.data) + tuple(top_path.siblings.data) return HashTreeOpening(siblings=HashDigestList(data=combined)) @@ -600,7 +600,7 @@ def verify_path( # Start: hash leaf parts to get leaf node. current = hasher.apply( parameter, - TreeTweak(level=0, index=int(position)), + TreeTweak(level=0, index=Uint64(position)), leaf_parts, ) pos = int(position) @@ -616,7 +616,7 @@ def verify_path( pos //= 2 # Parent position. current = hasher.apply( parameter, - TreeTweak(level=level + 1, index=pos), + TreeTweak(level=level + 1, index=Uint64(pos)), [left, right], ) diff --git a/src/lean_spec/subspecs/xmss/types.py b/src/lean_spec/subspecs/xmss/types.py index 1f4a9f2c..47b9384f 100644 --- a/src/lean_spec/subspecs/xmss/types.py +++ b/src/lean_spec/subspecs/xmss/types.py @@ -1,7 +1,5 @@ """Base types for the XMSS signature scheme.""" -from typing import List - from lean_spec.subspecs.koalabear import Fp from ...types import Uint64 @@ -49,7 +47,7 @@ class PRFKey(BaseBytes): """ -class HashDigestVector(SSZVector): +class HashDigestVector(SSZVector[Fp]): """ A single hash digest represented as a fixed-size vector of field elements. @@ -63,13 +61,8 @@ class HashDigestVector(SSZVector): ELEMENT_TYPE = Fp LENGTH = HASH_DIGEST_LENGTH - @property - def elements(self) -> List[Fp]: - """Return the field elements as a typed list.""" - return list(self.data) # type: ignore[arg-type] - -class HashDigestList(SSZList): +class HashDigestList(SSZList[HashDigestVector]): """ Variable-length list of hash digests. @@ -81,12 +74,8 @@ class HashDigestList(SSZList): ELEMENT_TYPE = HashDigestVector LIMIT = NODE_LIST_LIMIT - def __getitem__(self, index: int) -> HashDigestVector: - """Access a hash digest by index with proper typing.""" - return self.data[index] # type: ignore[return-value] - -class Parameter(SSZVector): +class Parameter(SSZVector[Fp]): """ The public parameter P. @@ -100,13 +89,8 @@ class Parameter(SSZVector): ELEMENT_TYPE = Fp LENGTH = PROD_CONFIG.PARAMETER_LEN - @property - def elements(self) -> List[Fp]: - """Return the field elements as a typed list.""" - return list(self.data) # type: ignore[arg-type] - -class Randomness(SSZVector): +class Randomness(SSZVector[Fp]): """ The randomness `rho` (ρ) used during signing. @@ -164,7 +148,7 @@ class HashTreeLayer(Container): """ -class HashTreeLayers(SSZList): +class HashTreeLayers(SSZList[HashTreeLayer]): """ Variable-length list of Merkle tree layers. @@ -180,7 +164,3 @@ class HashTreeLayers(SSZList): ELEMENT_TYPE = HashTreeLayer LIMIT = LAYERS_LIMIT - - def __getitem__(self, index: int) -> HashTreeLayer: - """Access a layer by index with proper typing.""" - return self.data[index] # type: ignore[return-value] diff --git a/src/lean_spec/types/bitfields.py b/src/lean_spec/types/bitfields.py index 9b5bba08..f14021aa 100644 --- a/src/lean_spec/types/bitfields.py +++ b/src/lean_spec/types/bitfields.py @@ -21,7 +21,7 @@ IO, Any, ClassVar, - Tuple, + Sequence, overload, ) @@ -42,12 +42,17 @@ class BaseBitvector(SSZModel): LENGTH: ClassVar[int] """Number of bits in the vector.""" - data: Tuple[Boolean, ...] = Field(default_factory=tuple) - """The immutable bit data stored as a tuple.""" + data: Sequence[Boolean] = Field(default_factory=tuple) + """ + The immutable bit data stored as a sequence of Booleans. + + Accepts lists, tuples, or iterables of bool-like values on input; + stored as a tuple of Boolean after validation. + """ @field_validator("data", mode="before") @classmethod - def _validate_vector_data(cls, v: Any) -> Tuple[Boolean, ...]: + def _coerce_and_validate(cls, v: Any) -> tuple[Boolean, ...]: """Validate and convert input data to typed tuple of Booleans.""" if not hasattr(cls, "LENGTH"): raise TypeError(f"{cls.__name__} must define LENGTH") @@ -56,11 +61,9 @@ def _validate_vector_data(cls, v: Any) -> Tuple[Boolean, ...]: v = tuple(v) if len(v) != cls.LENGTH: - raise ValueError( - f"{cls.__name__} requires exactly {cls.LENGTH} bits, but {len(v)} were provided." - ) + raise ValueError(f"{cls.__name__} requires exactly {cls.LENGTH} bits, got {len(v)}") - return tuple(Boolean(item) for item in v) + return tuple(Boolean(bit) for bit in v) @classmethod def is_fixed_size(cls) -> bool: @@ -81,10 +84,9 @@ def serialize(self, stream: IO[bytes]) -> int: @classmethod def deserialize(cls, stream: IO[bytes], scope: int) -> Self: """Read SSZ bytes from a stream and return an instance.""" - if scope != cls.get_byte_length(): - raise ValueError( - f"Invalid scope for {cls.__name__}: expected {cls.get_byte_length()}, got {scope}" - ) + expected_len = cls.get_byte_length() + if scope != expected_len: + raise ValueError(f"{cls.__name__}: expected {expected_len} bytes, got {scope}") data = stream.read(scope) if len(data) != scope: raise IOError(f"Expected {scope} bytes, got {len(data)}") @@ -111,9 +113,9 @@ def decode_bytes(cls, data: bytes) -> Self: Expects exactly ceil(LENGTH / 8) bytes. No delimiter bit for Bitvector. """ - expected_len = cls.get_byte_length() - if len(data) != expected_len: - raise ValueError(f"{cls.__name__} expected {expected_len} bytes, got {len(data)}") + expected = cls.get_byte_length() + if len(data) != expected: + raise ValueError(f"{cls.__name__}: expected {expected} bytes, got {len(data)}") bits = tuple(Boolean((data[i // 8] >> (i % 8)) & 1) for i in range(cls.LENGTH)) return cls(data=bits) @@ -129,12 +131,17 @@ class BaseBitlist(SSZModel): LIMIT: ClassVar[int] """Maximum number of bits allowed.""" - data: Tuple[Boolean, ...] = Field(default_factory=tuple) - """The immutable bit data stored as a tuple.""" + data: Sequence[Boolean] = Field(default_factory=tuple) + """ + The immutable bit data stored as a sequence of Booleans. + + Accepts lists, tuples, or iterables of bool-like values on input; + stored as a tuple of Boolean after validation. + """ @field_validator("data", mode="before") @classmethod - def _validate_list_data(cls, v: Any) -> Tuple[Boolean, ...]: + def _coerce_and_validate(cls, v: Any) -> tuple[Boolean, ...]: """Validate and convert input to a tuple of Boolean elements.""" if not hasattr(cls, "LIMIT"): raise TypeError(f"{cls.__name__} must define LIMIT") @@ -149,14 +156,9 @@ def _validate_list_data(cls, v: Any) -> Tuple[Boolean, ...]: # Check limit if len(elements) > cls.LIMIT: - raise ValueError( - f"{cls.__name__} cannot contain more than {cls.LIMIT} bits, got {len(elements)}" - ) + raise ValueError(f"{cls.__name__} cannot exceed {cls.LIMIT} bits, got {len(elements)}") - try: - return tuple(Boolean(element) for element in elements) - except Exception as e: - raise ValueError(f"Cannot convert elements to Boolean: {e}") from e + return tuple(Boolean(bit) for bit in elements) @overload def __getitem__(self, key: int) -> Boolean: ... @@ -178,10 +180,12 @@ def __setitem__(self, key: int, value: bool | Boolean) -> None: def __add__(self, other: Any) -> Self: """Concatenate this bitlist with another sequence.""" + # Cast to tuple for concatenation since Sequence doesn't support + + self_data = tuple(self.data) if isinstance(other, BaseBitlist): - new_data = self.data + other.data + new_data = self_data + tuple(other.data) elif isinstance(other, (list, tuple)): - new_data = self.data + tuple(Boolean(b) for b in other) + new_data = self_data + tuple(Boolean(b) for b in other) else: return NotImplemented return type(self)(data=new_data) @@ -250,7 +254,7 @@ def decode_bytes(cls, data: bytes) -> Self: the last data bit. All bits after the delimiter are assumed to be 0. """ if len(data) == 0: - raise ValueError("Cannot decode empty data to Bitlist") + raise ValueError("Cannot decode empty bytes to Bitlist") # Find the position of the delimiter bit (rightmost 1). delimiter_pos = None @@ -266,11 +270,9 @@ def decode_bytes(cls, data: bytes) -> Self: raise ValueError("No delimiter bit found in Bitlist data") # Extract data bits (everything before the delimiter). - num_data_bits = delimiter_pos - if num_data_bits > cls.LIMIT: - raise ValueError( - f"{cls.__name__} decoded length {num_data_bits} exceeds limit {cls.LIMIT}" - ) + num_bits = delimiter_pos + if num_bits > cls.LIMIT: + raise ValueError(f"{cls.__name__} decoded length {num_bits} exceeds limit {cls.LIMIT}") - bits = [bool((data[i // 8] >> (i % 8)) & 1) for i in range(num_data_bits)] + bits = tuple(Boolean((data[i // 8] >> (i % 8)) & 1) for i in range(num_bits)) return cls(data=bits) diff --git a/src/lean_spec/types/collections.py b/src/lean_spec/types/collections.py index 95a6d5d3..9c5924d1 100644 --- a/src/lean_spec/types/collections.py +++ b/src/lean_spec/types/collections.py @@ -7,9 +7,13 @@ IO, Any, ClassVar, - Tuple, + Generic, + Iterator, + Sequence, Type, + TypeVar, cast, + overload, ) from pydantic import Field, field_serializer, field_validator @@ -21,53 +25,83 @@ from .ssz_base import SSZModel, SSZType from .uint import Uint32 +T = TypeVar("T", bound=SSZType) +""" +Generic type parameter for SSZ collection elements. -class SSZVector(SSZModel): +This TypeVar enables proper static typing for collection access: + +- Bound to `SSZType` to ensure elements are valid SSZ types +- Used with `Generic[T]` to parameterize `SSZVector` and `SSZList` +- Allows type checkers to infer correct return types for `__getitem__` + +Example: + class Uint64Vector4(SSZVector[Uint64]): + ELEMENT_TYPE = Uint64 + LENGTH = 4 + + vec = Uint64Vector4(data=[...]) + x = vec[0] # Type checker infers `x: Uint64` +""" + + +class SSZVector(SSZModel, Generic[T]): """ - Base class for SSZ Vector types: fixed-length, immutable sequences. + Fixed-length, immutable SSZ sequence. + + An SSZ Vector contains exactly `LENGTH` elements of type `ELEMENT_TYPE`. + The length is fixed at the type level and cannot change at runtime. - To create a specific vector type, inherit from this class and set: - - ELEMENT_TYPE: The SSZ type of elements - - LENGTH: The exact number of elements + Subclasses must define: + ELEMENT_TYPE: The SSZ type of each element + LENGTH: The exact number of elements Example: - class Uint16Vector2(SSZVector): + class Uint16Vector2(SSZVector[Uint16]): ELEMENT_TYPE = Uint16 LENGTH = 2 + + vec = Uint16Vector2(data=[Uint16(1), Uint16(2)]) + assert len(vec) == 2 + assert vec[0] == Uint16(1) # Properly typed as Uint16 + + SSZ Encoding: + - Fixed-size elements: Serialized back-to-back + - Variable-size elements: Offset table followed by element data """ ELEMENT_TYPE: ClassVar[Type[SSZType]] - """The SSZ type of the elements in the vector.""" + """The SSZ type of elements in this vector.""" LENGTH: ClassVar[int] - """The exact number of elements in the vector.""" + """The exact number of elements (fixed at the type level).""" + + data: Sequence[T] = Field(default_factory=tuple) + """ + The immutable sequence of elements. - data: Tuple[SSZType, ...] = Field(default_factory=tuple) - """The immutable data stored in the vector.""" + Accepts lists or tuples on input; stored as a tuple after validation. + """ @field_serializer("data", when_used="json") - def _serialize_data(self, value: Tuple[SSZType, ...]) -> list[Any]: - """Serialize vector elements to JSON, preserving custom type serialization.""" + def _serialize_data(self, value: Sequence[T]) -> list[Any]: + """Serialize vector elements to JSON.""" from lean_spec.subspecs.koalabear import Fp result: list[Any] = [] for item in value: - # For BaseBytes subclasses, manually add 0x prefix if isinstance(item, BaseBytes): result.append("0x" + item.hex()) - # For Fp field elements, extract the value attribute elif isinstance(item, Fp): result.append(item.value) else: - # For other types (Uint, etc.), convert to int - # BaseUint inherits from int, so this cast is safe result.append(item) return result @field_validator("data", mode="before") @classmethod - def _validate_vector_data(cls, v: Any) -> Tuple[SSZType, ...]: - """Validate and convert input data to typed tuple.""" + def _validate_vector_data(cls, v: Any) -> tuple[SSZType, ...]: + """Validate and convert input to a typed tuple of exactly LENGTH elements.""" if not hasattr(cls, "ELEMENT_TYPE") or not hasattr(cls, "LENGTH"): raise TypeError(f"{cls.__name__} must define ELEMENT_TYPE and LENGTH") @@ -171,42 +205,74 @@ def __len__(self) -> int: """Return the number of elements in the vector.""" return len(self.data) - def __getitem__(self, index: int) -> SSZType: - """Access an element by index.""" + def __iter__(self) -> Iterator[T]: # type: ignore[override] + """Iterate over vector elements.""" + return iter(self.data) + + @overload + def __getitem__(self, index: int) -> T: ... + @overload + def __getitem__(self, index: slice) -> Sequence[T]: ... + + def __getitem__(self, index: int | slice) -> T | Sequence[T]: + """ + Access element(s) by index or slice. + + Returns properly typed results: + + - `vec[0]` returns `T` + - `vec[0:2]` returns `Sequence[T]` + """ return self.data[index] + @property + def elements(self) -> list[T]: + """Return the elements as a typed list.""" + return list(self.data) -class SSZList(SSZModel): + +class SSZList(SSZModel, Generic[T]): """ - Base class for SSZ List types - variable-length homogeneous collections. + Variable-length SSZ sequence with a maximum capacity. - An SSZ List is a sequence that can contain between 0 and LIMIT elements, - where all elements must be of the same SSZ type. + An SSZ List contains between 0 and `LIMIT` elements of type `ELEMENT_TYPE`. + Unlike Vector, the length can vary at runtime. Subclasses must define: - - ELEMENT_TYPE: The SSZ type of elements in the list - - LIMIT: Maximum number of elements allowed + ELEMENT_TYPE: The SSZ type of each element + LIMIT: The maximum number of elements allowed - Example usage: - class Uint64List32(SSZList): + Example: + class Uint64List32(SSZList[Uint64]): ELEMENT_TYPE = Uint64 LIMIT = 32 - my_list = Uint64List32(data=[1, 2, 3]) + my_list = Uint64List32(data=[Uint64(1), Uint64(2)]) + assert len(my_list) == 2 + assert my_list[0] == Uint64(1) # Properly typed as Uint64 + + SSZ Encoding: + - Fixed-size elements: Serialized back-to-back + - Variable-size elements: Offset table followed by element data + - Hash tree root includes the element count (mixed-in) """ ELEMENT_TYPE: ClassVar[Type[SSZType]] """The SSZ type of elements in this list.""" LIMIT: ClassVar[int] - """Maximum number of elements this list can contain.""" + """The maximum number of elements allowed.""" - data: Tuple[SSZType, ...] = Field(default_factory=tuple) - """The elements in this list, stored as an immutable tuple.""" + data: Sequence[T] = Field(default_factory=tuple) + """ + The immutable sequence of elements. + + Accepts lists or tuples on input; stored as a tuple after validation. + """ @field_serializer("data", when_used="json") - def _serialize_data(self, value: Tuple[SSZType, ...]) -> list[Any]: - """Serialize list elements to JSON, preserving custom type serialization.""" + def _serialize_data(self, value: Sequence[T]) -> list[Any]: + """Serialize list elements to JSON.""" from lean_spec.subspecs.koalabear import Fp result: list[Any] = [] @@ -225,7 +291,7 @@ def _serialize_data(self, value: Tuple[SSZType, ...]) -> list[Any]: @field_validator("data", mode="before") @classmethod - def _validate_list_data(cls, v: Any) -> Tuple[SSZType, ...]: + def _validate_list_data(cls, v: Any) -> tuple[SSZType, ...]: """Validate and convert input to a tuple of SSZType elements.""" if not hasattr(cls, "ELEMENT_TYPE") or not hasattr(cls, "LIMIT"): raise TypeError(f"{cls.__name__} must define ELEMENT_TYPE and LIMIT") @@ -264,7 +330,7 @@ def __add__(self, other: Any) -> Self: if isinstance(other, SSZList): new_data = self.data + other.data elif isinstance(other, (list, tuple)): - new_data = self.data + tuple(other) + new_data = tuple(self.data) + tuple(other) else: return NotImplemented return type(self)(data=new_data) @@ -366,6 +432,27 @@ def __len__(self) -> int: """Return the number of elements in the list.""" return len(self.data) - def __getitem__(self, index: int) -> SSZType: - """Access an element by index.""" + def __iter__(self) -> Iterator[T]: # type: ignore[override] + """Iterate over list elements.""" + return iter(self.data) + + @overload + def __getitem__(self, index: int) -> T: ... + @overload + def __getitem__(self, index: slice) -> Sequence[T]: ... + + def __getitem__(self, index: int | slice) -> T | Sequence[T]: + """ + Access element(s) by index or slice. + + Returns properly typed results: + + - `lst[0]` returns `T` + - `lst[0:2]` returns `Sequence[T]` + """ return self.data[index] + + @property + def elements(self) -> list[T]: + """Return the elements as a typed list.""" + return list(self.data) diff --git a/src/lean_spec/types/ssz_base.py b/src/lean_spec/types/ssz_base.py index fdcc2a4e..25ef5b9a 100644 --- a/src/lean_spec/types/ssz_base.py +++ b/src/lean_spec/types/ssz_base.py @@ -4,9 +4,9 @@ import io from abc import ABC, abstractmethod -from typing import IO, Any +from typing import IO, Any, Sequence -from typing_extensions import Iterator, Self +from typing_extensions import Self from .base import StrictBaseModel @@ -106,46 +106,19 @@ class SSZModel(StrictBaseModel, SSZType): Use this for containers and complex types that can benefit from Pydantic. For simple types that need special inheritance (like int), use SSZType directly. - - SSZModel provides natural iteration and indexing for collections with a 'data' field: - - `for item in collection` instead of `for item in collection.data` - - `collection[i]` instead of `collection.data[i]` - - `len(collection)` instead of `len(collection.data)` """ def __len__(self) -> int: """Return the length of the collection's data or number of container fields.""" - if hasattr(self, "data"): - return len(self.data) - # For containers, return number of fields + data: Sequence[Any] | None = getattr(self, "data", None) + if data is not None: + return len(data) return len(type(self).model_fields) - def __iter__(self) -> Iterator[Any]: # type: ignore[override] - """ - Iterate over the collection's data if it's a collection type, - otherwise iterate over container field (name, value) pairs. - - For SSZ collections with 'data' field, this iterates over the data contents. - For container types, this iterates over (field_name, field_value) pairs. - """ - if hasattr(self, "data"): - return iter(self.data) - # For containers, iterate over (field_name, field_value) pairs - return iter((name, getattr(self, name)) for name in type(self).model_fields.keys()) - - def __getitem__(self, key: Any) -> Any: - """Get an item from the collection's data or container field by name.""" - if hasattr(self, "data"): - return self.data[key] - # For containers, allow field access by name - if isinstance(key, str) and key in type(self).model_fields: - return getattr(self, key) - raise KeyError(f"Invalid key '{key}' for {self.__class__.__name__}") - def __repr__(self) -> str: """String representation showing the class name and data.""" - if hasattr(self, "data"): - return f"{self.__class__.__name__}(data={list(self.data)!r})" - # For containers, show field names and values + data: Sequence[Any] | None = getattr(self, "data", None) + if data is not None: + return f"{self.__class__.__name__}(data={list(data)!r})" field_strs = [f"{name}={getattr(self, name)!r}" for name in type(self).model_fields.keys()] return f"{self.__class__.__name__}({' '.join(field_strs)})" diff --git a/tests/consensus/devnet/state_transition/test_genesis.py b/tests/consensus/devnet/state_transition/test_genesis.py index 5e761711..4069a690 100644 --- a/tests/consensus/devnet/state_transition/test_genesis.py +++ b/tests/consensus/devnet/state_transition/test_genesis.py @@ -159,7 +159,7 @@ def test_genesis_block_hash_comparison() -> None: # Fill pubkeys with different values (1, 2, 3) pubkeys1 = [Bytes52(bytes([i + 1] * 52)) for i in range(3)] validators1 = Validators( - data=[Validator(pubkey=pubkey, index=i) for i, pubkey in enumerate(pubkeys1)] + data=[Validator(pubkey=pubkey, index=Uint64(i)) for i, pubkey in enumerate(pubkeys1)] ) genesis_state1 = State.generate_genesis( @@ -202,7 +202,7 @@ def test_genesis_block_hash_comparison() -> None: # Fill pubkeys with different values (10, 11, 12) pubkeys2 = [Bytes52(bytes([i + 10] * 52)) for i in range(3)] validators2 = Validators( - data=[Validator(pubkey=pubkey, index=i) for i, pubkey in enumerate(pubkeys2)] + data=[Validator(pubkey=pubkey, index=Uint64(i)) for i, pubkey in enumerate(pubkeys2)] ) genesis_state2 = State.generate_genesis( @@ -227,7 +227,7 @@ def test_genesis_block_hash_comparison() -> None: # Same as pubkeys1 pubkeys3 = [Bytes52(bytes([i + 1] * 52)) for i in range(3)] validators3 = Validators( - data=[Validator(pubkey=pubkey, index=i) for i, pubkey in enumerate(pubkeys3)] + data=[Validator(pubkey=pubkey, index=Uint64(i)) for i, pubkey in enumerate(pubkeys3)] ) genesis_state3 = State.generate_genesis( diff --git a/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py b/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py index c42e5628..d1b37edf 100644 --- a/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py +++ b/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py @@ -10,7 +10,7 @@ ) from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.containers.slot import Slot -from lean_spec.types import Bytes32, Uint64 +from lean_spec.types import Boolean, Bytes32, Uint64 class TestAggregationBits: @@ -18,19 +18,21 @@ class TestAggregationBits: def test_reject_empty_aggregation_bits(self) -> None: """Validate aggregated attestation must include at least one validator.""" - bits = AggregationBits(data=[False, False, False]) + bits = AggregationBits(data=[Boolean(False), Boolean(False), Boolean(False)]) with pytest.raises(AssertionError, match="at least one validator"): bits.to_validator_indices() def test_to_validator_indices_single_bit(self) -> None: """Test conversion with a single bit set.""" - bits = AggregationBits(data=[False, True, False]) + bits = AggregationBits(data=[Boolean(False), Boolean(True), Boolean(False)]) indices = bits.to_validator_indices() assert indices == [Uint64(1)] def test_to_validator_indices_multiple_bits(self) -> None: """Test conversion with multiple bits set.""" - bits = AggregationBits(data=[True, False, True, True, False]) + bits = AggregationBits( + data=[Boolean(True), Boolean(False), Boolean(True), Boolean(True), Boolean(False)] + ) indices = bits.to_validator_indices() assert indices == [Uint64(0), Uint64(2), Uint64(3)] diff --git a/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py b/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py index c651db07..04f37fec 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py +++ b/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py @@ -24,7 +24,7 @@ from lean_spec.subspecs.containers.validator import Validator from lean_spec.subspecs.forkchoice import Store from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.types import Bytes32, Uint64 +from lean_spec.types import Bytes32, Bytes52, Uint64 def test_on_block_processes_multi_validator_aggregations() -> None: @@ -32,7 +32,7 @@ def test_on_block_processes_multi_validator_aggregations() -> None: key_manager = XmssKeyManager(max_slot=Slot(10)) validators = Validators( data=[ - Validator(pubkey=key_manager[Uint64(i)].public.encode_bytes(), index=Uint64(i)) + Validator(pubkey=Bytes52(key_manager[Uint64(i)].public.encode_bytes()), index=Uint64(i)) for i in range(3) ] ) diff --git a/tests/lean_spec/subspecs/ssz/test_block.py b/tests/lean_spec/subspecs/ssz/test_block.py index a7f880db..cc6b8665 100644 --- a/tests/lean_spec/subspecs/ssz/test_block.py +++ b/tests/lean_spec/subspecs/ssz/test_block.py @@ -11,6 +11,7 @@ AttestationSignatures, ) from lean_spec.subspecs.containers.checkpoint import Checkpoint +from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.koalabear import Fp from lean_spec.subspecs.xmss.constants import PROD_CONFIG from lean_spec.subspecs.xmss.containers import Signature @@ -22,7 +23,7 @@ def test_encode_decode_signed_block_with_attestation_roundtrip() -> None: signed_block_with_attestation = SignedBlockWithAttestation( message=BlockWithAttestation( block=Block( - slot=0, + slot=Slot(0), proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32.zero(), @@ -31,10 +32,10 @@ def test_encode_decode_signed_block_with_attestation_roundtrip() -> None: proposer_attestation=Attestation( validator_id=Uint64(0), data=AttestationData( - slot=0, - head=Checkpoint(root=Bytes32.zero(), slot=0), - target=Checkpoint(root=Bytes32.zero(), slot=0), - source=Checkpoint(root=Bytes32.zero(), slot=0), + slot=Slot(0), + head=Checkpoint(root=Bytes32.zero(), slot=Slot(0)), + target=Checkpoint(root=Bytes32.zero(), slot=Slot(0)), + source=Checkpoint(root=Bytes32.zero(), slot=Slot(0)), ), ), ), diff --git a/tests/lean_spec/subspecs/ssz/test_hash.py b/tests/lean_spec/subspecs/ssz/test_hash.py index 7a8043f8..692ded7c 100644 --- a/tests/lean_spec/subspecs/ssz/test_hash.py +++ b/tests/lean_spec/subspecs/ssz/test_hash.py @@ -371,11 +371,12 @@ def test_hash_tree_root_bitvector( Its hash tree root is the Merkle root of these bytes, treated like a `ByteVector`. """ - # Create the Bitvector instance. + # Create the Bitvector instance with Boolean values. class TestBitvector(BaseBitvector): LENGTH = len(bits) - bv = TestBitvector(data=bits) + bool_bits = tuple(Boolean(b) for b in bits) + bv = TestBitvector(data=bool_bits) # Sanity check: ensure the serialization is correct. assert bv.encode_bytes().hex() == expect_serial_hex # Verify the hash tree root. @@ -407,11 +408,12 @@ def test_hash_tree_root_bitlist( part, and then mixes in the number of bits. """ - # Create the Bitlist instance. + # Create the Bitlist instance with Boolean values. class TestBitlist(BaseBitlist): LIMIT = limit - bl = TestBitlist(data=bits) + bool_bits = tuple(Boolean(b) for b in bits) + bl = TestBitlist(data=bool_bits) # Sanity check the SSZ serialization. assert bl.encode_bytes().hex() == expect_serial_hex # Verify the hash tree root. @@ -427,7 +429,7 @@ def test_hash_tree_root_bitvector_512_all_ones() -> None: class Bitvector512(BaseBitvector): LENGTH = 512 - bv = Bitvector512(data=(1,) * 512) + bv = Bitvector512(data=tuple(Boolean(1) for _ in range(512))) # Both chunks will be all `0xff` bytes. left = "ff" * 32 right = "ff" * 32 @@ -446,7 +448,7 @@ def test_hash_tree_root_bitlist_512_all_ones() -> None: class Bitlist512(BaseBitlist): LIMIT = 512 - bl = Bitlist512(data=(1,) * 512) + bl = Bitlist512(data=tuple(Boolean(1) for _ in range(512))) # The data part is 512 bits (64 bytes), which forms two full chunks of `0xff`. # The Merkle root of the data is the hash of these two chunks. base = h("ff" * 32, "ff" * 32) @@ -488,7 +490,7 @@ def test_hash_tree_root_vector_uint16_2() -> None: simply the serialized bytes, right-padded to 32 bytes. """ # SSZVector of two Uint16 values - using our new explicit class definition - v = Uint16Vector2(data=[0x4567, 0x0123]) + v = Uint16Vector2(data=[Uint16(0x4567), Uint16(0x0123)]) # Serialization (little-endian): 0x4567 -> "6745", 0x0123 -> "2301". # Concatenated: "67452301". This is 4 bytes, which fits in one chunk. expected = chunk("67452301") @@ -501,7 +503,7 @@ def test_hash_tree_root_list_uint16() -> None: Tests the hash tree root of a `List` of basic types. """ # Create a list of three Uint16 elements. - test_list = Uint16List32(data=(0xAABB, 0xC0AD, 0xEEFF)) + test_list = Uint16List32(data=(Uint16(0xAABB), Uint16(0xC0AD), Uint16(0xEEFF))) # The serialized data is "bbaaadc0ffee" (3 * 2 = 6 bytes). # The capacity is 32 * 2 = 64 bytes = 2 chunks. # The data is packed into chunks and Merkleized. Here, it's one data chunk and one zero chunk. @@ -517,7 +519,7 @@ def test_hash_tree_root_list_uint32_large_limit() -> None: Tests a `List` of basic types with a large capacity, requiring padding. """ # List of three Uint32s, capacity 128 elements. - test_list = Uint32List128(data=(0xAABB, 0xC0AD, 0xEEFF)) + test_list = Uint32List128(data=(Uint32(0xAABB), Uint32(0xC0AD), Uint32(0xEEFF))) # Capacity: 128 * 4 = 512 bytes = 16 chunks. Tree depth is 4 (2^4=16). # Serialized data: "bbaa0000adc00000ffee0000" (3 * 4 = 12 bytes), fits in one chunk. # This single chunk must be Merkleized with zero hashes up to depth 4. @@ -533,7 +535,7 @@ def test_hash_tree_root_list_uint256() -> None: Tests a `List` where each element is itself a 32-byte chunk. """ # Create a list of three Uint256 elements. - test_list = Uint256List32(data=(0xAABB, 0xC0AD, 0xEEFF)) + test_list = Uint256List32(data=(Uint256(0xAABB), Uint256(0xC0AD), Uint256(0xEEFF))) # Each Uint256 is a 32-byte leaf. We have 3 leaves. a = chunk("bbaa") # 0xAABB b = chunk("adc0") # 0xC0AD @@ -688,7 +690,11 @@ def test_hash_tree_root_container_var_some() -> None: Tests a container with a populated variable-size list. """ # Create a container with a list containing three elements. - v = Var(A=Uint16(0xABCD), B=Uint16List1024(data=(1, 2, 3)), C=Uint8(0xFF)) + v = Var( + A=Uint16(0xABCD), + B=Uint16List1024(data=(Uint16(1), Uint16(2), Uint16(3))), + C=Uint8(0xFF), + ) # Calculate the root of list B. # Data "010002000300" is padded to capacity (64 chunks, depth 6). base = merge(chunk("010002000300"), ZERO_HASHES[0:6]) @@ -706,10 +712,14 @@ def test_hash_tree_root_container_complex() -> None: # Instantiate the deeply nested container. v = Complex( A=Uint16(0xAABB), - B=Uint16List128(data=(0x1122, 0x3344)), + B=Uint16List128(data=(Uint16(0x1122), Uint16(0x3344))), C=Uint8(0xFF), D=ByteList256(data=b"foobar"), - E=Var(A=Uint16(0xABCD), B=Uint16List1024(data=(1, 2, 3)), C=Uint8(0xFF)), + E=Var( + A=Uint16(0xABCD), + B=Uint16List1024(data=(Uint16(1), Uint16(2), Uint16(3))), + C=Uint8(0xFF), + ), F=FixedVector4( data=[ Fixed(A=Uint8(0xCC), B=Uint64(0x4242424242424242), C=Uint32(0x13371337)), @@ -720,8 +730,16 @@ def test_hash_tree_root_container_complex() -> None: ), G=VarVector2( data=[ - Var(A=Uint16(0xDEAD), B=Uint16List1024(data=(1, 2, 3)), C=Uint8(0x11)), - Var(A=Uint16(0xBEEF), B=Uint16List1024(data=(4, 5, 6)), C=Uint8(0x22)), + Var( + A=Uint16(0xDEAD), + B=Uint16List1024(data=(Uint16(1), Uint16(2), Uint16(3))), + C=Uint8(0x11), + ), + Var( + A=Uint16(0xBEEF), + B=Uint16List1024(data=(Uint16(4), Uint16(5), Uint16(6))), + C=Uint8(0x22), + ), ] ), ) diff --git a/tests/lean_spec/subspecs/ssz/test_signed_attestation.py b/tests/lean_spec/subspecs/ssz/test_signed_attestation.py index f30e6fc0..107c26f4 100644 --- a/tests/lean_spec/subspecs/ssz/test_signed_attestation.py +++ b/tests/lean_spec/subspecs/ssz/test_signed_attestation.py @@ -1,4 +1,5 @@ from lean_spec.subspecs.containers import AttestationData, Checkpoint, SignedAttestation +from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.koalabear import Fp from lean_spec.subspecs.xmss.constants import PROD_CONFIG from lean_spec.subspecs.xmss.containers import Signature @@ -8,10 +9,10 @@ def test_encode_decode_signed_attestation_roundtrip() -> None: attestation_data = AttestationData( - slot=0, - head=Checkpoint(root=Bytes32.zero(), slot=0), - target=Checkpoint(root=Bytes32.zero(), slot=0), - source=Checkpoint(root=Bytes32.zero(), slot=0), + slot=Slot(0), + head=Checkpoint(root=Bytes32.zero(), slot=Slot(0)), + target=Checkpoint(root=Bytes32.zero(), slot=Slot(0)), + source=Checkpoint(root=Bytes32.zero(), slot=Slot(0)), ) signed_attestation = SignedAttestation( validator_id=Uint64(0), diff --git a/tests/lean_spec/subspecs/ssz/test_state.py b/tests/lean_spec/subspecs/ssz/test_state.py index 2f101e86..b89b1aed 100644 --- a/tests/lean_spec/subspecs/ssz/test_state.py +++ b/tests/lean_spec/subspecs/ssz/test_state.py @@ -6,6 +6,7 @@ ) from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.containers.config import Config +from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state.types import ( HistoricalBlockHashes, JustificationRoots, @@ -18,16 +19,16 @@ def test_encode_decode_state_roundtrip() -> None: block_header = BlockHeader( - slot=0, + slot=Slot(0), proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32.zero(), body_root=Bytes32.zero(), ) - temp_finalized = Checkpoint(root=Bytes32.zero(), slot=0) + temp_finalized = Checkpoint(root=Bytes32.zero(), slot=Slot(0)) state = State( config=Config(genesis_time=Uint64(1000)), - slot=0, + slot=Slot(0), latest_block_header=block_header, latest_justified=temp_finalized, latest_finalized=temp_finalized, diff --git a/tests/lean_spec/subspecs/xmss/test_merkle_tree.py b/tests/lean_spec/subspecs/xmss/test_merkle_tree.py index b1fa6934..ffd57e48 100644 --- a/tests/lean_spec/subspecs/xmss/test_merkle_tree.py +++ b/tests/lean_spec/subspecs/xmss/test_merkle_tree.py @@ -49,7 +49,7 @@ def _run_commit_open_verify_roundtrip( leaf_hashes: list[HashDigestVector] = [ hasher.apply( parameter, - TreeTweak(level=0, index=start_index + i), + TreeTweak(level=0, index=Uint64(start_index + i)), leaf_parts, ) for i, leaf_parts in enumerate(leaves) diff --git a/tests/lean_spec/subspecs/xmss/test_strict_types.py b/tests/lean_spec/subspecs/xmss/test_strict_types.py index cbd5f7cc..1bec45d0 100644 --- a/tests/lean_spec/subspecs/xmss/test_strict_types.py +++ b/tests/lean_spec/subspecs/xmss/test_strict_types.py @@ -45,7 +45,7 @@ class RandomClass: pass with pytest.raises((TypeError, ValidationError)): - Prf(config=RandomClass()) + Prf(config=RandomClass()) # type: ignore[arg-type] def test_prf_frozen(self) -> None: """Prf is immutable (frozen).""" @@ -80,7 +80,7 @@ class RandomClass: pass with pytest.raises((TypeError, ValidationError)): - Rand(config=RandomClass()) + Rand(config=RandomClass()) # type: ignore[arg-type] def test_rand_frozen(self) -> None: """Rand is immutable (frozen).""" @@ -127,7 +127,7 @@ class RandomClass: pass with pytest.raises((TypeError, ValidationError)): - TweakHasher(config=RandomClass(), poseidon=PROD_POSEIDON) + TweakHasher(config=RandomClass(), poseidon=PROD_POSEIDON) # type: ignore[arg-type] def test_tweak_hasher_rejects_wrong_type_poseidon(self) -> None: """TweakHasher rejects completely wrong type for poseidon.""" @@ -136,7 +136,7 @@ class RandomClass: pass with pytest.raises((TypeError, ValidationError)): - TweakHasher(config=PROD_CONFIG, poseidon=RandomClass()) + TweakHasher(config=PROD_CONFIG, poseidon=RandomClass()) # type: ignore[arg-type] def test_tweak_hasher_frozen(self) -> None: """TweakHasher is immutable (frozen).""" @@ -183,7 +183,7 @@ class RandomClass: pass with pytest.raises((TypeError, ValidationError)): - TargetSumEncoder(config=RandomClass(), message_hasher=PROD_MESSAGE_HASHER) + TargetSumEncoder(config=RandomClass(), message_hasher=PROD_MESSAGE_HASHER) # type: ignore[arg-type] def test_encoder_rejects_wrong_type_message_hasher(self) -> None: """TargetSumEncoder rejects completely wrong type for message_hasher.""" @@ -192,7 +192,7 @@ class RandomClass: pass with pytest.raises((TypeError, ValidationError)): - TargetSumEncoder(config=PROD_CONFIG, message_hasher=RandomClass()) + TargetSumEncoder(config=PROD_CONFIG, message_hasher=RandomClass()) # type: ignore[arg-type] def test_encoder_frozen(self) -> None: """TargetSumEncoder is immutable (frozen).""" @@ -313,7 +313,7 @@ def test_scheme_rejects_extra_fields(self) -> None: hasher=PROD_TWEAK_HASHER, encoder=PROD_TARGET_SUM_ENCODER, rand=PROD_RAND, - extra_field="should_fail", + extra_field="should_fail", # type: ignore[unknown-argument] ) @@ -358,7 +358,7 @@ class RandomClass: pass with pytest.raises((TypeError, ValidationError)): - PoseidonXmss(params16=RandomClass(), params24=PROD_POSEIDON.params24) + PoseidonXmss(params16=RandomClass(), params24=PROD_POSEIDON.params24) # type: ignore[arg-type] def test_poseidon_rejects_wrong_type_params24(self) -> None: """PoseidonXmss rejects completely wrong type for params24.""" @@ -367,7 +367,7 @@ class RandomClass: pass with pytest.raises((TypeError, ValidationError)): - PoseidonXmss(params16=PROD_POSEIDON.params16, params24=RandomClass()) + PoseidonXmss(params16=PROD_POSEIDON.params16, params24=RandomClass()) # type: ignore[arg-type] def test_poseidon_frozen(self) -> None: """PoseidonXmss is immutable (frozen).""" @@ -414,7 +414,7 @@ class RandomClass: pass with pytest.raises((TypeError, ValidationError)): - MessageHasher(config=RandomClass(), poseidon=PROD_POSEIDON) + MessageHasher(config=RandomClass(), poseidon=PROD_POSEIDON) # type: ignore[arg-type] def test_message_hasher_rejects_wrong_type_poseidon(self) -> None: """MessageHasher rejects completely wrong type for poseidon.""" @@ -423,7 +423,7 @@ class RandomClass: pass with pytest.raises((TypeError, ValidationError)): - MessageHasher(config=PROD_CONFIG, poseidon=RandomClass()) + MessageHasher(config=PROD_CONFIG, poseidon=RandomClass()) # type: ignore[arg-type] def test_message_hasher_frozen(self) -> None: """MessageHasher is immutable (frozen).""" diff --git a/tests/lean_spec/subspecs/xmss/test_utils.py b/tests/lean_spec/subspecs/xmss/test_utils.py index d09b41e7..b7d9ae77 100644 --- a/tests/lean_spec/subspecs/xmss/test_utils.py +++ b/tests/lean_spec/subspecs/xmss/test_utils.py @@ -1,7 +1,7 @@ """Tests for the utility functions in the XMSS signature scheme.""" import secrets -from typing import List, cast +from typing import List import pytest @@ -11,7 +11,7 @@ from lean_spec.subspecs.xmss.rand import TEST_RAND from lean_spec.subspecs.xmss.subtree import HashSubTree from lean_spec.subspecs.xmss.tweak_hash import TEST_TWEAK_HASHER -from lean_spec.subspecs.xmss.types import HashTreeLayer, Parameter +from lean_spec.subspecs.xmss.types import Parameter from lean_spec.subspecs.xmss.utils import ( expand_activation_time, int_to_base_p, @@ -138,12 +138,12 @@ def test_hash_subtree_from_prf_key() -> None: assert len(bottom_tree.layers) > 0 # Verify the root layer has exactly one node - root_layer = cast(HashTreeLayer, bottom_tree.layers.data[-1]) + root_layer = bottom_tree.layers.data[-1] assert len(root_layer.nodes) == 1 # Verify the leaf layer covers the right range leafs_per_bottom_tree = 1 << (config.LOG_LIFETIME // 2) - leaf_layer = cast(HashTreeLayer, bottom_tree.layers.data[0]) + leaf_layer = bottom_tree.layers.data[0] assert len(leaf_layer.nodes) == leafs_per_bottom_tree @@ -177,10 +177,7 @@ def test_hash_subtree_from_prf_key_deterministic() -> None: ) # Verify the roots are identical - assert ( - cast(HashTreeLayer, tree1.layers.data[-1]).nodes[0] - == cast(HashTreeLayer, tree2.layers.data[-1]).nodes[0] - ) + assert tree1.layers.data[-1].nodes[0] == tree2.layers.data[-1].nodes[0] def test_hash_subtree_from_prf_key_different_indices() -> None: @@ -213,7 +210,4 @@ def test_hash_subtree_from_prf_key_different_indices() -> None: ) # Verify the roots are different - assert ( - cast(HashTreeLayer, tree0.layers.data[-1]).nodes[0] - != cast(HashTreeLayer, tree1.layers.data[-1]).nodes[0] - ) + assert tree0.layers.data[-1].nodes[0] != tree1.layers.data[-1].nodes[0] diff --git a/tests/lean_spec/types/test_bitfields.py b/tests/lean_spec/types/test_bitfields.py index d05678a7..554c6255 100644 --- a/tests/lean_spec/types/test_bitfields.py +++ b/tests/lean_spec/types/test_bitfields.py @@ -4,10 +4,36 @@ from typing import Any import pytest -from pydantic import ValidationError, create_model +from pydantic import BaseModel, ValidationError from typing_extensions import Tuple from lean_spec.types.bitfields import BaseBitlist, BaseBitvector +from lean_spec.types.boolean import Boolean + + +# Define bitfield types at module level for reuse and model classes +class Bitvector4(BaseBitvector): + """A bitvector of exactly 4 bits.""" + + LENGTH = 4 + + +class Bitvector4Model(BaseModel): + """Model for testing Pydantic validation of Bitvector4.""" + + value: Bitvector4 + + +class Bitlist8(BaseBitlist): + """A bitlist with up to 8 bits.""" + + LIMIT = 8 + + +class Bitlist8Model(BaseModel): + """Model for testing Pydantic validation of Bitlist8.""" + + value: Bitlist8 class TestBitvector: @@ -33,59 +59,42 @@ def test_instantiate_raw_type_raises_error(self) -> None: def test_instantiation_success(self) -> None: """Tests successful instantiation with the correct number of valid boolean items.""" - - class Bitvector4(BaseBitvector): - LENGTH = 4 - - instance = Bitvector4(data=[True, False, 1, 0]) + instance = Bitvector4(data=[Boolean(True), Boolean(False), Boolean(1), Boolean(0)]) assert len(instance) == 4 - assert instance == Bitvector4(data=[True, False, True, False]) + assert instance == Bitvector4( + data=[Boolean(True), Boolean(False), Boolean(True), Boolean(False)] + ) @pytest.mark.parametrize( "values", [ - [True, False, True], # Too few - [True, False, True, False, True], # Too many + [Boolean(True), Boolean(False), Boolean(True)], # Too few + [Boolean(True), Boolean(False), Boolean(True), Boolean(False), Boolean(True)], ], ) - def test_instantiation_with_wrong_length_raises_error(self, values: list[bool]) -> None: + def test_instantiation_with_wrong_length_raises_error(self, values: list[Boolean]) -> None: """Tests that providing the wrong number of items during instantiation fails.""" - - class Bitvector4(BaseBitvector): - LENGTH = 4 - with pytest.raises(ValueError, match="requires exactly 4 bits"): Bitvector4(data=values) def test_pydantic_validation_accepts_valid_list(self) -> None: """Tests that Pydantic validation correctly accepts a valid list of booleans.""" - - class Bitvector4(BaseBitvector): - LENGTH = 4 - - model = create_model("Model", value=(Bitvector4, ...)) - instance: Any = model(value={"data": [True, False, True, False]}) + bits = [Boolean(True), Boolean(False), Boolean(True), Boolean(False)] + instance = Bitvector4Model(value={"data": bits}) # type: ignore[arg-type] assert isinstance(instance.value, Bitvector4) - assert instance.value == Bitvector4(data=[True, False, True, False]) + assert instance.value == Bitvector4(data=bits) @pytest.mark.parametrize( "invalid_value", [ - {"data": [True, False, True]}, # Too short - {"data": [True, False, True, False, True]}, # Too long + {"data": [Boolean(True), Boolean(False), Boolean(True)]}, # Too short + {"data": [Boolean(b) for b in [True, False, True, False, True]]}, # Too long ], ) def test_pydantic_validation_rejects_invalid_values(self, invalid_value: Any) -> None: - """ - Tests that Pydantic validation rejects lists of the wrong length or with invalid types. - """ - - class Bitvector4(BaseBitvector): - LENGTH = 4 - - model = create_model("Model", value=(Bitvector4, ...)) + """Tests that Pydantic validation rejects lists of the wrong length.""" with pytest.raises(ValidationError): - model(value=invalid_value) + Bitvector4Model(value=invalid_value) def test_bitvector_is_immutable(self) -> None: """Tests that attempting to change an item in a Bitvector raises a TypeError.""" @@ -93,7 +102,7 @@ def test_bitvector_is_immutable(self) -> None: class Bitvector2(BaseBitvector): LENGTH = 2 - vec = Bitvector2(data=[True, False]) + vec = Bitvector2(data=[Boolean(True), Boolean(False)]) with pytest.raises(TypeError): vec[0] = False # type: ignore[index] # Should fail because SSZModel is immutable @@ -121,13 +130,9 @@ def test_instantiate_raw_type_raises_error(self) -> None: def test_instantiation_success(self) -> None: """Tests successful instantiation with a valid number of items.""" - - class Bitlist8(BaseBitlist): - LIMIT = 8 - - instance = Bitlist8(data=[True, False, 1, 0]) + instance = Bitlist8(data=[Boolean(True), Boolean(False), Boolean(1), Boolean(0)]) assert len(instance) == 4 - expected = Bitlist8(data=[True, False, True, False]) + expected = Bitlist8(data=[Boolean(True), Boolean(False), Boolean(True), Boolean(False)]) assert instance == expected def test_instantiation_over_limit_raises_error(self) -> None: @@ -136,59 +141,53 @@ def test_instantiation_over_limit_raises_error(self) -> None: class Bitlist4(BaseBitlist): LIMIT = 4 - with pytest.raises(ValueError, match="cannot contain more than 4 bits"): - Bitlist4(data=[True, False, True, False, True]) + with pytest.raises(ValueError, match="cannot exceed 4 bits"): + Bitlist4(data=[Boolean(b) for b in [True, False, True, False, True]]) def test_pydantic_validation_accepts_valid_list(self) -> None: """Tests that Pydantic validation correctly accepts a valid list of booleans.""" - - class Bitlist8(BaseBitlist): - LIMIT = 8 - - model = create_model("Model", value=(Bitlist8, ...)) - instance: Any = model(value={"data": [True, False, True, False]}) + bits = [Boolean(True), Boolean(False), Boolean(True), Boolean(False)] + instance = Bitlist8Model(value={"data": bits}) # type: ignore[arg-type] assert isinstance(instance.value, Bitlist8) assert len(instance.value) == 4 @pytest.mark.parametrize( "invalid_value", [ - {"data": [True] * 9}, # Too long + {"data": [Boolean(True)] * 9}, # Too long ], ) def test_pydantic_validation_rejects_invalid_values(self, invalid_value: Any) -> None: - """Tests that Pydantic validation rejects lists that are too long or have invalid types.""" - - class Bitlist8(BaseBitlist): - LIMIT = 8 - - model = create_model("Model", value=(Bitlist8, ...)) + """Tests that Pydantic validation rejects lists that exceed the limit.""" with pytest.raises(ValidationError): - model(value=invalid_value) + Bitlist8Model(value=invalid_value) def test_add_with_list(self) -> None: """Tests concatenating a Bitlist with a regular list.""" - - class Bitlist8(BaseBitlist): - LIMIT = 8 - - bitlist = Bitlist8(data=[True, False, True]) - result = bitlist + [False, True] + bitlist = Bitlist8(data=[Boolean(True), Boolean(False), Boolean(True)]) + result = bitlist + [Boolean(False), Boolean(True)] assert len(result) == 5 - assert list(result.data) == [True, False, True, False, True] + assert list(result.data) == [ + Boolean(True), + Boolean(False), + Boolean(True), + Boolean(False), + Boolean(True), + ] assert isinstance(result, Bitlist8) def test_add_with_bitlist(self) -> None: """Tests concatenating two Bitlists of the same type.""" - - class Bitlist8(BaseBitlist): - LIMIT = 8 - - bitlist1 = Bitlist8(data=[True, False]) - bitlist2 = Bitlist8(data=[True, True]) + bitlist1 = Bitlist8(data=[Boolean(True), Boolean(False)]) + bitlist2 = Bitlist8(data=[Boolean(True), Boolean(True)]) result = bitlist1 + bitlist2 assert len(result) == 4 - assert list(result.data) == [True, False, True, True] + assert list(result.data) == [ + Boolean(True), + Boolean(False), + Boolean(True), + Boolean(True), + ] assert isinstance(result, Bitlist8) def test_add_exceeding_limit_raises_error(self) -> None: @@ -197,9 +196,9 @@ def test_add_exceeding_limit_raises_error(self) -> None: class Bitlist4(BaseBitlist): LIMIT = 4 - bitlist = Bitlist4(data=[True, False, True]) - with pytest.raises(ValueError, match="cannot contain more than 4 bits"): - bitlist + [False, True] + bitlist = Bitlist4(data=[Boolean(True), Boolean(False), Boolean(True)]) + with pytest.raises(ValueError, match="cannot exceed 4 bits"): + bitlist + [Boolean(False), Boolean(True)] class TestBitfieldSerialization: @@ -222,7 +221,8 @@ def test_bitvector_serialization_deserialization( class TestBitvector(BaseBitvector): LENGTH = length - instance = TestBitvector(data=value) + bool_value = tuple(Boolean(b) for b in value) + instance = TestBitvector(data=bool_value) # Test serialization encoded = instance.encode_bytes() @@ -250,7 +250,8 @@ def test_bitlist_serialization_deserialization( class TestBitlist(BaseBitlist): LIMIT = limit - instance = TestBitlist(data=value) + bool_value = tuple(Boolean(b) for b in value) + instance = TestBitlist(data=bool_value) # Test serialization encoded = instance.encode_bytes() @@ -275,7 +276,7 @@ def test_bitlist_decode_invalid_data(self) -> None: class Bitlist8(BaseBitlist): LIMIT = 8 - with pytest.raises(ValueError, match="Cannot decode empty data"): + with pytest.raises(ValueError, match="Cannot decode empty bytes"): Bitlist8.decode_bytes(b"") @@ -302,7 +303,7 @@ class Bitvector8(BaseBitvector): LENGTH = 8 stream = io.BytesIO(b"\xff") - with pytest.raises(ValueError, match="Invalid scope"): + with pytest.raises(ValueError, match="expected 1 bytes, got 2"): Bitvector8.deserialize(stream, scope=2) def test_bitvector_deserialize_premature_end(self) -> None: @@ -339,7 +340,8 @@ def test_bitvector_encode_decode( class TestBitvector(BaseBitvector): LENGTH = length - instance = TestBitvector(data=value) + bool_value = tuple(Boolean(b) for b in value) + instance = TestBitvector(data=bool_value) encoded = instance.encode_bytes() assert encoded.hex() == expected_hex @@ -374,7 +376,8 @@ def test_bitlist_encode_decode( class TestBitlist(BaseBitlist): LIMIT = limit - instance = TestBitlist(data=value) + bool_value = tuple(Boolean(b) for b in value) + instance = TestBitlist(data=bool_value) encoded = instance.encode_bytes() assert encoded.hex() == expected_hex diff --git a/tests/lean_spec/types/test_boolean.py b/tests/lean_spec/types/test_boolean.py index 4142c11a..7e6168ba 100644 --- a/tests/lean_spec/types/test_boolean.py +++ b/tests/lean_spec/types/test_boolean.py @@ -4,16 +4,21 @@ from typing import Any, Callable import pytest -from pydantic import ValidationError, create_model +from pydantic import BaseModel, ValidationError from lean_spec.types.boolean import Boolean +class BooleanModel(BaseModel): + """Model for testing Pydantic validation of Boolean.""" + + value: Boolean + + @pytest.mark.parametrize("valid_value", [True, False]) def test_pydantic_validation_accepts_valid_bool(valid_value: bool) -> None: """Tests that Pydantic validation correctly accepts a valid boolean.""" - model = create_model("Model", value=(Boolean, ...)) - instance: Any = model(value=valid_value) + instance = BooleanModel(value=valid_value) # type: ignore[arg-type] assert isinstance(instance.value, Boolean) assert instance.value == Boolean(valid_value) @@ -21,9 +26,8 @@ def test_pydantic_validation_accepts_valid_bool(valid_value: bool) -> None: @pytest.mark.parametrize("invalid_value", [1, 0, 1.0, "True"]) def test_pydantic_strict_mode_rejects_invalid_types(invalid_value: Any) -> None: """Tests that Pydantic's strict mode rejects types that are not `bool`.""" - model = create_model("Model", value=(Boolean, ...)) with pytest.raises(ValidationError): - model(value=invalid_value) + BooleanModel(value=invalid_value) @pytest.mark.parametrize("valid_value", [True, False, 1, 0]) diff --git a/tests/lean_spec/types/test_byte_arrays.py b/tests/lean_spec/types/test_byte_arrays.py index d578b2dc..8888f4e4 100644 --- a/tests/lean_spec/types/test_byte_arrays.py +++ b/tests/lean_spec/types/test_byte_arrays.py @@ -250,15 +250,15 @@ def test_pydantic_accepts_various_inputs_for_vectors() -> None: def test_pydantic_validates_vector_lengths() -> None: with pytest.raises(ValueError): - ModelVectors(root=b"\x11" * 31, key=b"\x00\x01\x02\x03") # too short + ModelVectors(root=Bytes32(b"\x11" * 31), key=Bytes4(b"\x00\x01\x02\x03")) # too short with pytest.raises(ValueError): - ModelVectors(root=b"\x11" * 33, key=b"\x00\x01\x02\x03") # too long + ModelVectors(root=Bytes32(b"\x11" * 33), key=Bytes4(b"\x00\x01\x02\x03")) # too long with pytest.raises(ValueError): - ModelVectors(root=b"\x11" * 32, key=b"\x00\x01\x02") # key too short + ModelVectors(root=Bytes32(b"\x11" * 32), key=Bytes4(b"\x00\x01\x02")) # key too short def test_pydantic_accepts_and_serializes_bytelist() -> None: - m = ModelLists(payload=ByteList16(data="0x000102030405060708090a0b0c0d0e0f")) + m = ModelLists(payload=ByteList16(data=bytes.fromhex("000102030405060708090a0b0c0d0e0f"))) assert isinstance(m.payload, ByteList16) assert m.payload.encode_bytes() == bytes(range(16)) @@ -278,7 +278,7 @@ def test_pydantic_accepts_and_serializes_bytelist() -> None: def test_pydantic_bytelist_limit_enforced() -> None: with pytest.raises(ValueError): - ModelLists(payload=bytes(range(17))) # over limit + ModelLists(payload=ByteList16(data=bytes(range(17)))) # over limit def test_add_repr_equality_hash_do_not_crash_on_aliases() -> None: @@ -321,8 +321,8 @@ def test_bytelist_hex_and_concat_behaviour_like_vector() -> None: class ByteList8(BaseByteList): LIMIT = 8 - x = ByteList8(data="0x00010203") - y = ByteList8(data=[4, 5]) + x = ByteList8(data=bytes.fromhex("00010203")) + y = ByteList8(data=bytes([4, 5])) # __add__ returns bytes conc = x + y assert conc == b"\x00\x01\x02\x03\x04\x05" diff --git a/tests/lean_spec/types/test_collections.py b/tests/lean_spec/types/test_collections.py index c81a9cc2..4d98c03b 100644 --- a/tests/lean_spec/types/test_collections.py +++ b/tests/lean_spec/types/test_collections.py @@ -3,7 +3,7 @@ from typing import Any, Tuple import pytest -from pydantic import ValidationError, create_model +from pydantic import BaseModel, ValidationError from typing_extensions import Type from lean_spec.subspecs.koalabear import Fp @@ -200,6 +200,12 @@ class FpList8(SSZList): sig_test_data = tuple(sig_test_data_list) +class Uint8Vector2Model(BaseModel): + """Model for testing Pydantic validation of Uint8Vector2.""" + + value: Uint8Vector2 + + class TestSSZVector: """Tests for the fixed-length, immutable SSZVector type.""" @@ -221,7 +227,7 @@ def test_instantiate_raw_type_raises_error(self) -> None: def test_instantiation_success(self) -> None: """Tests successful instantiation with the correct number of valid items.""" vec_type = Uint8Vector4 - instance = vec_type(data=[1, 2, 3, 4]) + instance = vec_type(data=[Uint8(1), Uint8(2), Uint8(3), Uint8(4)]) assert len(instance) == 4 assert list(instance) == [Uint8(1), Uint8(2), Uint8(3), Uint8(4)] @@ -229,32 +235,37 @@ def test_instantiation_with_wrong_length_raises_error(self) -> None: """Tests that providing the wrong number of items during instantiation fails.""" vec_type = Uint8Vector4 with pytest.raises(ValueError, match="requires exactly 4 items"): - vec_type(data=[1, 2, 3]) # Too few + vec_type(data=[Uint8(1), Uint8(2), Uint8(3)]) # Too few with pytest.raises(ValueError, match="requires exactly 4 items"): - vec_type(data=[1, 2, 3, 4, 5]) # Too many + vec_type(data=[Uint8(1), Uint8(2), Uint8(3), Uint8(4), Uint8(5)]) # Too many def test_pydantic_validation(self) -> None: """Tests that Pydantic validation works for SSZVector types.""" - model = create_model("Model", value=(Uint8Vector2, ...)) - # Test valid data - instance: Any = model(value={"data": [10, 20]}) + # Test valid data - Pydantic coerces dict to Uint8Vector2 + instance = Uint8Vector2Model(value={"data": [10, 20]}) # type: ignore[arg-type] assert isinstance(instance.value, Uint8Vector2) assert list(instance.value) == [Uint8(10), Uint8(20)] # Test invalid data with pytest.raises(ValidationError): - model(value={"data": [10]}) # Too short + Uint8Vector2Model(value={"data": [10]}) # type: ignore[arg-type] with pytest.raises(ValidationError): - model(value={"data": [10, 20, 30]}) # Too long + Uint8Vector2Model(value={"data": [10, 20, 30]}) # type: ignore[arg-type] with pytest.raises(TypeError): - model(value={"data": [10, "bad"]}) # Wrong element type + Uint8Vector2Model(value={"data": [10, "bad"]}) # type: ignore[arg-type] def test_vector_is_immutable(self) -> None: """Tests that attempting to change an item in an SSZVector raises a TypeError.""" - vec = Uint8Vector2(data=[1, 2]) + vec = Uint8Vector2(data=[Uint8(1), Uint8(2)]) with pytest.raises(TypeError): vec[0] = 3 # type: ignore[index] # Should fail because SSZModel is immutable +class Uint8List4Model(BaseModel): + """Model for testing Pydantic validation of Uint8List4.""" + + value: Uint8List4 + + class TestList: """Tests for the variable-length, capacity-limited List type.""" @@ -277,49 +288,50 @@ def test_instantiation_over_limit_raises_error(self) -> None: """Tests that providing more items than the limit during instantiation fails.""" list_type = Uint8List4 with pytest.raises(ValueError, match="cannot contain more than 4 elements"): - list_type(data=[1, 2, 3, 4, 5]) + list_type(data=[Uint8(1), Uint8(2), Uint8(3), Uint8(4), Uint8(5)]) def test_pydantic_validation(self) -> None: """Tests that Pydantic validation works for List types.""" - model = create_model("Model", value=(Uint8List4, ...)) # Test valid data - instance: Any = model(value=Uint8List4(data=[10, 20])) + instance = Uint8List4Model(value=Uint8List4(data=[Uint8(10), Uint8(20)])) assert isinstance(instance.value, Uint8List4) assert list(instance.value) == [Uint8(10), Uint8(20)] - # Test invalid data - with pytest.raises(ValidationError): - model(value=Uint8List4(data=[10, 20, 30, 40, 50])) # Too long + # Test invalid data - list too long with pytest.raises(ValidationError): - model(value=Uint8List4(data=[10, "bad"])) # Wrong element type + Uint8List4Model( + value=Uint8List4(data=[Uint8(10), Uint8(20), Uint8(30), Uint8(40), Uint8(50)]) + ) def test_append_at_limit_raises_error(self) -> None: """Tests that creating a list at limit +1 fails during construction.""" with pytest.raises(ValueError, match="cannot contain more than 4 elements"): - BooleanList4(data=[True] * 5) + BooleanList4(data=[Boolean(True)] * 5) def test_extend_over_limit_raises_error(self) -> None: """Tests that creating a list over the limit fails during construction.""" with pytest.raises(ValueError, match="cannot contain more than 4 elements"): - BooleanList4(data=[True, False, True, False, True]) + BooleanList4( + data=[Boolean(True), Boolean(False), Boolean(True), Boolean(False), Boolean(True)] + ) def test_add_with_list(self) -> None: """Tests concatenating an SSZList with a regular list.""" - list1 = Uint8List10(data=[1, 2, 3]) + list1 = Uint8List10(data=[Uint8(1), Uint8(2), Uint8(3)]) result = list1 + [4, 5] assert list(result) == [Uint8(1), Uint8(2), Uint8(3), Uint8(4), Uint8(5)] assert isinstance(result, Uint8List10) def test_add_with_sszlist(self) -> None: """Tests concatenating two SSZLists of the same type.""" - list1 = Uint8List10(data=[1, 2]) - list2 = Uint8List10(data=[3, 4]) + list1 = Uint8List10(data=[Uint8(1), Uint8(2)]) + list2 = Uint8List10(data=[Uint8(3), Uint8(4)]) result = list1 + list2 assert list(result) == [Uint8(1), Uint8(2), Uint8(3), Uint8(4)] assert isinstance(result, Uint8List10) def test_add_exceeding_limit_raises_error(self) -> None: """Tests that concatenating beyond the limit raises an error.""" - list1 = Uint8List4(data=[1, 2, 3]) + list1 = Uint8List4(data=[Uint8(1), Uint8(2), Uint8(3)]) with pytest.raises(ValueError, match="cannot contain more than 4 elements"): list1 + [4, 5] diff --git a/tests/lean_spec/types/test_uint.py b/tests/lean_spec/types/test_uint.py index 84a0f67a..e973154b 100644 --- a/tests/lean_spec/types/test_uint.py +++ b/tests/lean_spec/types/test_uint.py @@ -1,10 +1,10 @@ """Unsigned Integer Type Tests.""" import io -from typing import IO, Any, Type +from typing import Any, Type import pytest -from pydantic import ValidationError, create_model +from pydantic import BaseModel, ValidationError from lean_spec.types.uint import ( BaseUint, @@ -20,17 +20,50 @@ """A collection of all Uint types to test against.""" +# Model classes for Pydantic validation tests +class Uint8Model(BaseModel): + value: Uint8 + + +class Uint16Model(BaseModel): + value: Uint16 + + +class Uint32Model(BaseModel): + value: Uint32 + + +class Uint64Model(BaseModel): + value: Uint64 + + +class Uint128Model(BaseModel): + value: Uint128 + + +class Uint256Model(BaseModel): + value: Uint256 + + +UINT_MODELS: dict[Type[BaseUint], Type[BaseModel]] = { + Uint8: Uint8Model, + Uint16: Uint16Model, + Uint32: Uint32Model, + Uint64: Uint64Model, + Uint128: Uint128Model, + Uint256: Uint256Model, +} +"""Mapping from Uint types to their corresponding Pydantic model classes.""" + + @pytest.mark.parametrize("uint_class", ALL_UINT_TYPES) def test_pydantic_validation_accepts_valid_int(uint_class: Type[BaseUint]) -> None: """Tests that Pydantic validation correctly accepts a valid integer.""" - # Create the model dynamically - model = create_model("Model", value=(uint_class, ...)) - - # This should pass without errors - instance: Any = model(value=10) - assert isinstance(instance.value, uint_class) - # This assert will also be fixed by the changes in the next section - assert instance.value == uint_class(10) + model = UINT_MODELS[uint_class] + instance = model(value=10) + value = instance.value # type: ignore[attr-defined] + assert isinstance(value, uint_class) + assert value == uint_class(10) @pytest.mark.parametrize("uint_class", ALL_UINT_TYPES) @@ -38,13 +71,8 @@ def test_pydantic_validation_accepts_valid_int(uint_class: Type[BaseUint]) -> No def test_pydantic_strict_mode_rejects_invalid_types( uint_class: Type[BaseUint], invalid_value: Any ) -> None: - """ - Tests that Pydantic's strict mode rejects types that could be coerced to an int. - """ - # Create the model dynamically - model = create_model("Model", value=(uint_class, ...)) - - # Pydantic should raise a ValidationError because of the strict=True flag + """Tests that Pydantic's strict mode rejects types that could be coerced to an int.""" + model = UINT_MODELS[uint_class] with pytest.raises(ValidationError): model(value=invalid_value) diff --git a/tests/lean_spec/types/test_union.py b/tests/lean_spec/types/test_union.py index 22e61448..b7ab6ac1 100644 --- a/tests/lean_spec/types/test_union.py +++ b/tests/lean_spec/types/test_union.py @@ -166,7 +166,7 @@ def test_union_serialize_matches_reference() -> None: (OptionalNumericUnion(selector=1, value=Uint16(43707)), "01bbaa"), (NumericUnion(selector=1, value=Uint32(3735928559)), "01efbeadde"), (ComplexUnion(selector=2, value=Uint8(170)), "02aa"), - (ContainerUnion(selector=1, value=SingleField(A=0xAB)), "01ab"), + (ContainerUnion(selector=1, value=SingleField(A=Uint8(0xAB))), "01ab"), ] for union_instance, expected_hex in test_cases: @@ -177,7 +177,7 @@ def test_union_serialize_matches_reference() -> None: def test_union_with_nested_composites_roundtrip() -> None: """Test serialization roundtrip with complex nested types.""" # Create a union with nested container - original = ContainerUnion(selector=0, value=SingleField(A=42)) + original = ContainerUnion(selector=0, value=SingleField(A=Uint8(42))) # Encode and decode encoded = original.encode_bytes() diff --git a/tox.ini b/tox.ini index 12a73bf9..24f8c398 100644 --- a/tox.ini +++ b/tox.ini @@ -25,22 +25,22 @@ commands = [testenv:lint] description = Lint and code formatting checks (ruff) commands = - ruff check --no-fix --show-fixes src tests packages - ruff format --check src tests packages + ruff check --no-fix --show-fixes + ruff format --check [testenv:fix] description = Auto-fix linting and formatting issues (ruff) commands = - ruff check --fix src tests packages - ruff format src tests packages + ruff check --fix + ruff format [testenv:typecheck] -description = Run type checking (mypy) -commands = mypy src tests packages +description = Run type checking (ty) +commands = ty check [testenv:spellcheck] description = Run spell checking (codespell) -commands = codespell src tests packages docs README.md CLAUDE.md --skip="*.lock,*.svg,.git,__pycache__,.mypy_cache,.pytest_cache" --ignore-words=.codespell-ignore-words.txt +commands = codespell src tests packages docs README.md CLAUDE.md --skip="*.lock,*.svg,.git,__pycache__,.pytest_cache" --ignore-words=.codespell-ignore-words.txt [testenv:mdformat] description = Check markdown formatting for docs (mdformat) diff --git a/uv.lock b/uv.lock index 3930a3ac..af84db2a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12" [manifest] @@ -582,13 +582,13 @@ dev = [ { name = "mkdocs" }, { name = "mkdocs-material" }, { name = "mkdocstrings", extra = ["python"] }, - { name = "mypy" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, { name = "ruff" }, { name = "tomli-w" }, { name = "twine" }, + { name = "ty" }, ] docs = [ { name = "mdformat" }, @@ -598,8 +598,8 @@ docs = [ ] lint = [ { name = "codespell" }, - { name = "mypy" }, { name = "ruff" }, + { name = "ty" }, ] test = [ { name = "hypothesis" }, @@ -627,13 +627,13 @@ dev = [ { name = "mkdocs", specifier = ">=1.6.1,<2" }, { name = "mkdocs-material", specifier = ">=9.5.45,<10" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.27.0,<1" }, - { name = "mypy", specifier = ">=1.17.0,<2" }, { name = "pytest", specifier = ">=8.3.3,<9" }, { name = "pytest-cov", specifier = ">=6.0.0,<7" }, { name = "pytest-xdist", specifier = ">=3.6.1,<4" }, { name = "ruff", specifier = ">=0.13.2,<1" }, { name = "tomli-w", specifier = ">=1.0.0" }, { name = "twine", specifier = ">=5.1.0,<6" }, + { name = "ty", specifier = ">=0.0.1a34" }, ] docs = [ { name = "mdformat", specifier = "==0.7.22" }, @@ -643,8 +643,8 @@ docs = [ ] lint = [ { name = "codespell", specifier = ">=2.4.1,<3" }, - { name = "mypy", specifier = ">=1.17.0,<2" }, { name = "ruff", specifier = ">=0.13.2,<1" }, + { name = "ty", specifier = ">=0.0.1a34" }, ] test = [ { name = "hypothesis", specifier = ">=6.138.14" }, @@ -908,47 +908,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, ] -[[package]] -name = "mypy" -version = "1.18.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, - { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, - { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, - { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, - { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, - { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, - { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, - { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, - { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, - { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, - { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, - { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - [[package]] name = "nh3" version = "0.3.1" @@ -1503,6 +1462,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/ec/00f9d5fd040ae29867355e559a94e9a8429225a0284a3f5f091a3878bfc0/twine-5.1.1-py3-none-any.whl", hash = "sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997", size = 38650, upload-time = "2024-06-26T15:00:43.825Z" }, ] +[[package]] +name = "ty" +version = "0.0.1a34" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/f9/f467d2fbf02a37af5d779eb21c59c7d5c9ce8c48f620d590d361f5220208/ty-0.0.1a34.tar.gz", hash = "sha256:659e409cc3b5c9fb99a453d256402a4e3bd95b1dbcc477b55c039697c807ab79", size = 4735988, upload-time = "2025-12-12T18:29:23.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/b7/d5a5c611baaa20e85971a7c9a527aaf3e8fb47e15de88d1db39c64ee3638/ty-0.0.1a34-py3-none-linux_armv6l.whl", hash = "sha256:00c138e28b12a80577ee3e15fc638eb1e35cf5aa75f5967bf2d1893916ce571c", size = 9708675, upload-time = "2025-12-12T18:29:06.571Z" }, + { url = "https://files.pythonhosted.org/packages/cb/62/0b78976c8da58b90a86d1a1b8816ff4a6e8437f6e52bb6800c4483242e7f/ty-0.0.1a34-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cbb9c187164675647143ecb56e684d6766f7d5ba7f6874a369fe7c3d380a6c92", size = 9515760, upload-time = "2025-12-12T18:28:56.901Z" }, + { url = "https://files.pythonhosted.org/packages/39/1f/4e3d286b37aab3428a30b8f5db5533b8ce6e23b1bd84f77a137bd782b418/ty-0.0.1a34-py3-none-macosx_11_0_arm64.whl", hash = "sha256:68b2375b366ee799a896594cde393a1b60414efdfd31399c326bfc136bfc41f3", size = 9064633, upload-time = "2025-12-12T18:29:10.211Z" }, + { url = "https://files.pythonhosted.org/packages/5d/31/e17049b868f5cac7590c000f31ff9453e4360125416da4e8195e82b5409a/ty-0.0.1a34-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f6b68d9673e43bdd5bdcaa6b5db50e873431fc44dde5e25e253e8226ec93ac1", size = 9310295, upload-time = "2025-12-12T18:29:21.635Z" }, + { url = "https://files.pythonhosted.org/packages/77/1d/7a89b3032e84a01223d0c33e47f33eef436ca36949b28600554a2a4da1f8/ty-0.0.1a34-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:832b360fd397c076e294c252db52581b9ecb38d8063d6262ac927610540702be", size = 9498451, upload-time = "2025-12-12T18:29:24.955Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5e/e782c4367d14b965b1ee9bddc3f3102982ff1cc2dae699c201ecd655e389/ty-0.0.1a34-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb6fc497f1feb67e299fd3507ed30498c7e15b31099b3dcdbeca6b7ac2d3129", size = 9912522, upload-time = "2025-12-12T18:29:00.252Z" }, + { url = "https://files.pythonhosted.org/packages/9c/25/4d72d7174b60adeb9df6e4c5d8552161da2b84ddcebed8ab37d0f7f266ab/ty-0.0.1a34-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:284c8cfd64f255d942ef21953e3d40d087c74dec27e16495bd656decdd208f59", size = 10518743, upload-time = "2025-12-12T18:28:54.944Z" }, + { url = "https://files.pythonhosted.org/packages/05/c5/30a6e377bcab7d5b65d5c78740635b23ecee647bf268c9dc82a91d41c9ba/ty-0.0.1a34-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c34b028305642fd3a9076d4b07d651a819c61a65371ef38cde60f0b54dce6180", size = 10285473, upload-time = "2025-12-12T18:29:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/97/aa/d2cd564ee37a587c8311383a5687584c9aed241a9e67301ee0280301eef3/ty-0.0.1a34-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad997a21648dc64017f11a96b7bb44f088ab0fd589decadc2d686fc97b102f4e", size = 10298873, upload-time = "2025-12-12T18:29:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/2e/80/c427dabd51b5d8b50fc375e18674c098877a9d6545af810ccff4e40ff74a/ty-0.0.1a34-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1afe9798f94c0fbb9e42ff003dfcb4df982f97763d93e5b1d53f9da865a53af", size = 9851399, upload-time = "2025-12-12T18:29:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d8/7240c0e13bc3405b190b4437fbc67c86aa70e349b282e5fa79282181532b/ty-0.0.1a34-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bd335010aa211fbf8149d3507d6331bdb947d5328ca31388cecdbd2eb49275c3", size = 9261475, upload-time = "2025-12-12T18:29:04.638Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a1/6538f8fe7a5b1a71b20461d905969b7f62574cf9c8c6af580b765a647289/ty-0.0.1a34-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:29ebcc56aabaf6aa85c3baf788e211455ffc9935b807ddc9693954b6990e9a3c", size = 9554878, upload-time = "2025-12-12T18:29:16.349Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f2/b8ab163b928de329d88a5f04a5c399a40c1c099b827c70e569e539f9a755/ty-0.0.1a34-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0cbb5a68fddec83c39db6b5f0a5c5da5a3f7d7620e4bcb4ad5bf3a0c7f89ab45", size = 9651340, upload-time = "2025-12-12T18:29:19.92Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1b/1e4e24b684ee5f22dda18d86846430b123fb2e985f0c0eb986e6eccec1b9/ty-0.0.1a34-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f9b3fd934982a9497237bf39fa472f6d201260ac95b3dc75ba9444d05ec01654", size = 9944488, upload-time = "2025-12-12T18:28:58.544Z" }, + { url = "https://files.pythonhosted.org/packages/80/b0/6435f1795f76c57598933624af58bf67385c96b8fa3252f5f9087173e21a/ty-0.0.1a34-py3-none-win32.whl", hash = "sha256:bdabc3f1a048bc2891d4184b818a7ee855c681dd011d00ee672a05bfe6451156", size = 9151401, upload-time = "2025-12-12T18:28:53.028Z" }, + { url = "https://files.pythonhosted.org/packages/73/2e/adce0d7c07f6de30c7f3c125744ec818c7f04b14622a739fe17d4d0bdb93/ty-0.0.1a34-py3-none-win_amd64.whl", hash = "sha256:a4caa2e58685d6801719becbd0504fe61e3ab94f2509e84759f755a0ca480ada", size = 10031079, upload-time = "2025-12-12T18:29:14.556Z" }, + { url = "https://files.pythonhosted.org/packages/23/0d/1f123c69ce121dcabf5449a456a9a37c3bbad396e9e7484514f1fe568f96/ty-0.0.1a34-py3-none-win_arm64.whl", hash = "sha256:dd02c22b538657b042d154fe2d5e250dfb20c862b32e6036a6ffce2fd1ebca9d", size = 9534879, upload-time = "2025-12-12T18:29:18.187Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"