Skip to content
Merged
24 changes: 23 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: style kernel-builder-cli-docs quality pin-actions
.PHONY: style kernel-builder-cli-docs quality bump-dev bump-dev-dry-run pre-release pre-release-dry-run pin-actions


export check_dirs := kernels/src kernels/tests kernels-data/bindings/python
Expand Down Expand Up @@ -27,3 +27,25 @@ pin-actions:
quality:
ruff format --check ${check_dirs}
ruff check ${check_dirs}

# Bump every version site to the next dev release based on the currently
# installed `kernels` package version (e.g. installed 0.13.0 -> 0.14.0.dev0).
# Refreshes Cargo.lock and kernels/uv.lock so all sites stay consistent.
bump-dev:
python scripts/bump_to_dev.py
cargo check --workspace
cd kernels && uv lock
Comment thread
sayakpaul marked this conversation as resolved.

bump-dev-dry-run:
python scripts/bump_to_dev.py --dry-run

# Strip the `.dev0` / `-dev0` suffix from every version site in prep for a
# release (e.g. codebase 0.14.0.dev0 -> 0.14.0). Refreshes Cargo.lock and
# kernels/uv.lock so all sites stay consistent.
pre-release:
python scripts/pre_release.py
cargo check --workspace
cd kernels && uv lock

pre-release-dry-run:
python scripts/pre_release.py --dry-run
2 changes: 1 addition & 1 deletion docs/source/api/kernels.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@

### get_locked_kernel

[[autodoc]] kernels.get_locked_kernel
[[autodoc]] kernels.get_locked_kernel
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why it's here.

1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
pinact
pkg-config
rust
uv # For `make bump-dev` / `make pre-release` to refresh kernels/uv.lock.
];
buildInputs = [
black
Expand Down
74 changes: 74 additions & 0 deletions scripts/_version_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Shared helpers for bump_to_dev.py and pre_release.py.

Both scripts rewrite the same set of version sites; only the version-computation
step differs. Constants, I/O helpers, and the interactive prompt live here.
"""

from __future__ import annotations

from pathlib import Path

import tomlkit
from packaging.version import Version

REPO_ROOT = Path(__file__).resolve().parent.parent

PRIMARY_PYPROJECT = REPO_ROOT / "kernels" / "pyproject.toml"

PYPROJECT_FILES = [PRIMARY_PYPROJECT]

CARGO_FILES = [
REPO_ROOT / "kernels-data" / "Cargo.toml",
REPO_ROOT / "kernels-data" / "bindings" / "python" / "Cargo.toml",
REPO_ROOT / "kernel-builder" / "Cargo.toml",
REPO_ROOT / "kernel-abi-check" / "kernel-abi-check" / "Cargo.toml",
REPO_ROOT / "kernel-abi-check" / "bindings" / "python" / "Cargo.toml",
]


def display_path(path: Path) -> Path:
try:
return path.relative_to(REPO_ROOT)
except ValueError:
return path


def _version_table(doc, path: Path):
"""Return the [project] or [package] table holding the top-level ``version``.

pyproject.toml stores it under ``[project]``; Cargo.toml under ``[package]``.
"""
for name in ("project", "package"):
table = doc.get(name)
if table is not None and "version" in table:
return table
raise SystemExit(
f"Could not find a top-level `version` under [project] or [package] in {display_path(path)}."
)


def get_codebase_version() -> Version:
doc = tomlkit.parse(PRIMARY_PYPROJECT.read_text())
return Version(str(_version_table(doc, PRIMARY_PYPROJECT)["version"]))


def replace_top_level_version(path: Path, new_version: str, *, dry_run: bool) -> str | None:
doc = tomlkit.parse(path.read_text())
table = _version_table(doc, path)

old_version = str(table["version"])
if old_version == new_version:
return None

table["version"] = new_version
if not dry_run:
path.write_text(tomlkit.dumps(doc))
return old_version


def confirm(prompt: str) -> bool:
try:
answer = input(f"{prompt} [y/N]: ").strip().lower()
except EOFError:
return False
return answer in ("y", "yes")
107 changes: 107 additions & 0 deletions scripts/bump_to_dev.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Bump all version strings in the repo to the next development version.

Reads the current ``kernels`` version from ``kernels/pyproject.toml`` (the
source-of-truth in the codebase — no install required).

Example: codebase at ``0.13.0`` -> Python sites get ``0.14.0.dev0`` (PEP 440)
and Cargo sites get ``0.14.0-dev0``.
"""

from __future__ import annotations

import argparse
from pathlib import Path

from packaging.version import Version

from _version_common import (
CARGO_FILES,
PRIMARY_PYPROJECT,
PYPROJECT_FILES,
confirm,
display_path,
get_codebase_version,
replace_top_level_version,
)


def next_dev_versions(current: Version) -> tuple[str, str]:
if current.is_prerelease or current.is_postrelease or current.local is not None:
raise SystemExit(
f"Codebase version `{current}` is not a plain release (e.g. 0.13.0). "
"This tool bumps from a release to the next dev cycle. Set "
f"{display_path(PRIMARY_PYPROJECT)} to a release version first."
)

release = current.release
if len(release) < 2:
raise SystemExit(
f"Codebase version `{current}` is missing a minor component; "
"expected at least MAJOR.MINOR (e.g. 0.13.0)."
)

major, minor = release[0], release[1]
next_minor = f"{major}.{minor + 1}.0"
return f"{next_minor}.dev0", f"{next_minor}-dev0"


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show which files would change without writing them.",
)
parser.add_argument(
"-y",
"--yes",
action="store_true",
help="Skip the interactive confirmation prompt.",
)
args = parser.parse_args(argv)

