Skip to content

refactor(all): modernize codebase with python 3.11 features #1812

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ Users can select any of the artifacts depending on their testing needs for their

### πŸ“‹ Misc

- Remove Python 3.10 support ([#1808](https://github.com/ethereum/execution-spec-tests/pull/1808)).
- πŸ”€ Remove Python 3.10 support ([#1808](https://github.com/ethereum/execution-spec-tests/pull/1808)).
- πŸ”€ Modernize codebase with Python 3.11 language features ([#1812](https://github.com/ethereum/execution-spec-tests/pull/1812)).
- ✨ Add changelog formatting validation to CI to ensure consistent punctuation in bullet points [#1691](https://github.com/ethereum/execution-spec-tests/pull/1691).
- ✨ Added the [EIP checklist template](https://eest.ethereum.org/main/writing_tests/checklist_templates/eip_testing_checklist_template/) that serves as a reference to achieve better coverage when implementing tests for new EIPs ([#1327](https://github.com/ethereum/execution-spec-tests/pull/1327)).
- ✨ Added [Post-Mortems of Missed Test Scenarios](https://eest.ethereum.org/main/writing_tests/post_mortems/) to the documentation that serves as a reference list of all cases that were missed during the test implementation phase of a new EIP, and includes the steps taken in order to prevent similar test cases to be missed in the future ([#1327](https://github.com/ethereum/execution-spec-tests/pull/1327)).
Expand Down
4 changes: 2 additions & 2 deletions src/cli/order_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@

import json
from pathlib import Path
from typing import Any, Dict, List, Union, cast
from typing import Any, Dict, List, cast

import click


def recursive_sort(item: Union[Dict[str, Any], List[Any]]) -> Union[Dict[str, Any], List[Any]]:
def recursive_sort(item: Dict[str, Any] | List[Any]) -> Dict[str, Any] | List[Any]:
"""
Recursively sorts an item.

Expand Down
72 changes: 55 additions & 17 deletions src/ethereum_clis/transition_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from abc import abstractmethod
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Mapping, Optional, Type
from typing import Any, Dict, List, LiteralString, Mapping, Optional, Type
from urllib.parse import urlencode

from requests import Response
Expand All @@ -20,6 +20,7 @@
from ethereum_test_base_types import BlobSchedule
from ethereum_test_exceptions import ExceptionMapper
from ethereum_test_forks import Fork
from ethereum_test_forks.helpers import get_development_forks, get_forks
from ethereum_test_types import Alloc, Environment, Transaction

from .ethereum_cli import EthereumCLI
Expand All @@ -38,6 +39,12 @@
SLOW_REQUEST_TIMEOUT = 180


def get_valid_transition_tool_names() -> set[str]:
"""Get all valid transition tool names from deployed and development forks."""
all_available_forks = get_forks() + get_development_forks()
return {fork.transition_tool_name() for fork in all_available_forks}


class TransitionTool(EthereumCLI):
"""
Transition tool abstract base class which should be inherited by all transition tool
Expand Down Expand Up @@ -424,6 +431,49 @@ def _evaluate_stream(

return output

def safe_t8n_args(
self, fork_name: str, chain_id: int, reward: int, temp_dir=None
) -> List[str]:
"""Safely construct t8n arguments with validated inputs."""
# Validate fork name against actual transition tool names from all available forks
valid_forks = get_valid_transition_tool_names()
if fork_name not in valid_forks:
raise ValueError(f"Invalid fork name: {fork_name}")

# Validate chain ID (should be positive integer)
if not isinstance(chain_id, int) or chain_id <= 0:
raise ValueError(f"Invalid chain ID: {chain_id}")

# Validate reward (should be non-negative integer)
if not isinstance(reward, int) or reward < 0:
raise ValueError(f"Invalid reward: {reward}")

# Use literal strings for command flags
input_alloc: LiteralString = "--input.alloc=stdin"
input_txs: LiteralString = "--input.txs=stdin"
input_env: LiteralString = "--input.env=stdin"
output_result: LiteralString = "--output.result=stdout"
output_alloc: LiteralString = "--output.alloc=stdout"
output_body: LiteralString = "--output.body=stdout"
trace_flag: LiteralString = "--trace"

args = [
input_alloc,
input_txs,
input_env,
output_result,
output_alloc,
output_body,
f"--state.fork={fork_name}",
f"--state.chainid={chain_id}",
f"--state.reward={reward}",
]

if self.trace and temp_dir:
args.extend([trace_flag, f"--output.basedir={temp_dir.name}"])

return args

def construct_args_stream(
self, t8n_data: TransitionToolData, temp_dir: tempfile.TemporaryDirectory
) -> List[str]:
Expand All @@ -432,22 +482,10 @@ def construct_args_stream(
if self.subcommand:
command.append(self.subcommand)

args = command + [
"--input.alloc=stdin",
"--input.txs=stdin",
"--input.env=stdin",
"--output.result=stdout",
"--output.alloc=stdout",
"--output.body=stdout",
f"--state.fork={t8n_data.fork_name}",
f"--state.chainid={t8n_data.chain_id}",
f"--state.reward={t8n_data.reward}",
]

if self.trace:
args.append("--trace")
args.append(f"--output.basedir={temp_dir.name}")
return args
safe_args = self.safe_t8n_args(
t8n_data.fork_name, t8n_data.chain_id, t8n_data.reward, temp_dir
)
return command + safe_args

def dump_debug_stream(
self,
Expand Down
22 changes: 8 additions & 14 deletions src/ethereum_test_base_types/base_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@
to_number,
)

N = TypeVar("N", bound="Number")


class ToStringSchema:
"""
Expand All @@ -44,7 +42,7 @@ def __get_pydantic_core_schema__(
class Number(int, ToStringSchema):
"""Class that helps represent numbers in tests."""

def __new__(cls, input_number: NumberConvertible | N):
def __new__(cls, input_number: NumberConvertible | Self):
"""Create a new Number object."""
return super(Number, cls).__new__(cls, to_number(input_number))

Expand All @@ -57,7 +55,7 @@ def hex(self) -> str:
return hex(self)

@classmethod
def or_none(cls: Type[N], input_number: N | NumberConvertible | None) -> N | None:
def or_none(cls: Type[Self], input_number: Self | NumberConvertible | None) -> Self | None:
"""Convert the input to a Number while accepting None."""
if input_number is None:
return input_number
Expand All @@ -67,7 +65,7 @@ def or_none(cls: Type[N], input_number: N | NumberConvertible | None) -> N | Non
class Wei(Number):
"""Class that helps represent wei that can be parsed from strings."""

def __new__(cls, input_number: NumberConvertible | N):
def __new__(cls, input_number: NumberConvertible | Self):
"""Create a new Number object."""
if isinstance(input_number, str):
words = input_number.split()
Expand Down Expand Up @@ -209,9 +207,6 @@ def __get_pydantic_core_schema__(
)


S = TypeVar("S", bound="FixedSizeHexNumber")


class FixedSizeHexNumber(int, ToStringSchema):
"""
A base class that helps represent an integer as a fixed byte-length
Expand All @@ -233,7 +228,7 @@ class Sized(cls): # type: ignore

return Sized

def __new__(cls, input_number: NumberConvertible | N):
def __new__(cls, input_number: NumberConvertible | Self):
"""Create a new Number object."""
i = to_number(input_number)
if i > cls.max_value:
Expand Down Expand Up @@ -276,9 +271,6 @@ class HashInt(FixedSizeHexNumber[32]): # type: ignore
pass


T = TypeVar("T", bound="FixedSizeBytes")


class FixedSizeBytes(Bytes):
"""Class that helps represent bytes of fixed length in tests."""

Expand All @@ -296,7 +288,7 @@ class Sized(cls): # type: ignore

def __new__(
cls,
input_bytes: FixedSizeBytesConvertible | T,
input_bytes: FixedSizeBytesConvertible | Self,
*,
left_padding: bool = False,
right_padding: bool = False,
Expand All @@ -319,7 +311,9 @@ def __hash__(self) -> int:
return super(FixedSizeBytes, self).__hash__()

@classmethod
def or_none(cls: Type[T], input_bytes: T | FixedSizeBytesConvertible | None) -> T | None:
def or_none(
cls: Type[Self], input_bytes: Self | FixedSizeBytesConvertible | None
) -> Self | None:
"""Convert the input to a Fixed Size Bytes while accepting None."""
if input_bytes is None:
return input_bytes
Expand Down
5 changes: 2 additions & 3 deletions src/ethereum_test_base_types/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@

from pydantic import BaseModel, ConfigDict, RootModel
from pydantic.alias_generators import to_camel
from typing_extensions import Self

from .mixins import ModelCustomizationsMixin

Model = TypeVar("Model", bound=BaseModel)

RootModelRootType = TypeVar("RootModelRootType")


Expand All @@ -27,7 +26,7 @@ class EthereumTestRootModel(RootModel[RootModelRootType], ModelCustomizationsMix
class CopyValidateModel(EthereumTestBaseModel):
"""Model that supports copying with validation."""

def copy(self: Model, **kwargs) -> Model:
def copy(self: Self, **kwargs) -> Self:
"""Create a copy of the model with the updated fields that are validated."""
return self.__class__(**(self.model_dump(exclude_unset=True) | kwargs))

Expand Down
6 changes: 3 additions & 3 deletions src/ethereum_test_fixtures/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from .file import Fixtures


@dataclass(kw_only=True)
@dataclass(kw_only=True, slots=True)
class TestInfo:
"""Contains test information from the current node."""

Expand All @@ -34,9 +34,9 @@ class TestInfo:
def strip_test_name(cls, name: str) -> str:
"""Remove test prefix from a python test case name."""
if name.startswith(cls.test_prefix):
return name[len(cls.test_prefix) :]
return name.removeprefix(cls.test_prefix)
if name.endswith(cls.filler_suffix):
return name[: -len(cls.filler_suffix)]
return name.removesuffix(cls.filler_suffix)
return name

def get_name_and_parameters(self) -> Tuple[str, str]:
Expand Down
8 changes: 4 additions & 4 deletions src/ethereum_test_rpc/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import time
from itertools import count
from pprint import pprint
from typing import Any, ClassVar, Dict, List, Literal, Union
from typing import Any, ClassVar, Dict, List, Literal

import requests
from jwt import encode
Expand All @@ -23,7 +23,7 @@
TransactionByHashResponse,
)

BlockNumberType = Union[int, Literal["latest", "earliest", "pending"]]
BlockNumberType = int | Literal["latest", "earliest", "pending"]


class SendTransactionExceptionError(Exception):
Expand Down Expand Up @@ -71,7 +71,7 @@ def __init_subclass__(cls) -> None:
"""Set namespace of the RPC class to the lowercase of the class name."""
namespace = cls.__name__
if namespace.endswith("RPC"):
namespace = namespace[:-3]
namespace = namespace.removesuffix("RPC")
cls.namespace = namespace.lower()

def post_request(self, method: str, *params: Any, extra_headers: Dict | None = None) -> Any:
Expand Down Expand Up @@ -111,7 +111,7 @@ class EthRPC(BaseRPC):

transaction_wait_timeout: int = 60

BlockNumberType = Union[int, Literal["latest", "earliest", "pending"]]
BlockNumberType = int | Literal["latest", "earliest", "pending"]

def __init__(
self,
Expand Down
4 changes: 2 additions & 2 deletions src/ethereum_test_rpc/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from enum import Enum
from hashlib import sha256
from typing import Annotated, Any, List, Union
from typing import Annotated, Any, List

from pydantic import AliasChoices, Field, model_validator

Expand Down Expand Up @@ -97,7 +97,7 @@ class PayloadStatusEnum(str, Enum):


class BlockTransactionExceptionWithMessage(
ExceptionWithMessage[Union[BlockException, TransactionException]] # type: ignore
ExceptionWithMessage[BlockException | TransactionException] # type: ignore
):
"""Exception returned from the execution client with a message."""

Expand Down
10 changes: 4 additions & 6 deletions src/ethereum_test_specs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
from functools import reduce
from os import path
from pathlib import Path
from typing import Callable, ClassVar, Dict, Generator, List, Sequence, Type, TypeVar
from typing import Callable, ClassVar, Dict, Generator, List, Sequence, Type

import pytest
from pydantic import BaseModel, Field, PrivateAttr
from typing_extensions import Self

from ethereum_clis import Result, TransitionTool
from ethereum_test_base_types import to_hex
Expand Down Expand Up @@ -48,9 +49,6 @@ def verify_result(result: Result, env: Environment):
assert result.withdrawals_root == to_hex(Withdrawal.list_root(env.withdrawals))


T = TypeVar("T", bound="BaseTest")


class BaseTest(BaseModel):
"""Represents a base Ethereum test which must return a single test fixture."""

Expand Down Expand Up @@ -91,11 +89,11 @@ def __pydantic_init_subclass__(cls, **kwargs):

@classmethod
def from_test(
cls: Type[T],
cls: Type[Self],
*,
base_test: "BaseTest",
**kwargs,
) -> T:
) -> Self:
"""Create a test in a different format from a base test."""
new_instance = cls(
tag=base_test.tag,
Expand Down
4 changes: 2 additions & 2 deletions src/ethereum_test_specs/helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Helper functions."""

from dataclasses import dataclass
from enum import Enum
from enum import StrEnum
from typing import Any, Dict, List

from ethereum_clis import Result
Expand All @@ -15,7 +15,7 @@
from ethereum_test_types import Transaction, TransactionReceipt


class ExecutionContext(Enum):
class ExecutionContext(StrEnum):
"""The execution context in which a test case can fail."""

BLOCK = "Block"
Expand Down
Loading
Loading