From 06b5b2ea7cfff51ec58fc20f82b42dc5df18e3c2 Mon Sep 17 00:00:00 2001 From: Hannes Vogt Date: Thu, 26 Mar 2026 15:48:24 +0100 Subject: [PATCH 1/3] Script to bump all package versions --- scripts/bump_version.py | 137 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 scripts/bump_version.py diff --git a/scripts/bump_version.py b/scripts/bump_version.py new file mode 100644 index 0000000000..c9d7f4329f --- /dev/null +++ b/scripts/bump_version.py @@ -0,0 +1,137 @@ +# ICON4Py - ICON inspired code in Python and GT4Py +# +# Copyright (c) 2022-2024, ETH Zurich and MeteoSwiss +# All rights reserved. +# +# Please, refer to the LICENSE file in the root directory. +# SPDX-License-Identifier: BSD-3-Clause + +"""Bump the version of all icon4py namespace packages to a new version.""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path +from typing import Annotated, Final + +import typer + +from scripts._common import REPO_ROOT + + +def _find_versioned_package_dirs() -> list[Path]: + """Return directories of all pyproject.toml files in the repo that have a + ``[tool.bumpversion]`` section, sorted with the repo root last.""" + dirs = [] + for pyproject in sorted(REPO_ROOT.rglob("pyproject.toml")): + if ".venv" in pyproject.parts: + continue + if "[tool.bumpversion]" in pyproject.read_text(): + dirs.append(pyproject.parent) + # Ensure repo root (if present) comes last so sub-packages bump first + if REPO_ROOT in dirs: + dirs.remove(REPO_ROOT) + dirs.append(REPO_ROOT) + return dirs + + +#: Pattern matching versioned icon4py cross-package dependency constraints, e.g. +#: ``icon4py-common>=0.0.6`` or ``icon4py-tools>=0.0.6``. +_ICON4PY_DEP_CONSTRAINT_RE: Final = re.compile(r"(icon4py-[\w-]+)>=([\d]+\.[\d]+\.[\d]+)") + + +cli = typer.Typer(no_args_is_help=True, help=__doc__) + + +def _detect_current_version(pkg_dirs: list[Path]) -> str: + """Read the current version from the first versioned package found.""" + for pkg_dir in pkg_dirs: + pyproject = pkg_dir / "pyproject.toml" + text = pyproject.read_text() + m = re.search(r"^# managed by bump-my-version:\nversion = \"([\d.]+)\"", text, re.MULTILINE) + if m: + return m.group(1) + raise RuntimeError("Could not detect current version from any namespace package.") + + +def _bump_package(pkg_dir: Path, new_version: str, dry_run: bool, verbose: bool) -> None: + pyproject = pkg_dir / "pyproject.toml" + if not pyproject.exists(): + typer.echo(f" [skip] no pyproject.toml in {pkg_dir}", err=True) + return + + cmd = [ + sys.executable, + "-m", + "bumpversion", + "bump", + "--new-version", + new_version, + "--allow-dirty", + "--config-file", + str(pyproject), + ] + if dry_run: + cmd.append("--dry-run") + if verbose: + cmd.extend(["-v", "-v"]) + + typer.echo(f" Bumping {pkg_dir.relative_to(REPO_ROOT)} → {new_version}") + result = subprocess.run(cmd, cwd=pkg_dir, capture_output=not verbose, check=False) + if result.returncode != 0: + err = result.stderr.decode() if result.stderr else "" + raise typer.Exit( + typer.echo(f" [ERROR] bump-my-version failed for {pkg_dir}:\n{err}", err=True) or 1 + ) + + +def _update_cross_package_constraints( + current_version: str, new_version: str, dry_run: bool +) -> None: + """Replace ``icon4py-*>={current_version}`` with ``icon4py-*>={new_version}`` + in every pyproject.toml across the repo (excluding .venv).""" + for pyproject in sorted(REPO_ROOT.rglob("pyproject.toml")): + # Skip anything inside the virtual environment + if ".venv" in pyproject.parts: + continue + text = pyproject.read_text() + new_text = _ICON4PY_DEP_CONSTRAINT_RE.sub( + lambda m: f"{m.group(1)}>={new_version}" + if m.group(2) == current_version + else m.group(0), + text, + ) + if new_text != text: + rel = pyproject.relative_to(REPO_ROOT) + typer.echo(f" Updating cross-package constraints in {rel}") + if not dry_run: + pyproject.write_text(new_text) + + +@cli.command() +def bump( + new_version: Annotated[str, typer.Argument(help="Target version, e.g. '0.1.0'")], + dry_run: Annotated[ + bool, typer.Option("--dry-run", help="Show what would change without writing files") + ] = False, + verbose: Annotated[ + bool, typer.Option("--verbose", "-v", help="Pass verbose flag to bump-my-version") + ] = False, +) -> None: + """Bump all namespace packages to NEW_VERSION and update cross-package constraints.""" + pkg_dirs = _find_versioned_package_dirs() + current_version = _detect_current_version(pkg_dirs) + typer.echo(f"Bumping all packages: {current_version} → {new_version}") + if dry_run: + typer.echo(" (dry-run mode — no files will be written)") + + typer.echo("\n[1/2] Running bump-my-version for each package:") + for pkg_dir in pkg_dirs: + _bump_package(pkg_dir, new_version, dry_run=dry_run, verbose=verbose) + + typer.echo("\n[2/2] Updating cross-package dependency constraints:") + _update_cross_package_constraints(current_version, new_version, dry_run=dry_run) + + typer.echo("\nDone.") From a3eb6638015e3a34e331da30f3bec8f90889ab11 Mon Sep 17 00:00:00 2001 From: Hannes Vogt Date: Thu, 26 Mar 2026 16:08:28 +0100 Subject: [PATCH 2/3] fix dependency update --- scripts/bump_version.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/bump_version.py b/scripts/bump_version.py index c9d7f4329f..ab507297a3 100644 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -39,7 +39,9 @@ def _find_versioned_package_dirs() -> list[Path]: #: Pattern matching versioned icon4py cross-package dependency constraints, e.g. #: ``icon4py-common>=0.0.6`` or ``icon4py-tools>=0.0.6``. -_ICON4PY_DEP_CONSTRAINT_RE: Final = re.compile(r"(icon4py-[\w-]+)>=([\d]+\.[\d]+\.[\d]+)") +_ICON4PY_DEP_CONSTRAINT_RE: Final = re.compile( + r"(icon4py-[\w-]+(?:\[[\w,]+\])?)>=([\d]+\.[\d]+\.[\d]+)" +) cli = typer.Typer(no_args_is_help=True, help=__doc__) From c6dd4f5db51b1f340379c2f5538151b188353f54 Mon Sep 17 00:00:00 2001 From: Hannes Vogt Date: Thu, 26 Mar 2026 17:23:16 +0100 Subject: [PATCH 3/3] support ~= --- scripts/bump_version.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/bump_version.py b/scripts/bump_version.py index ab507297a3..5c5688be23 100644 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -38,9 +38,9 @@ def _find_versioned_package_dirs() -> list[Path]: #: Pattern matching versioned icon4py cross-package dependency constraints, e.g. -#: ``icon4py-common>=0.0.6`` or ``icon4py-tools>=0.0.6``. +#: ``icon4py-common>=0.0.6``, ``icon4py-common~=0.0.6``, or ``icon4py-tools~=0.0.6``. _ICON4PY_DEP_CONSTRAINT_RE: Final = re.compile( - r"(icon4py-[\w-]+(?:\[[\w,]+\])?)>=([\d]+\.[\d]+\.[\d]+)" + r"(icon4py-[\w-]+(?:\[[\w,]+\])?)(~=|>=)([\d]+\.[\d]+\.[\d]+)" ) @@ -92,7 +92,7 @@ def _bump_package(pkg_dir: Path, new_version: str, dry_run: bool, verbose: bool) def _update_cross_package_constraints( current_version: str, new_version: str, dry_run: bool ) -> None: - """Replace ``icon4py-*>={current_version}`` with ``icon4py-*>={new_version}`` + """Replace ``icon4py-*{op}{current_version}`` with ``icon4py-*{op}{new_version}`` in every pyproject.toml across the repo (excluding .venv).""" for pyproject in sorted(REPO_ROOT.rglob("pyproject.toml")): # Skip anything inside the virtual environment @@ -100,8 +100,8 @@ def _update_cross_package_constraints( continue text = pyproject.read_text() new_text = _ICON4PY_DEP_CONSTRAINT_RE.sub( - lambda m: f"{m.group(1)}>={new_version}" - if m.group(2) == current_version + lambda m: f"{m.group(1)}{m.group(2)}{new_version}" + if m.group(3) == current_version else m.group(0), text, )