current = get_codebase_version()
python_dev, cargo_dev = next_dev_versions(current)

print(f"Codebase kernels version : {current}")
print(f"Next Python dev version : {python_dev}")
print(f"Next Cargo dev version : {cargo_dev}")
print()

if not args.dry_run and not args.yes:
if not confirm(f"Bump all version sites to {python_dev} / {cargo_dev}?"):
print("Aborted; no files changed.")
return 1
print()

changed: list[tuple[Path, str, str]] = []
for path in PYPROJECT_FILES:
old = replace_top_level_version(path, python_dev, dry_run=args.dry_run)
if old is not None:
changed.append((path, old, python_dev))
for path in CARGO_FILES:
old = replace_top_level_version(path, cargo_dev, dry_run=args.dry_run)
if old is not None:
changed.append((path, old, cargo_dev))

verb = "Would update" if args.dry_run else "Updated"
if not changed:
print("All files already at the target version; nothing to do.")
return 0

print(f"{verb} {len(changed)} file(s):")
for path, old, new in changed:
print(f" {display_path(path)}: {old} -> {new}")

if not args.dry_run:
print()
print("Note: Cargo.lock and kernels/uv.lock are refreshed by the `bump-dev`")
print("Makefile target; if you ran this script directly, regenerate them with")
print("`cargo check --workspace` and `(cd kernels && uv lock)`.")

return 0


if __name__ == "__main__":
raise SystemExit(main())
107 changes: 107 additions & 0 deletions scripts/pre_release.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Strip the development suffix from all version strings in the repo.

Reads the current ``kernels`` version from ``kernels/pyproject.toml`` (the
source-of-truth in the codebase — no install required) and, assuming it is a
development version like ``0.14.0.dev0``, rewrites every version site to the
corresponding release: ``0.14.0``. The inverse of ``bump_to_dev.py``.

Example: codebase at ``0.14.0.dev0`` -> all sites become ``0.14.0``.
"""

from __future__ import annotations

import argparse
from pathlib import Path

from packaging.version import Version

from _version_common import (
CARGO_FILES,
PRIMARY_PYPROJECT,
PYPROJECT_FILES,
confirm,
display_path,
get_codebase_version,
replace_top_level_version,
)


def next_release_version(current: Version) -> str:
if (
not current.is_devrelease
or current.pre is not None
or current.post is not None
or current.local is not None
):
raise SystemExit(
f"Codebase version `{current}` is not a development version "
"(e.g. 0.14.0.dev0). This tool strips the dev suffix ahead of a "
f"release. Set {display_path(PRIMARY_PYPROJECT)} to a dev version first."
)

release = current.release
major = release[0] if len(release) > 0 else 0
minor = release[1] if len(release) > 1 else 0
patch = release[2] if len(release) > 2 else 0
return f"{major}.{minor}.{patch}"


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show which files would change without writing them.",
)
parser.add_argument(
"-y",
"--yes",
action="store_true",
help="Skip the interactive confirmation prompt.",
)
args = parser.parse_args(argv)

current = get_codebase_version()
release = next_release_version(current)

print(f"Codebase kernels version : {current}")
print(f"Next release version : {release}")
print()

if not args.dry_run and not args.yes:
if not confirm(f"Strip dev suffix from all version sites -> {release}?"):
print("Aborted; no files changed.")
return 1
print()

changed: list[tuple[Path, str, str]] = []
for path in PYPROJECT_FILES:
old = replace_top_level_version(path, release, dry_run=args.dry_run)
if old is not None:
changed.append((path, old, release))
for path in CARGO_FILES:
old = replace_top_level_version(path, release, dry_run=args.dry_run)
if old is not None:
changed.append((path, old, release))

verb = "Would update" if args.dry_run else "Updated"
if not changed:
print("All files already at the target version; nothing to do.")
return 0

print(f"{verb} {len(changed)} file(s):")
for path, old, new in changed:
print(f" {display_path(path)}: {old} -> {new}")

if not args.dry_run:
print()
print("Note: Cargo.lock and kernels/uv.lock are refreshed by the `pre-release`")
print("Makefile target; if you ran this script directly, regenerate them with")
print("`cargo check --workspace` and `(cd kernels && uv lock)`.")

return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading