Skip to content

Commit 45614ba

Browse files
tcoratgerclaude
andcommitted
refactor(testing): attach filler plugin state via typed StashKeys
The fill plugin smuggled five attributes onto the pytest config object: the fixture collector, the selected fork class, and the per-test fixture path and format. Each assignment needed a type: ignore for setting an undeclared attribute, and each read used a hasattr probe, six ignores in total. Store this state in config.stash keyed by module-level StashKeys instead. The stash is the documented, type-safe place for plugin state, so the collector reads back as a FixtureCollector and the fork class as type[BaseFork] with no ignores, and presence checks use "in" on the stash. Resolve the fork class by membership and direct indexing so it is never optional, since the type checker does not narrow through the NoReturn exit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fe89d8e commit 45614ba

1 file changed

Lines changed: 41 additions & 22 deletions

File tree

  • packages/testing/src/consensus_testing/pytest_plugins

packages/testing/src/consensus_testing/pytest_plugins/filler.py

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import pytest
1212

1313
from consensus_testing.crypto_mode import AggregationProver, CryptoMode
14-
from consensus_testing.forks import FORKS_BY_NAME
14+
from consensus_testing.forks import FORKS_BY_NAME, BaseFork
1515
from consensus_testing.keys import DEFAULT_MAX_SLOT, XmssKeyManager
1616
from consensus_testing.test_fixtures import FIXTURE_FORMATS, FixtureInfo
1717
from lean_spec.spec.forks import Slot, ValidatorIndex
@@ -103,9 +103,11 @@ def add_fixture(
103103

104104
if config is not None:
105105
fixture_path = self.fixture_output_file(test_nodeid, fixture_format)
106-
config.fixture_path_absolute = str(fixture_path.absolute()) # type: ignore[attribute-defined]
107-
config.fixture_path_relative = str(fixture_path.relative_to(self.output_directory)) # type: ignore[attribute-defined]
108-
config.fixture_format = fixture_format # type: ignore[attribute-defined]
106+
config.stash[FIXTURE_PATH_ABSOLUTE_KEY] = str(fixture_path.absolute())
107+
config.stash[FIXTURE_PATH_RELATIVE_KEY] = str(
108+
fixture_path.relative_to(self.output_directory)
109+
)
110+
config.stash[FIXTURE_FORMAT_KEY] = fixture_format
109111

110112
def write_fixtures(self) -> None:
111113
"""Write all collected fixtures to disk, grouped by test function."""
@@ -127,6 +129,22 @@ def write_fixtures(self) -> None:
127129
json.dump(all_tests, output_handle, indent=4)
128130

129131

132+
FIXTURE_COLLECTOR_KEY: pytest.StashKey[FixtureCollector] = pytest.StashKey()
133+
"""Stash key for the session's fixture collector."""
134+
135+
TEST_FORK_CLASS_KEY: pytest.StashKey[type[BaseFork]] = pytest.StashKey()
136+
"""Stash key for the fork class selected by the fork option."""
137+
138+
FIXTURE_PATH_ABSOLUTE_KEY: pytest.StashKey[str] = pytest.StashKey()
139+
"""Stash key for the absolute path of the current test's fixture file."""
140+
141+
FIXTURE_PATH_RELATIVE_KEY: pytest.StashKey[str] = pytest.StashKey()
142+
"""Stash key for the current test's fixture path relative to the output directory."""
143+
144+
FIXTURE_FORMAT_KEY: pytest.StashKey[str] = pytest.StashKey()
145+
"""Stash key for the current test's fixture format name."""
146+
147+
130148
def pytest_addoption(parser: pytest.Parser) -> None:
131149
"""Add command-line options for fixture generation."""
132150
group = parser.getgroup("fill", "leanSpec fixture generation")
@@ -219,8 +237,8 @@ def pytest_configure(config: pytest.Config) -> None:
219237
)
220238
pytest.exit("Missing required --fork option.", returncode=pytest.ExitCode.USAGE_ERROR)
221239

222-
fork_class = FORKS_BY_NAME.get(fork_name.lower())
223-
if fork_class is None:
240+
fork_name_normalized = fork_name.lower()
241+
if fork_name_normalized not in FORKS_BY_NAME:
224242
print(
225243
f"Error: Unsupported fork: {fork_name}\n",
226244
file=sys.stderr,
@@ -231,6 +249,8 @@ def pytest_configure(config: pytest.Config) -> None:
231249
)
232250
pytest.exit("Invalid fork specified.", returncode=pytest.ExitCode.USAGE_ERROR)
233251

