Skip to content

Commit ac7b93f

Browse files
authored
Merge pull request #3234 from CoolCat467/pre-commit-tests-sync
Add tool to keep test-requirements in sync with pre-commit
2 parents cb16631 + 3733c5f commit ac7b93f

File tree

5 files changed

+197
-0
lines changed

5 files changed

+197
-0
lines changed

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ repos:
5858
pass_filenames: false
5959
additional_dependencies: ["astor", "attrs", "black", "ruff"]
6060
files: ^src\/trio\/_core\/(_run|(_i(o_(common|epoll|kqueue|windows)|nstrumentation)))\.py$
61+
- id: sync-test-requirements
62+
name: synchronize test requirements
63+
language: python
64+
entry: python src/trio/_tools/sync_requirements.py
65+
pass_filenames: false
66+
additional_dependencies: ["pyyaml"]
67+
files: ^(test-requirements\.txt)|(\.pre-commit-config\.yaml)$
6168
- repo: https://github.com/astral-sh/uv-pre-commit
6269
rev: 0.7.2
6370
hooks:
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
from typing import TYPE_CHECKING
5+
6+
from trio._tests.pytest_plugin import skip_if_optional_else_raise
7+
8+
# imports in gen_exports that are not in `install_requires` in requirements
9+
try:
10+
import yaml # noqa: F401
11+
except ImportError as error:
12+
skip_if_optional_else_raise(error)
13+
14+
from trio._tools.sync_requirements import (
15+
update_requirements,
16+
yield_pre_commit_version_data,
17+
)
18+
19+
if TYPE_CHECKING:
20+
from pathlib import Path
21+
22+
23+
def test_yield_pre_commit_version_data() -> None:
24+
text = """
25+
repos:
26+
- repo: https://github.com/astral-sh/ruff-pre-commit
27+
rev: v0.11.0
28+
- repo: https://github.com/psf/black-pre-commit-mirror
29+
rev: 25.1.0
30+
"""
31+
results = tuple(yield_pre_commit_version_data(text))
32+
assert results == (
33+
("ruff-pre-commit", "0.11.0"),
34+
("black-pre-commit-mirror", "25.1.0"),
35+
)
36+
37+
38+
def test_update_requirements(
39+
tmp_path: Path,
40+
) -> None:
41+
requirements_file = tmp_path / "requirements.txt"
42+
assert not requirements_file.exists()
43+
requirements_file.write_text(
44+
"""# comment
45+
# also comment but spaces line start
46+
waffles are delicious no equals
47+
black==3.1.4 ; specific version thingy
48+
mypy==1.15.0
49+
ruff==1.2.5
50+
# required by soupy cat""",
51+
encoding="utf-8",
52+
)
53+
assert update_requirements(requirements_file, {"black": "3.1.5", "ruff": "1.2.7"})
54+
assert (
55+
requirements_file.read_text(encoding="utf-8")
56+
== """# comment
57+
# also comment but spaces line start
58+
waffles are delicious no equals
59+
black==3.1.5 ; specific version thingy
60+
mypy==1.15.0
61+
ruff==1.2.7
62+
# required by soupy cat"""
63+
)
64+
65+
66+
def test_update_requirements_no_changes(
67+
tmp_path: Path,
68+
) -> None:
69+
requirements_file = tmp_path / "requirements.txt"
70+
assert not requirements_file.exists()
71+
original = """# comment
72+
# also comment but spaces line start
73+
waffles are delicious no equals
74+
black==3.1.4 ; specific version thingy
75+
mypy==1.15.0
76+
ruff==1.2.5
77+
# required by soupy cat"""
78+
requirements_file.write_text(original, encoding="utf-8")
79+
assert not update_requirements(
80+
requirements_file, {"black": "3.1.4", "ruff": "1.2.5"}
81+
)
82+
assert requirements_file.read_text(encoding="utf-8") == original

