Skip to content

Commit 49b6aa0

Browse files
committed
Merge remote-tracking branch 'origin/main' into support-python-3.14
# Conflicts: # .github/workflows/test.yml # ci/test_custom_linters.py
2 parents 86d13cb + 1209106 commit 49b6aa0

3 files changed

Lines changed: 62 additions & 79 deletions

File tree

.github/workflows/test.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
strategy:
2727
fail-fast: false
2828
matrix:
29-
python-version: ['3.14']
29+
python-version: ['3.13']
3030
ci_pattern:
3131
- tests/mock_vws/test_query.py::TestContentType
3232
- tests/mock_vws/test_query.py::TestSuccess
@@ -114,13 +114,13 @@ jobs:
114114
- tests/mock_vws/test_update_target.py::TestInactiveProject
115115
- tests/mock_vws/test_requests_mock_usage.py
116116
- tests/mock_vws/test_respx_mock_usage.py
117-
- tests/mock_vws/test_target_validators.py
118117
- tests/mock_vws/test_flask_app_usage.py
119118
- tests/mock_vws/test_vumark_generation_api.py
119+
- tests/mock_vws/test_target_validators.py
120120
- tests/mock_vws/test_docker.py
121+
- ci/test_custom_linters.py
121122
- README.rst
122-
- docs/source/basic-example.rst
123-
- docs/source/httpx-example.rst
123+
- docs/
124124

125125
steps:
126126
- uses: actions/checkout@v6
@@ -191,7 +191,7 @@ jobs:
191191
runs-on: ubuntu-latest
192192
strategy:
193193
matrix:
194-
python-version: ['3.14']
194+
python-version: ['3.13']
195195
platform: [ubuntu-latest]
196196

197197
steps:
@@ -237,7 +237,7 @@ jobs:
237237
runs-on: windows-latest
238238
strategy:
239239
matrix:
240-
python-version: ['3.14']
240+
python-version: ['3.13']
241241

242242
steps:
243243
- uses: actions/checkout@v6

ci/test_custom_linters.py

Lines changed: 55 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
"""Custom lint tests."""
22

3-
import subprocess
4-
import sys
53
from pathlib import Path
6-
from typing import TYPE_CHECKING
74

85
import pytest
96
import yaml
107
from beartype import beartype
118

12-
if TYPE_CHECKING:
13-
from collections.abc import Iterable
14-
159

1610
@beartype
1711
def _ci_patterns(*, repository_root: Path) -> set[str]:
@@ -25,34 +19,44 @@ def _ci_patterns(*, repository_root: Path) -> set[str]:
2519
return ci_patterns
2620