252+
fork_class = FORKS_BY_NAME[fork_name_normalized]
253+
234254
# Check output directory
235255
if output_directory.exists() and any(output_directory.iterdir()):
236256
if not clean:
@@ -250,8 +270,8 @@ def pytest_configure(config: pytest.Config) -> None:
250270

251271
output_directory.mkdir(parents=True, exist_ok=True)
252272

253-
config.fixture_collector = FixtureCollector(output_directory, fork_name) # type: ignore[attribute-defined]
254-
config.test_fork_class = fork_class # type: ignore[attribute-defined]
273+
config.stash[FIXTURE_COLLECTOR_KEY] = FixtureCollector(output_directory, fork_name)
274+
config.stash[TEST_FORK_CLASS_KEY] = fork_class
255275

256276

257277
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
@@ -261,10 +281,10 @@ def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item
261281
Real-crypto vectors cannot be mocked, so the fast mocked lane keeps only the
262282
smoke subset and leaves the rest to the real lane.
263283
"""
264-
if not hasattr(config, "test_fork_class"):
284+
if TEST_FORK_CLASS_KEY not in config.stash:
265285
return
266286

267-
fork_class = config.test_fork_class
287+
fork_class = config.stash[TEST_FORK_CLASS_KEY]
268288
verbose = config.getoption("verbose")
269289
mocking = AggregationProver.get_mode() == CryptoMode.MOCKED
270290
deselected_items = []
@@ -349,8 +369,8 @@ def pytest_sessionstart(session: pytest.Session) -> None:
349369

350370
def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
351371
"""Write all collected fixtures at the end of the session."""
352-
if hasattr(session.config, "fixture_collector"):
353-
session.config.fixture_collector.write_fixtures()
372+
if FIXTURE_COLLECTOR_KEY in session.config.stash:
373+
session.config.stash[FIXTURE_COLLECTOR_KEY].write_fixtures()
354374

355375

356376
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
@@ -360,17 +380,16 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo[None]) ->
360380
report = outcome.get_result()
361381

362382
if call.when == "call":
363-
if hasattr(item.config, "fixture_path_absolute") and hasattr(
364-
item.config, "fixture_path_relative"
365-
):
383+
stash = item.config.stash
384+
if FIXTURE_PATH_ABSOLUTE_KEY in stash and FIXTURE_PATH_RELATIVE_KEY in stash:
366385
report.user_properties.append(
367-
("fixture_path_absolute", item.config.fixture_path_absolute)
386+
("fixture_path_absolute", stash[FIXTURE_PATH_ABSOLUTE_KEY])
368387
)
369388
report.user_properties.append(
370-
("fixture_path_relative", item.config.fixture_path_relative)
389+
("fixture_path_relative", stash[FIXTURE_PATH_RELATIVE_KEY])
371390
)
372-
if hasattr(item.config, "fixture_format"):
373-
report.user_properties.append(("fixture_format", item.config.fixture_format))
391+
if FIXTURE_FORMAT_KEY in stash:
392+
report.user_properties.append(("fixture_format", stash[FIXTURE_FORMAT_KEY]))
374393

375394

376395
@pytest.fixture
@@ -470,8 +489,8 @@ def fill_and_collect(**spec_fields: Any) -> Any:
470489
network=fork.name(),
471490
).model_copy(update={"proof_setting": proof_setting})
472491

473-
if hasattr(request.config, "fixture_collector"):
474-
request.config.fixture_collector.add_fixture(
492+
if FIXTURE_COLLECTOR_KEY in request.config.stash:
493+
request.config.stash[FIXTURE_COLLECTOR_KEY].add_fixture(
475494
fixture_format=spec_class.format_name,
476495
fixture=filled_fixture,
477496
test_nodeid=request.node.nodeid,
@@ -489,7 +508,7 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
489508
if "fork" not in metafunc.fixturenames:
490509
return
491510

492-
fork_class = metafunc.config.test_fork_class # type: ignore[attribute-defined]
511+
fork_class = metafunc.config.stash[TEST_FORK_CLASS_KEY]
493512

494513
if not _check_markers_valid_for_fork(list(metafunc.definition.iter_markers()), fork_class):
495514
verbose = metafunc.config.getoption("verbose")

0 commit comments

Comments
 (0)