Skip to content

Commit cf23bea

Browse files
authored
[feat] Make utils release (#496)
* feat: add test and docs for get_loaded_kernels. * feat: implement utilities for managing release versions. * remove unneeded stuff * up * switch to toml. * regex * Version * flake * tomlkit.
1 parent 6e7004e commit cf23bea

6 files changed

Lines changed: 313 additions & 2 deletions

File tree

Makefile

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: style kernel-builder-cli-docs quality pin-actions
1+
.PHONY: style kernel-builder-cli-docs quality bump-dev bump-dev-dry-run pre-release pre-release-dry-run pin-actions
22

33

44
export check_dirs := kernels/src kernels/tests kernels-data/bindings/python
@@ -27,3 +27,25 @@ pin-actions:
2727
quality:
2828
ruff format --check ${check_dirs}
2929
ruff check ${check_dirs}
30+
31+
# Bump every version site to the next dev release based on the currently
32+
# installed `kernels` package version (e.g. installed 0.13.0 -> 0.14.0.dev0).
33+
# Refreshes Cargo.lock and kernels/uv.lock so all sites stay consistent.
34+
bump-dev:
35+
python scripts/bump_to_dev.py
36+
cargo check --workspace
37+
cd kernels && uv lock
38+
39+
bump-dev-dry-run:
40+
python scripts/bump_to_dev.py --dry-run
41+
42+
# Strip the `.dev0` / `-dev0` suffix from every version site in prep for a
43+
# release (e.g. codebase 0.14.0.dev0 -> 0.14.0). Refreshes Cargo.lock and
44+
# kernels/uv.lock so all sites stay consistent.
45+
pre-release:
46+
python scripts/pre_release.py
47+
cargo check --workspace
48+
cd kernels && uv lock
49+
50+
pre-release-dry-run:
51+
python scripts/pre_release.py --dry-run

docs/source/api/kernels.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@
2626

2727
### get_locked_kernel
2828

29-
[[autodoc]] kernels.get_locked_kernel
29+
[[autodoc]] kernels.get_locked_kernel

flake.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
pinact
152152
pkg-config
153153
rust
154+
uv # For `make bump-dev` / `make pre-release` to refresh kernels/uv.lock.
154155
];
155156
buildInputs = [
156157
black

scripts/_version_common.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Shared helpers for bump_to_dev.py and pre_release.py.
2+
3+
Both scripts rewrite the same set of version sites; only the version-computation
4+
step differs. Constants, I/O helpers, and the interactive prompt live here.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from pathlib import Path
10+
11+
import tomlkit
12+
from packaging.version import Version
13+
14+
REPO_ROOT = Path(__file__).resolve().parent.parent
15+
16+
PRIMARY_PYPROJECT = REPO_ROOT / "kernels" / "pyproject.toml"
17+
18+
PYPROJECT_FILES = [PRIMARY_PYPROJECT]
19+
20+
CARGO_FILES = [
21+
REPO_ROOT / "kernels-data" / "Cargo.toml",
22+
REPO_ROOT / "kernels-data" / "bindings" / "python" / "Cargo.toml",
23+
REPO_ROOT / "kernel-builder" / "Cargo.toml",
24+
REPO_ROOT / "kernel-abi-check" / "kernel-abi-check" / "Cargo.toml",
25+
REPO_ROOT / "kernel-abi-check" / "bindings" / "python" / "Cargo.toml",
26+
]
27+
28+
29+
def display_path(path: Path) -> Path:
30+
try:
31+
return path.relative_to(REPO_ROOT)
32+
except ValueError:
33+
return path
34+
35+
36+
def _version_table(doc, path: Path):
37+
"""Return the [project] or [package] table holding the top-level ``version``.
38+
39+
pyproject.toml stores it under ``[project]``; Cargo.toml under ``[package]``.
40+
"""
41+
for name in ("project", "package"):
42+
table = doc.get(name)
43+
if table is not None and "version" in table:
44+
return table
45+
raise SystemExit(
46+
f"Could not find a top-level `version` under [project] or [package] in {display_path(path)}."
47+
)
48+
49+
50+
def get_codebase_version() -> Version:
51+
doc = tomlkit.parse(PRIMARY_PYPROJECT.read_text())
52+
return Version(str(_version_table(doc, PRIMARY_PYPROJECT)["version"]))
53+
54+
55+
def replace_top_level_version(path: Path, new_version: str, *, dry_run: bool) -> str | None:
56+
doc = tomlkit.parse(path.read_text())
57+
table = _version_table(doc, path)
58+
59+
old_version = str(table["version"])
60+
if old_version == new_version:
61+
return None
62+
63+
table["version"] = new_version
64+
if not dry_run:
65+
path.write_text(tomlkit.dumps(doc))
66+
return old_version
67+
68+
69+
def confirm(prompt: str) -> bool:
70+
try:
71+
answer = input(f"{prompt} [y/N]: ").strip().lower()
72+
except EOFError:
73+
return False
74+
return answer in ("y", "yes")

scripts/bump_to_dev.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/env python3
2+
"""Bump all version strings in the repo to the next development version.
3+
4+
Reads the current ``kernels`` version from ``kernels/pyproject.toml`` (the
5+
source-of-truth in the codebase — no install required).
6+
7+
Example: codebase at ``0.13.0`` -> Python sites get ``0.14.0.dev0`` (PEP 440)
8+
and Cargo sites get ``0.14.0-dev0``.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import argparse
14+
from pathlib import Path
15+
16+
from packaging.version import Version
17+
18+
from _version_common import (
19+
CARGO_FILES,
20+
PRIMARY_PYPROJECT,
21+
PYPROJECT_FILES,
22+
confirm,
23+
display_path,
24+
get_codebase_version,
25+
replace_top_level_version,
26+
)
27+
28+
29+
def next_dev_versions(current: Version) -> tuple[str, str]:
30+
if current.is_prerelease or current.is_postrelease or current.local is not None:
31+
raise SystemExit(
32+
f"Codebase version `{current}` is not a plain release (e.g. 0.13.0). "
33+
"This tool bumps from a release to the next dev cycle. Set "
34+
f"{display_path(PRIMARY_PYPROJECT)} to a release version first."
35+
)
36+
37+
release = current.release
38+
if len(release) < 2:
39+
raise SystemExit(
40+
f"Codebase version `{current}` is missing a minor component; "
41+
"expected at least MAJOR.MINOR (e.g. 0.13.0)."
42+
)
43+
44+
major, minor = release[0], release[1]
45+
next_minor = f"{major}.{minor + 1}.0"
46+
return f"{next_minor}.dev0", f"{next_minor}-dev0"
47+
48+
49+
def main(argv: list[str] | None = None) -> int:
50+
parser = argparse.ArgumentParser(description=__doc__)
51+
parser.add_argument(
52+
"--dry-run",
53+
action="store_true",
54+
help="Show which files would change without writing them.",
55+
)
56+
parser.add_argument(
57+
"-y",
58+
"--yes",
59+
action="store_true",
60+
help="Skip the interactive confirmation prompt.",
61+
)
62+
args = parser.parse_args(argv)
63+
64+
current = get_codebase_version()
65+
python_dev, cargo_dev = next_dev_versions(current)
66+
67+
print(f"Codebase kernels version : {current}")
68+
print(f"Next Python dev version : {python_dev}")
69+
print(f"Next Cargo dev version : {cargo_dev}")
70+
print()
71+
72+
if not args.dry_run and not args.yes:
73+
if not confirm(f"Bump all version sites to {python_dev} / {cargo_dev}?"):
74+
print("Aborted; no files changed.")
75+
return 1
76+
print()
77+
78+
changed: list[tuple[Path, str, str]] = []
79+
for path in PYPROJECT_FILES:
80+
old = replace_top_level_version(path, python_dev, dry_run=args.dry_run)
81+
if old is not None:
82+
changed.append((path, old, python_dev))
83+
for path in CARGO_FILES:
84+
old = replace_top_level_version(path, cargo_dev, dry_run=args.dry_run)
85+
if old is not None:
86+
changed.append((path, old, cargo_dev))
87+
88+
verb = "Would update" if args.dry_run else "Updated"
89+
if not changed:
90+
print("All files already at the target version; nothing to do.")
91+
return 0
92+
93+
print(f"{verb} {len(changed)} file(s):")
94+
for path, old, new in changed:
95+
print(f" {display_path(path)}: {old} -> {new}")
96+
97+
if not args.dry_run:
98+
print()
99+
print("Note: Cargo.lock and kernels/uv.lock are refreshed by the `bump-dev`")
100+
print("Makefile target; if you ran this script directly, regenerate them with")
101+
print("`cargo check --workspace` and `(cd kernels && uv lock)`.")
102+
103+
return 0
104+
105+
106+
if __name__ == "__main__":
107+
raise SystemExit(main())

scripts/pre_release.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/env python3
2+
"""Strip the development suffix from all version strings in the repo.
3+
4+
Reads the current ``kernels`` version from ``kernels/pyproject.toml`` (the
5+
source-of-truth in the codebase — no install required) and, assuming it is a
6+
development version like ``0.14.0.dev0``, rewrites every version site to the
7+
corresponding release: ``0.14.0``. The inverse of ``bump_to_dev.py``.
8+
9+
Example: codebase at ``0.14.0.dev0`` -> all sites become ``0.14.0``.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import argparse
15+
from pathlib import Path
16+
17+
from packaging.version import Version
18+
19+
from _version_common import (
20+
CARGO_FILES,
21+
PRIMARY_PYPROJECT,
22+
PYPROJECT_FILES,
23+
confirm,
24+
display_path,
25+
get_codebase_version,
26+
replace_top_level_version,
27+
)
28+
29+
30+
def next_release_version(current: Version) -> str:
31+
if (
32+
not current.is_devrelease
33+
or current.pre is not None
34+
or current.post is not None
35+
or current.local is not None
36+
):
37+
raise SystemExit(
38+
f"Codebase version `{current}` is not a development version "
39+
"(e.g. 0.14.0.dev0). This tool strips the dev suffix ahead of a "
40+
f"release. Set {display_path(PRIMARY_PYPROJECT)} to a dev version first."
41+
)
42+
43+
release = current.release
44+
major = release[0] if len(release) > 0 else 0
45+
minor = release[1] if len(release) > 1 else 0
46+
patch = release[2] if len(release) > 2 else 0
47+
return f"{major}.{minor}.{patch}"
48+
49+
50+
def main(argv: list[str] | None = None) -> int:
51+
parser = argparse.ArgumentParser(description=__doc__)
52+
parser.add_argument(
53+
"--dry-run",
54+
action="store_true",
55+
help="Show which files would change without writing them.",
56+
)
57+
parser.add_argument(
58+
"-y",
59+
"--yes",
60+
action="store_true",
61+
help="Skip the interactive confirmation prompt.",
62+
)
63+
args = parser.parse_args(argv)
64+
65+
current = get_codebase_version()
66+
release = next_release_version(current)
67+
68+
print(f"Codebase kernels version : {current}")
69+
print(f"Next release version : {release}")
70+
print()
71+
72+
if not args.dry_run and not args.yes:
73+
if not confirm(f"Strip dev suffix from all version sites -> {release}?"):
74+
print("Aborted; no files changed.")
75+
return 1
76+
print()
77+
78+
changed: list[tuple[Path, str, str]] = []
79+
for path in PYPROJECT_FILES:
80+
old = replace_top_level_version(path, release, dry_run=args.dry_run)
81+
if old is not None:
82+
changed.append((path, old, release))
83+
for path in CARGO_FILES:
84+
old = replace_top_level_version(path, release, dry_run=args.dry_run)
85+
if old is not None:
86+
changed.append((path, old, release))
87+
88+
verb = "Would update" if args.dry_run else "Updated"
89+
if not changed:
90+
print("All files already at the target version; nothing to do.")
91+
return 0
92+
93+
print(f"{verb} {len(changed)} file(s):")
94+
for path, old, new in changed:
95+
print(f" {display_path(path)}: {old} -> {new}")
96+
97+
if not args.dry_run:
98+
print()
99+
print("Note: Cargo.lock and kernels/uv.lock are refreshed by the `pre-release`")
100+
print("Makefile target; if you ran this script directly, regenerate them with")
101+
print("`cargo check --workspace` and `(cd kernels && uv lock)`.")
102+
103+
return 0
104+
105+
106+
if __name__ == "__main__":
107+
raise SystemExit(main())

0 commit comments

Comments
 (0)