2721

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+
2834
@beartype
29-
def _collect(
30-
*, ci_pattern: str, repository_root: Path
31-
) -> subprocess.CompletedProcess[str]:
32-
"""Run ``pytest --collect-only`` for ``ci_pattern`` in a fresh
33-
subprocess.
34-
35-
A real subprocess (not ``pytest.main``) is used so that plugin state
36-
(notably ``pytest-beartype-tests`` re-wrapping the same test
37-
functions) does not accumulate across iterations and trigger
38-
``Cannot stringify annotation containing string formatting`` under
39-
Python 3.14 deferred annotations.
40-
"""
41-
return subprocess.run(
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(
4239
args=[
43-
sys.executable,
44-
"-m",
45-
"pytest",
4640
"-q",
4741
"--collect-only",
42+
# Disable pytest-retry to avoid:
43+
# ```
44+
# ValueError: no option named 'filtered_exceptions'
45+
# ```
46+
# which causes the nested run to exit with INTERNAL_ERROR
47+
# before any items are collected.
48+
"-p",
49+
"no:pytest-retry",
50+
# Disable warnings to avoid many instances of:
51+
# ```
52+
# Unknown config option: retry_delay
53+
# ```
4854
"--disable-warnings",
4955
ci_pattern,
5056
],
51-
check=False,
52-
cwd=repository_root,
53-
capture_output=True,
54-
text=True,
57+
plugins=[plugin],
5558
)
59+
return plugin.collected
5660

5761

5862
def test_ci_patterns_valid(request: pytest.FixtureRequest) -> None:
@@ -61,38 +65,43 @@ def test_ci_patterns_valid(request: pytest.FixtureRequest) -> None:
6165
test in
6266
the test suite.
6367
"""
64-
repository_root = request.config.rootpath
65-
ci_patterns = _ci_patterns(repository_root=repository_root)
68+
ci_patterns = _ci_patterns(repository_root=request.config.rootpath)
6669

6770
for ci_pattern in ci_patterns:
68-
result = _collect(
69-
ci_pattern=ci_pattern,
70-
repository_root=repository_root,
71-
)
72-
message = (
73-
f'"{ci_pattern}" does not match any tests.\n'
74-
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
71+
collect_only_result = pytest.main(
72+
args=[
73+
"--collect-only",
74+
ci_pattern,
75+
# Disable pytest-retry to avoid:
76+
# ```
77+
# ValueError: no option named 'filtered_exceptions'
78+
# ````
79+
"-p",
80+
"no:pytest-retry",
81+
# Disable warnings to avoid many instances of:
82+
# ```
83+
# Unknown config option: retry_delay
84+
# ```
85+
"--disable-warnings",
86+
],
7587
)
76-
assert result.returncode == 0, message
7788

89+
message = f'"{ci_pattern}" does not match any tests.'
90+
assert collect_only_result == 0, message
7891

79-
def test_tests_collected_once(
80-
*,
81-
request: pytest.FixtureRequest,
82-
) -> None:
92+
93+
def test_tests_collected_once(request: pytest.FixtureRequest) -> None:
8394
"""Each test in the test suite is collected exactly once.
8495
8596
This does not necessarily mean that they are run - they may be skipped.
8697
"""
87-
repository_root = request.config.rootpath
88-
ci_patterns = _ci_patterns(repository_root=repository_root)
98+
ci_patterns = _ci_patterns(repository_root=request.config.rootpath)
99+
all_tests = _tests_from_pattern(ci_pattern=".")
100+
assert all_tests
89101
tests_to_patterns: dict[str, set[str]] = {}
90102

91103
for pattern in ci_patterns:
92-
tests = _tests_from_pattern(
93-
ci_pattern=pattern,
94-
repository_root=repository_root,
95-
)
104+
tests = _tests_from_pattern(ci_pattern=pattern)
96105
for test in tests:
97106
if test in tests_to_patterns:
98107
tests_to_patterns[test].add(pattern)
@@ -107,32 +116,5 @@ def test_tests_collected_once(
107116
)
108117
assert len(patterns) == 1, message
109118

110-
all_tests = _tests_from_pattern(
111-
ci_pattern=".",
112-
repository_root=repository_root,
113-
)
114-
# Exclude this file's own meta-tests from the comparison: they are
115-
# not part of any CI pattern by design (they validate the patterns).
116-
all_tests = {t for t in all_tests if not t.startswith("ci/")}
117119
assert tests_to_patterns.keys() - all_tests == set()
118120
assert all_tests - tests_to_patterns.keys() == set()
119-
120-
121-
@beartype
122-
def _tests_from_pattern(
123-
*,
124-
ci_pattern: str,
125-
repository_root: Path,
126-
) -> set[str]:
127-
"""From a CI pattern, get all tests ``pytest`` would collect."""
128-
result = _collect(
129-
ci_pattern=ci_pattern,
130-
repository_root=repository_root,
131-
)
132-
tests: Iterable[str] = set()
133-
for line in result.stdout.splitlines():
134-
# We filter empty lines and lines which look like
135-
# "9 tests collected in 0.01s".
136-
if line and "collected in" not in line:
137-
tests = {*tests, line}
138-
return set(tests)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ ignore_names = [
423423
# pytest configuration
424424
"pytest_collect_file",
425425
"pytest_collection_modifyitems",
426+
"pytest_itemcollected",
426427
"pytest_plugins",
427428
"pytest_set_filtered_exceptions",
428429
"pytest_addoption",

0 commit comments

Comments
 (0)