diff --git a/justfile b/justfile index 716854825..07de730e4 100644 --- a/justfile +++ b/justfile @@ -245,58 +245,7 @@ set_python_project_versions() { local python_executable="" version="$(semver_to_pep440 "$1")" python_executable="$(uv_python_executable)" - - "$python_executable" - "$version" <<'PY' -from pathlib import Path -import re -import sys -import tomllib - -version = sys.argv[1] -project_paths = ( - Path("pyproject.toml"), - *sorted(Path("adapters").glob("**/pyproject.toml")), -) -pin_pattern = re.compile(r'(nemo-fabric-[a-z0-9-]+\s*==\s*)([^"\s,;]+)') - -for path in project_paths: - text = path.read_text() - updated, count = re.subn( - r'^version\s*=\s*"[^"]+"$', - f'version = "{version}"', - text, - count=1, - flags=re.MULTILINE, - ) - if count != 1: - raise SystemExit(f"Failed to find exactly one project version in {path}") - updated = pin_pattern.sub(rf"\g<1>{version}", updated) - if updated != text: - path.write_text(updated) - print(f"{path} version and internal pins updated to {version}") - else: - print(f"{path} already set to {version}") - -runtime_path = Path("python/pyproject.toml") -runtime = tomllib.loads(runtime_path.read_text()) -project = runtime.get("project", {}) -if "version" in project or "version" not in project.get("dynamic", []): - raise SystemExit( - "python/pyproject.toml must keep a dynamic version derived from Cargo.toml" - ) - -mismatched_pins = [] -for path in project_paths: - for match in pin_pattern.finditer(path.read_text()): - if match.group(2) != version: - mismatched_pins.append(f"{path}: {match.group(0)}") -if mismatched_pins: - raise SystemExit( - "Internal Python dependency pins are not synchronized: " - + ", ".join(mismatched_pins) - ) -print("python/pyproject.toml continues to derive its version from Cargo.toml") -PY + "$python_executable" scripts/ci/set_python_project_versions.py "$version" } set_project_version() { diff --git a/scripts/ci/set_python_project_versions.py b/scripts/ci/set_python_project_versions.py new file mode 100644 index 000000000..0d09d8cb5 --- /dev/null +++ b/scripts/ci/set_python_project_versions.py @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import re +import sys +import tomllib +from pathlib import Path + + +PROJECT_VERSION_PATTERN = re.compile(r'^version\s*=\s*"[^"]+"$', flags=re.MULTILINE) +INTERNAL_PIN_PATTERN = re.compile( + r"(?Pnemo-fabric-[a-z0-9-]+(?:\[[^\]]+\])?\s*==\s*)" + r'(?P[^"\s,;]+)' +) + + +def set_python_project_versions(root: Path, version: str) -> None: + project_paths = ( + root / "pyproject.toml", + *sorted((root / "adapters").glob("**/pyproject.toml")), + ) + + for path in project_paths: + text = path.read_text(encoding="utf-8") + updated, count = PROJECT_VERSION_PATTERN.subn( + f'version = "{version}"', + text, + count=1, + ) + if count != 1: + raise SystemExit(f"Failed to find exactly one project version in {path}") + updated = INTERNAL_PIN_PATTERN.sub( + lambda match: f"{match.group('prefix')}{version}", + updated, + ) + if updated != text: + path.write_text(updated, encoding="utf-8") + print( + f"{path.relative_to(root)} version and internal pins updated to {version}" + ) + else: + print(f"{path.relative_to(root)} already set to {version}") + + runtime_path = root / "python" / "pyproject.toml" + runtime = tomllib.loads(runtime_path.read_text(encoding="utf-8")) + project = runtime.get("project", {}) + if "version" in project or "version" not in project.get("dynamic", []): + raise SystemExit( + "python/pyproject.toml must keep a dynamic version derived from Cargo.toml" + ) + + mismatched_pins = [] + for path in project_paths: + for match in INTERNAL_PIN_PATTERN.finditer(path.read_text(encoding="utf-8")): + if match.group("version") != version: + mismatched_pins.append(f"{path.relative_to(root)}: {match.group(0)}") + if mismatched_pins: + raise SystemExit( + "Internal Python dependency pins are not synchronized: " + + ", ".join(mismatched_pins) + ) + print("python/pyproject.toml continues to derive its version from Cargo.toml") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + raise SystemExit("Usage: set_python_project_versions.py ") + set_python_project_versions(Path.cwd(), sys.argv[1]) diff --git a/tests/scripts/test_set_python_project_versions.py b/tests/scripts/test_set_python_project_versions.py new file mode 100644 index 000000000..728a6492d --- /dev/null +++ b/tests/scripts/test_set_python_project_versions.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import sys +import tomllib +from pathlib import Path + + +CI_SCRIPTS = Path(__file__).resolve().parents[2] / "scripts" / "ci" +sys.path.insert(0, str(CI_SCRIPTS)) + +import set_python_project_versions # noqa: E402 + + +def test_set_python_project_versions_updates_internal_pins_with_extras( + tmp_path: Path, +): + (tmp_path / "adapters" / "claude").mkdir(parents=True) + (tmp_path / "python").mkdir() + (tmp_path / "pyproject.toml").write_text( + """\ +[project] +name = "nemo-fabric" +version = "0.2.0" +dependencies = [ + "nemo-fabric-runtime == 0.2.0", +] + +[project.optional-dependencies] +claude = [ + "nemo-fabric-adapters-claude[harness] == 0.2.0", +] +hermes-agent = [ + "nemo-fabric-adapters-hermes[harness] == 0.2.0; python_version < '3.14'", +] +""", + encoding="utf-8", + ) + (tmp_path / "adapters" / "claude" / "pyproject.toml").write_text( + """\ +[project] +name = "nemo-fabric-adapters-claude" +version = "0.2.0" +dependencies = [ + "nemo-fabric-adapters-common == 0.2.0", +] +""", + encoding="utf-8", + ) + runtime_path = tmp_path / "python" / "pyproject.toml" + runtime_path.write_text( + """\ +[project] +name = "nemo-fabric-runtime" +dynamic = ["version"] +""", + encoding="utf-8", + ) + + set_python_project_versions.set_python_project_versions(tmp_path, "0.2.0rc5") + + root_project = tomllib.loads( + (tmp_path / "pyproject.toml").read_text(encoding="utf-8") + )["project"] + adapter_project = tomllib.loads( + (tmp_path / "adapters" / "claude" / "pyproject.toml").read_text( + encoding="utf-8" + ) + )["project"] + + assert root_project["version"] == "0.2.0rc5" + assert root_project["dependencies"] == ["nemo-fabric-runtime == 0.2.0rc5"] + assert root_project["optional-dependencies"]["claude"] == [ + "nemo-fabric-adapters-claude[harness] == 0.2.0rc5" + ] + assert root_project["optional-dependencies"]["hermes-agent"] == [ + "nemo-fabric-adapters-hermes[harness] == 0.2.0rc5; python_version < '3.14'" + ] + assert adapter_project["version"] == "0.2.0rc5" + assert adapter_project["dependencies"] == [ + "nemo-fabric-adapters-common == 0.2.0rc5" + ] + assert tomllib.loads(runtime_path.read_text(encoding="utf-8"))["project"][ + "dynamic" + ] == ["version"]