src/trio/_tools/sync_requirements.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env python3
2+
3+
"""Sync Requirements - Automatically upgrade test requirements pinned
4+
versions from pre-commit config file."""
5+
6+
from __future__ import annotations
7+
8+
import sys
9+
from pathlib import Path
10+
from typing import TYPE_CHECKING
11+
12+
from yaml import load as load_yaml
13+
14+
if TYPE_CHECKING:
15+
from collections.abc import Generator
16+
17+
from yaml import CLoader as _CLoader, Loader as _Loader
18+
19+
Loader: type[_CLoader | _Loader]
20+
21+
try:
22+
from yaml import CLoader as Loader
23+
except ImportError:
24+
from yaml import Loader
25+
26+
27+
def yield_pre_commit_version_data(
28+
pre_commit_text: str,
29+
) -> Generator[tuple[str, str], None, None]:
30+
"""Yield (name, rev) tuples from pre-commit config file."""
31+
pre_commit_config = load_yaml(pre_commit_text, Loader)
32+
for repo in pre_commit_config["repos"]:
33+
if "repo" not in repo or "rev" not in repo:
34+
continue
35+
url = repo["repo"]
36+
name = url.rsplit("/", 1)[-1]
37+
rev = repo["rev"].removeprefix("v")
38+
yield name, rev
39+
40+
41+
def update_requirements(
42+
requirements: Path,
43+
version_data: dict[str, str],
44+
) -> bool:
45+
"""Return if updated requirements file.
46+
47+
Update requirements file to match versions in version_data."""
48+
changed = False
49+
old_lines = requirements.read_text(encoding="utf-8").splitlines(True)
50+
51+
with requirements.open("w", encoding="utf-8") as file:
52+
for line in old_lines:
53+
# If comment or not version mark line, ignore.
54+
if line.startswith("#") or "==" not in line:
55+
file.write(line)
56+
continue
57+
name, rest = line.split("==", 1)
58+
# Maintain extra markers if they exist
59+
old_version = rest.strip()
60+
extra = "\n"
61+
if ";" in rest:
62+
old_version, extra = rest.split(";", 1)
63+
old_version = old_version.strip()
64+
extra = " ;" + extra
65+
version = version_data.get(name)
66+
# If does not exist, skip
67+
if version is None:
68+
file.write(line)
69+
continue
70+
# Otherwise might have changed
71+
new_line = f"{name}=={version}{extra}"
72+
if new_line != line:
73+
if not changed:
74+
changed = True
75+
print("Changed test requirements version to match pre-commit")
76+
print(f"{name}=={old_version} -> {name}=={version}")
77+
file.write(new_line)
78+
return changed
79+
80+
81+
def main() -> int:
82+
"""Run program."""
83+
84+
source_root = Path.cwd().absolute()
85+
86+
# Double-check we found the right directory
87+
assert (source_root / "LICENSE").exists()
88+
pre_commit = source_root / ".pre-commit-config.yaml"
89+
test_requirements = source_root / "test-requirements.txt"
90+
91+
pre_commit_text = pre_commit.read_text(encoding="utf-8")
92+
93+
# Get tool versions from pre-commit
94+
# Get correct names
95+
pre_commit_versions = {
96+
name.removesuffix("-mirror").removesuffix("-pre-commit"): version
97+
for name, version in yield_pre_commit_version_data(pre_commit_text)
98+
}
99+
changed = update_requirements(test_requirements, pre_commit_versions)
100+
return int(changed)
101+
102+
103+
if __name__ == "__main__":
104+
sys.exit(main())

test-requirements.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ types-pyOpenSSL
2727
# annotations in doc files
2828
types-docutils
2929
sphinx
30+
# sync-requirements
31+
types-PyYAML
3032

3133
# Trio's own dependencies
3234
cffi; os_name == "nt"

test-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ types-docutils==0.21.0.20241128
179179
# via -r test-requirements.in
180180
types-pyopenssl==24.1.0.20240722
181181
# via -r test-requirements.in
182+
types-pyyaml==6.0.12.20250326
183+
# via -r test-requirements.in
182184
types-setuptools==80.0.0.20250429
183185
# via types-cffi
184186
typing-extensions==4.13.2

0 commit comments

Comments
 (0)