Skip to content

Commit 2165b37

Browse files
committed
Re-apply subprocess collection: in-process approach fails on Python 3.14 full-suite runs
1 parent 84346a0 commit 2165b37

1 file changed

Lines changed: 63 additions & 60 deletions

File tree

ci/test_custom_linters.py

Lines changed: 63 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Custom lint tests."""
22

3+
import subprocess
4+
import sys
35
from pathlib import Path
46

57
import pytest
@@ -19,52 +21,61 @@ def _ci_patterns(*, repository_root: Path) -> set[str]:
1921
return ci_patterns
2022

2123

22-
class _CollectPlugin:
23-
"""Pytest plugin that records the node IDs of collected items."""
24-
25-
def __init__(self) -> None:
26-
"""Start with an empty set of collected node IDs."""
27-
self.collected: set[str] = set()
28-
29-
def pytest_itemcollected(self, item: pytest.Item) -> None:
30-
"""Record each collected item's node ID."""
31-
self.collected.add(item.nodeid)
32-
33-
3424
@beartype
35-
def _tests_from_pattern(*, ci_pattern: str) -> set[str]:
36-
"""From a CI pattern, get all tests ``pytest`` would collect."""
37-
plugin = _CollectPlugin()
38-
pytest.main(
25+
def _collect(
26+
*, ci_pattern: str, repository_root: Path
27+
) -> subprocess.CompletedProcess[str]:
28+
"""Run ``pytest --collect-only`` for ``ci_pattern`` in a fresh
29+
subprocess.
30+
31+
A real subprocess (not ``pytest.main``) is used so that plugin state
32+
-- notably ``pytest-beartype-tests`` re-wrapping the same test
33+
functions -- does not accumulate across iterations and trigger
34+
``Cannot stringify annotation containing string formatting`` under
35+
Python 3.14 deferred annotations.
36+
See https://github.com/adamtheturtle/pytest-beartype-tests/issues/30.
37+
"""
38+
return subprocess.run(
3939
args=[
40+
sys.executable,
41+
"-m",
42+
"pytest",
4043
"-q",
4144
"--collect-only",
4245
# Disable pytest-retry to avoid:
4346
# ```
4447
# ValueError: no option named 'filtered_exceptions'
4548
# ```
46-
# which causes the nested run to exit with INTERNAL_ERROR
47-
# before any items are collected.
4849
"-p",
4950
"no:pytest-retry",
50-
# Disable pytest-beartype-tests to avoid
51-
# https://github.com/beartype/beartype/issues/637 — wrapping
52-
# collected items with @beartype installs a buggy
53-
# __annotate_beartype__ closure on the underlying test
54-
# function, which crashes a subsequent nested collection on
55-
# Python 3.14.
56-
"-p",
57-
"no:pytest_beartype_tests",
5851
# Disable warnings to avoid many instances of:
5952
# ```
6053
# Unknown config option: retry_delay
6154
# ```
6255
"--disable-warnings",
6356
ci_pattern,
6457
],
65-
plugins=[plugin],
58+
check=False,
59+
cwd=repository_root,
60+
capture_output=True,
61+
text=True,
6662
)
67-
return plugin.collected
63+
64+
65+
@beartype
66+
def _tests_from_pattern(*, ci_pattern: str, repository_root: Path) -> set[str]:
67+
"""From a CI pattern, get all tests ``pytest`` would collect."""
68+
result = _collect(
69+
ci_pattern=ci_pattern,
70+
repository_root=repository_root,
71+
)
72+
tests: set[str] = set()
73+
for line in result.stdout.splitlines():
74+
# We filter empty lines and lines which look like
75+
# "9 tests collected in 0.01s".
76+
if line and "collected in" not in line:
77+
tests.add(line)
78+
return tests
6879

6980

7081
def test_ci_patterns_valid(request: pytest.FixtureRequest) -> None:
@@ -73,53 +84,45 @@ def test_ci_patterns_valid(request: pytest.FixtureRequest) -> None:
7384
test in
7485
the test suite.
7586
"""
76-
ci_patterns = _ci_patterns(repository_root=request.config.rootpath)
87+
repository_root = request.config.rootpath
88+
ci_patterns = _ci_patterns(repository_root=repository_root)
7789

7890
for ci_pattern in ci_patterns:
79-
collect_only_result = pytest.main(
80-
args=[
81-
"--collect-only",
82-
ci_pattern,
83-
# Disable pytest-retry to avoid:
84-
# ```
85-
# ValueError: no option named 'filtered_exceptions'
86-
# ````
87-
"-p",
88-
"no:pytest-retry",
89-
# Disable pytest-beartype-tests to avoid
90-
# https://github.com/beartype/beartype/issues/637 —
91-
# wrapping collected items with @beartype installs a
92-
# buggy __annotate_beartype__ closure on the underlying
93-
# test function, which crashes a subsequent nested
94-
# collection on Python 3.14.
95-
"-p",
96-
"no:pytest_beartype_tests",
97-
# Disable warnings to avoid many instances of:
98-
# ```
99-
# Unknown config option: retry_delay
100-
# ```
101-
"--disable-warnings",
102-
],
91+
result = _collect(
92+
ci_pattern=ci_pattern,
93+
repository_root=repository_root,
10394
)
104-
105-
message = f'"{ci_pattern}" does not match any tests.'
106-
assert collect_only_result == 0, message
95+
message = (
96+
f'"{ci_pattern}" does not match any tests.\n'
97+
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
98+
)
99+
assert result.returncode == 0, message
107100

108101

109102
def test_tests_collected_once(request: pytest.FixtureRequest) -> None:
110103
"""Each test in the test suite is collected exactly once.
111104
112105
This does not necessarily mean that they are run - they may be skipped.
113106
"""
114-
ci_patterns = _ci_patterns(repository_root=request.config.rootpath)
115-
all_tests = _tests_from_pattern(ci_pattern=".")
107+
repository_root = request.config.rootpath
108+
ci_patterns = _ci_patterns(repository_root=repository_root)
109+
all_tests = _tests_from_pattern(
110+
ci_pattern=".",
111+
repository_root=repository_root,
112+
)
116113
assert all_tests
117114
tests_to_patterns: dict[str, set[str]] = {}
118115

119116
for pattern in ci_patterns:
120-
tests = _tests_from_pattern(ci_pattern=pattern)
117+
tests = _tests_from_pattern(
118+
ci_pattern=pattern,
119+
repository_root=repository_root,
120+
)
121121
for test in tests:
122-
tests_to_patterns.setdefault(test, set()).add(pattern)
122+
if test in tests_to_patterns:
123+
tests_to_patterns[test].add(pattern)
124+
else:
125+
tests_to_patterns[test] = {pattern}
123126

124127
for test_name, patterns in tests_to_patterns.items():
125128
message = (

0 commit comments

Comments
 (0)