Skip to content

Commit 08f1629

Browse files
authored
refactor(cli,pytest): improve the cli click interfaces for EEST pytest-based commands (#1654)
1 parent 5e8b461 commit 08f1629

File tree

11 files changed

+506
-283
lines changed

11 files changed

+506
-283
lines changed

docs/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ Users can select any of the artifacts depending on their testing needs for their
4545
- ✨ 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)).
4646
- ✨ Added a new `eest` sub-command, `eest info`, to easily print a cloned EEST repository's version and the versions of relevant tools, e.g., `python`, `uv` ([#1621](https://github.com/ethereum/execution-spec-tests/pull/1621)).
4747
- ✨ Add `CONTRIBUTING.md` for execution-spec-tests and improve coding standards documentation ([#1604](https://github.com/ethereum/execution-spec-tests/pull/1604)).
48-
- 🔀 Updated from pytest 7 to [pytest 8](https://docs.pytest.org/en/stable/changelog.html#features-and-improvements), benefits include improved type hinting and hook typing, stricter mark handling, and clearer error messages for plugin and metadata development [#1433](https://github.com/ethereum/execution-spec-tests/pull/1433).
48+
- 🔀 Updated from pytest 7 to [pytest 8](https://docs.pytest.org/en/stable/changelog.html#features-and-improvements), benefits include improved type hinting and hook typing, stricter mark handling, and clearer error messages for plugin and metadata development ([#1433](https://github.com/ethereum/execution-spec-tests/pull/1433)).
4949
- 🐞 Fix bug in ported-from plugin and coverage script that made PRs fail with modified tests that contained no ported tests ([#1661](https://github.com/ethereum/execution-spec-tests/pull/1661)).
50+
- 🔀 Refactor the `click`-based CLI interface used for pytest-based commands (`fill`, `execute`, `consume`) to make them more extensible ([#1654](https://github.com/ethereum/execution-spec-tests/pull/1654)).
5051

5152
### 🧪 Test Cases
5253

src/cli/pytest_commands/base.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
"""Base classes and utilities for pytest-based CLI commands."""
2+
3+
import sys
4+
from abc import ABC, abstractmethod
5+
from dataclasses import dataclass
6+
from functools import wraps
7+
from typing import Any, Callable, Dict, List, Optional
8+
9+
import click
10+
import pytest
11+
from rich.console import Console
12+
13+
14+
@dataclass
15+
class PytestExecution:
16+
"""Configuration for a single pytest execution."""
17+
18+
config_file: str
19+
"""Path to the pytest configuration file (e.g., 'pytest.ini')."""
20+
21+
args: List[str]
22+
"""Arguments to pass to pytest."""
23+
24+
description: Optional[str] = None
25+
"""Optional description for this execution phase."""
26+
27+
28+
class ArgumentProcessor(ABC):
29+
"""Base class for processing command-line arguments."""
30+
31+
@abstractmethod
32+
def process_args(self, args: List[str]) -> List[str]:
33+
"""Process the given arguments and return modified arguments."""
34+
pass
35+
36+
37+
class PytestRunner:
38+
"""Handles execution of pytest commands."""
39+
40+
def __init__(self):
41+
"""Initialize the pytest runner with a console for output."""
42+
self.console = Console(highlight=False)
43+
44+
def run_single(self, config_file: str, args: List[str]) -> int:
45+
"""Run pytest once with the given configuration and arguments."""
46+
pytest_args = ["-c", config_file] + args
47+
48+
if self._is_verbose(args):
49+
pytest_cmd = f"pytest {' '.join(pytest_args)}"
50+
self.console.print(f"Executing: [bold]{pytest_cmd}[/bold]")
51+
52+
return pytest.main(pytest_args)
53+
54+
def _is_verbose(self, args: List[str]) -> bool:
55+
"""Check if verbose output is requested."""
56+
return any(arg in ["-v", "--verbose", "-vv", "-vvv"] for arg in args)
57+
58+
def run_multiple(self, executions: List[PytestExecution]) -> int:
59+
"""
60+
Run multiple pytest executions in sequence.
61+
62+
Returns the exit code of the final execution, or the first non-zero exit code.
63+
"""
64+
for i, execution in enumerate(executions):
65+
if execution.description and len(executions) > 1:
66+
self.console.print(
67+
f"Phase {i + 1}/{len(executions)}: [italic]{execution.description}[/italic]"
68+
)
69+
70+
result = self.run_single(execution.config_file, execution.args)
71+
if result != 0:
72+
return result
73+
74+
return 0
75+
76+
77+
class PytestCommand:
78+
"""
79+
Base class for pytest-based CLI commands.
80+
81+
Provides a standard structure for commands that execute pytest
82+
with specific configurations and argument processing.
83+
"""
84+
85+
def __init__(
86+
self,
87+
config_file: str,
88+
argument_processors: Optional[List[ArgumentProcessor]] = None,
89+
):
90+
"""
91+
Initialize the pytest command.
92+
93+
Args:
94+
config_file: Pytest configuration file to use
95+
argument_processors: List of processors to apply to arguments
96+
97+
"""
98+
self.config_file = config_file
99+
self.argument_processors = argument_processors or []
100+
self.runner = PytestRunner()
101+
102+
def execute(self, pytest_args: List[str]) -> None:
103+
"""Execute the command with the given pytest arguments."""
104+
executions = self.create_executions(pytest_args)
105+
result = self.runner.run_multiple(executions)
106+
sys.exit(result)
107+
108+
def create_executions(self, pytest_args: List[str]) -> List[PytestExecution]:
109+
"""
110+
Create the list of pytest executions for this command.
111+
112+
This method can be overridden by subclasses to implement
113+
multi-phase execution (e.g., for future fill command).
114+
"""
115+
processed_args = self.process_arguments(pytest_args)
116+
117+
return [
118+
PytestExecution(
119+
config_file=self.config_file,
120+
args=processed_args,
121+
)
122+
]
123+
124+
def process_arguments(self, args: List[str]) -> List[str]:
125+
"""Apply all argument processors to the given arguments."""
126+
processed_args = args[:]
127+
128+
for processor in self.argument_processors:
129+
processed_args = processor.process_args(processed_args)
130+
131+
return processed_args
132+
133+
134+
def common_pytest_options(func: Callable[..., Any]) -> Callable[..., Any]:
135+
"""
136+
Apply common Click options for pytest-based commands.
137+
138+
This decorator adds the standard help options that all pytest commands use.
139+
"""
140+
func = click.option(
141+
"-h",
142+
"--help",
143+
"help_flag",
144+
is_flag=True,
145+
default=False,
146+
expose_value=True,
147+
help="Show help message.",
148+
)(func)
149+
150+
func = click.option(
151+
"--pytest-help",
152+
"pytest_help_flag",
153+
is_flag=True,
154+
default=False,
155+
expose_value=True,
156+
help="Show pytest's help message.",
157+
)(func)
158+
159+
return click.argument("pytest_args", nargs=-1, type=click.UNPROCESSED)(func)
160+
161+
162+
def create_pytest_command_decorator(
163+
config_file: str,
164+
argument_processors: Optional[List[ArgumentProcessor]] = None,
165+
context_settings: Optional[Dict[str, Any]] = None,
166+
) -> Callable[[Callable[..., Any]], click.Command]:
167+
"""
168+
Create a Click command decorator for a pytest-based command.
169+
170+
Args:
171+
config_file: Pytest configuration file to use
172+
argument_processors: List of argument processors to apply
173+
context_settings: Additional Click context settings
174+
175+
Returns:
176+
A decorator that creates a Click command executing pytest
177+
178+
"""
179+
default_context_settings = {"ignore_unknown_options": True}
180+
if context_settings:
181+
default_context_settings.update(context_settings)
182+
183+
def decorator(func: Callable[..., Any]) -> click.Command:
184+
command = PytestCommand(config_file, argument_processors)
185+
186+
@click.command(
187+
context_settings=default_context_settings,
188+
)
189+
@common_pytest_options
190+
@wraps(func)
191+
def wrapper(pytest_args: List[str], **kwargs) -> None:
192+
command.execute(list(pytest_args))
193+
194+
return wrapper
195+
196+
return decorator
Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
"""CLI entry point for the EIP version checker pytest-based command."""
22

3-
import sys
43
from typing import List
54

65
import click
7-
import pytest
86

97
from config.check_eip_versions import CheckEipVersionsConfig
108

11-
from .common import common_click_options, handle_help_flags
9+
from .base import PytestCommand, common_pytest_options
10+
from .processors import HelpFlagsProcessor
1211

1312

1413
@click.command(context_settings={"ignore_unknown_options": True})
15-
@common_click_options
14+
@common_pytest_options
1615
def check_eip_versions(pytest_args: List[str], **kwargs) -> None:
1716
"""Run pytest with the `spec_version_checker` plugin."""
18-
args = ["-c", "pytest-check-eip-versions.ini"]
19-
args += ["--until", CheckEipVersionsConfig().UNTIL_FORK]
20-
args += handle_help_flags(list(pytest_args), pytest_type="check-eip-versions")
21-
result = pytest.main(args)
22-
sys.exit(result)
17+
command = PytestCommand(
18+
config_file="pytest-check-eip-versions.ini",
19+
argument_processors=[HelpFlagsProcessor("check-eip-versions")],
20+
)
21+
22+
args_with_until = ["--until", CheckEipVersionsConfig().UNTIL_FORK] + list(pytest_args)
23+
command.execute(args_with_until)

src/cli/pytest_commands/common.py

Lines changed: 0 additions & 92 deletions
This file was deleted.

0 commit comments

Comments
 (0)