diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 126f89fed..01910bfe2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -122,9 +122,7 @@ jobs: - name: Stamp dev source distribution metadata if: ${{ github.event_name == 'workflow_dispatch' && inputs.build_dev_matrix }} run: | - python scripts/release/set_dev_wheel_version.py \ - "${{ inputs.dev_version }}" \ - --package-name "${DEV_PACKAGE_NAME}" + python scripts/release/set_dev_wheel_version.py "${{ inputs.dev_version }}" - name: Build sdist run: uv run maturin sdist --out dist - name: Verify sdist license files @@ -197,9 +195,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && inputs.build_dev_matrix }} shell: bash run: | - python scripts/release/set_dev_wheel_version.py \ - "${{ inputs.dev_version }}" \ - --package-name "${DEV_PACKAGE_NAME}" + python scripts/release/set_dev_wheel_version.py "${{ inputs.dev_version }}" - name: Setup QEMU for Linux cross builds if: runner.os == 'Linux' && runner.arch != 'ARM64' && matrix.manylinux_arch == 'aarch64' uses: docker/setup-qemu-action@v3 @@ -314,9 +310,7 @@ jobs: - name: Stamp dev wheel metadata shell: bash run: | - python scripts/release/set_dev_wheel_version.py \ - "${{ inputs.dev_version }}" \ - --package-name "${DEV_PACKAGE_NAME}" + python scripts/release/set_dev_wheel_version.py "${{ inputs.dev_version }}" - name: Build manylinux x86_64 wheel shell: bash run: | diff --git a/INSTALLATION.md b/INSTALLATION.md index 4b88767f6..99298cdb4 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -4,6 +4,8 @@ Switchyard supports modular installation based on your use case. Install only th ## System Requirements +- Python 3.12 or newer. If the active interpreter is older, run + `uv pip install --python 3.12 nemo-switchyard`. - Linux x86_64 wheels require an x86-64-v3 / AVX2-class CPU (post 2013). - Linux aarch64 wheels require a Neoverse N1-class CPU (post 2020). diff --git a/README.md b/README.md index 751ad11c6..d84cae242 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ not already available, then install the published Switchyard tool: ```bash curl -LsSf https://astral.sh/uv/install.sh | sh source "$HOME/.local/bin/env" -uv tool install "nemo-switchyard[cli,server]" +uv tool install --python 3.12 "nemo-switchyard[cli,server]" ``` The coding agent you launch must also be installed and on your `PATH`. This does diff --git a/docs/cli_reference.md b/docs/cli_reference.md index dbe3a11cc..e6818519e 100644 --- a/docs/cli_reference.md +++ b/docs/cli_reference.md @@ -9,7 +9,7 @@ Switchyard has two command-line paths: ## Launcher Path: `switchyard launch` -Install the launcher with `uv tool install "nemo-switchyard[cli,server]"`. The +Install the launcher with `uv tool install --python 3.12 "nemo-switchyard[cli,server]"`. The selected coding agent must also be installed and available on `PATH`. ### Usage diff --git a/docs/getting_started.md b/docs/getting_started.md index a610ef28c..7519bada6 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -25,7 +25,7 @@ source "$HOME/.local/bin/env" Then install the published Switchyard tool: ```bash -uv tool install "nemo-switchyard[cli,server]" +uv tool install --python 3.12 "nemo-switchyard[cli,server]" ``` This creates an isolated Python tool environment containing the `switchyard` diff --git a/docs/internal/release_workflow.md b/docs/internal/release_workflow.md index b6a44b7fa..8be709d32 100644 --- a/docs/internal/release_workflow.md +++ b/docs/internal/release_workflow.md @@ -91,7 +91,7 @@ To preview the metadata stamp locally: ```bash python scripts/release/set_dev_wheel_version.py 0.0.1.dev0 --print-version -python scripts/release/set_dev_wheel_version.py 0.0.1.dev0 --package-name nemo-switchyard +python scripts/release/set_dev_wheel_version.py 0.0.1.dev0 ``` Do not commit the stamped package metadata unless the release process explicitly requires it. diff --git a/scripts/release/set_dev_wheel_version.py b/scripts/release/set_dev_wheel_version.py index cfa8e7796..d4194b559 100644 --- a/scripts/release/set_dev_wheel_version.py +++ b/scripts/release/set_dev_wheel_version.py @@ -4,28 +4,16 @@ """Stamp temporary Python package metadata for dev wheel artifact builds.""" -from __future__ import annotations - import argparse -import dataclasses import re import sys from pathlib import Path DEV_VERSION_RE = re.compile(r"^(?P\d+\.\d+\.\d+)\.dev(?P\d*)$") -PACKAGE_NAME_RE = re.compile(r'^(name\s*=\s*")([^"]+)(".*)$') PACKAGE_VERSION_RE = re.compile(r'^(version\s*=\s*")([^"]+)(".*)$') -PYTHON_VERSION_RE = re.compile(r'^(__version__\s*=\s*")([^"]+)(".*)$', re.MULTILINE) - - -@dataclasses.dataclass(frozen=True) -class DevWheelVersion: - """Normalized metadata used for a short-lived dev wheel artifact build.""" - version: str - -def parse_dev_wheel_version(version: str) -> DevWheelVersion: +def parse_dev_wheel_version(version: str) -> str: """Return the normalized PEP 440 `.dev` version or raise `ValueError`.""" match = DEV_VERSION_RE.fullmatch(version) @@ -33,16 +21,15 @@ def parse_dev_wheel_version(version: str) -> DevWheelVersion: raise ValueError("dev wheel versions must look like 0.0.1.dev0") number = match.group("number") or "0" - return DevWheelVersion(version=f"{match.group('release')}.dev{number}") + return f"{match.group('release')}.dev{number}" -def update_pyproject(path: Path, *, package_name: str, version: str) -> bool: - """Set `[project]` name and version in `pyproject.toml`.""" +def update_pyproject(path: Path, version: str) -> bool: + """Set `[project].version` in `pyproject.toml`.""" lines = path.read_text().splitlines(keepends=True) in_project = False changed = False - found_name = False found_version = False output: list[str] = [] @@ -53,9 +40,6 @@ def update_pyproject(path: Path, *, package_name: str, version: str) -> bool: updated = line if in_project: - updated, count = PACKAGE_NAME_RE.subn(rf"\g<1>{package_name}\g<3>", updated, count=1) - if count: - found_name = True updated, count = PACKAGE_VERSION_RE.subn(rf"\g<1>{version}\g<3>", updated, count=1) if count: found_version = True @@ -63,8 +47,6 @@ def update_pyproject(path: Path, *, package_name: str, version: str) -> bool: changed = changed or updated != line output.append(updated) - if not found_name: - raise ValueError(f"{path}: missing [project] name") if not found_version: raise ValueError(f"{path}: missing [project] version") if changed: @@ -72,43 +54,16 @@ def update_pyproject(path: Path, *, package_name: str, version: str) -> bool: return changed -def update_python_init(path: Path, version: str) -> bool: - """Set `switchyard.__version__` for the dev wheel artifact.""" - - text = path.read_text() - updated, count = PYTHON_VERSION_RE.subn(rf"\g<1>{version}\g<3>", text, count=1) - if count != 1: - raise ValueError(f"{path}: missing __version__") - if updated != text: - path.write_text(updated) - return True - return False +def apply_version(version: str) -> None: + """Set the wheel version in `pyproject.toml`.""" - -def apply_version(version: DevWheelVersion, *, package_name: str) -> None: - """Update package metadata files used by maturin wheel builds.""" - - changes = [ - ( - "pyproject.toml", - update_pyproject( - Path("pyproject.toml"), - package_name=package_name, - version=version.version, - ), - ), - ("switchyard/__init__.py", update_python_init(Path("switchyard/__init__.py"), version.version)), - ] - - changed = [path for path, did_change in changes if did_change] + changed = update_pyproject(Path("pyproject.toml"), version) if changed: print("Set dev wheel metadata:") - print(f" Package: {package_name}") - print(f" Version: {version.version}") - for path in changed: - print(f" updated {path}") + print(f" Version: {version}") + print(" updated pyproject.toml") else: - print(f"dev wheel metadata already set for {package_name} {version.version}") + print(f"dev wheel metadata already set to {version}") def main(argv: list[str] | None = None) -> int: @@ -116,11 +71,6 @@ def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("version", help="PEP 440 .dev version, such as 0.0.1.dev0") - parser.add_argument( - "--package-name", - default="nemo-switchyard", - help="Distribution name to stamp into wheel metadata", - ) parser.add_argument( "--print-version", action="store_true", @@ -131,9 +81,9 @@ def main(argv: list[str] | None = None) -> int: try: version = parse_dev_wheel_version(args.version) if args.print_version: - print(version.version) + print(version) return 0 - apply_version(version, package_name=args.package_name) + apply_version(version) except ValueError as exc: print(f"error: {exc}", file=sys.stderr) return 2 diff --git a/switchyard/__init__.py b/switchyard/__init__.py index 83527c707..fdedd8bad 100644 --- a/switchyard/__init__.py +++ b/switchyard/__init__.py @@ -9,6 +9,7 @@ format translation, and extensible middleware. """ +from importlib import metadata as _metadata from typing import TYPE_CHECKING, Any from switchyard.lib.backends import ( @@ -190,4 +191,8 @@ def __getattr__(name: str) -> Any: "AnyResponseStream", ] -__version__ = "0.1.0" +try: + __version__ = _metadata.version("nemo-switchyard") +except _metadata.PackageNotFoundError: + # A source checkout may not have installed distribution metadata. + __version__ = "0.0.0+unknown" diff --git a/switchyard/cli/switchyard_cli.py b/switchyard/cli/switchyard_cli.py index 05e7dacce..67279700d 100644 --- a/switchyard/cli/switchyard_cli.py +++ b/switchyard/cli/switchyard_cli.py @@ -4,13 +4,11 @@ """Switchyard command-line entry point.""" -from __future__ import annotations - import argparse import logging import os -from importlib.metadata import PackageNotFoundError, version +from switchyard import __version__ from switchyard.cli.command_utils import ( quiet_dependency_loggers as _quiet_dependency_loggers, ) @@ -71,19 +69,8 @@ def _cmd_serve(args: argparse.Namespace) -> None: ) -def _switchyard_version() -> str: - """Resolve the installed distribution version.""" - - try: - return version("nemo-switchyard") - except PackageNotFoundError: - from switchyard import __version__ - - return __version__ - - def _add_launch_parser( - subparsers: argparse._SubParsersAction[argparse.ArgumentParser], + subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]", ) -> None: launch = subparsers.add_parser( "launch", @@ -125,7 +112,7 @@ def _build_parser() -> argparse.ArgumentParser: parser.add_argument( "--version", action="version", - version=f"%(prog)s {_switchyard_version()}", + version=f"%(prog)s {__version__}", ) subparsers = parser.add_subparsers(dest="command") diff --git a/tests/getting_started/test_getting_started.py b/tests/getting_started/test_getting_started.py index 7a6b3c1e6..e8ba4581f 100644 --- a/tests/getting_started/test_getting_started.py +++ b/tests/getting_started/test_getting_started.py @@ -12,7 +12,7 @@ def test_getting_started_documents_current_paths() -> None: ).read_text() assert guide.index("## Launcher Path") < guide.index("## Server Path") - assert 'uv tool install "nemo-switchyard[cli,server]"' in guide + assert 'uv tool install --python 3.12 "nemo-switchyard[cli,server]"' in guide assert "switchyard launch claude --model switchyard" in guide assert "cargo build --locked --release -p switchyard-server" in guide assert "./target/release/switchyard-server --config routes.toml --dry-run" in guide diff --git a/tests/readme/test_readme.py b/tests/readme/test_readme.py index 7c5b1a3ee..3aebc93c2 100644 --- a/tests/readme/test_readme.py +++ b/tests/readme/test_readme.py @@ -10,7 +10,7 @@ def test_readme_documents_current_paths() -> None: readme = (Path(__file__).resolve().parents[2] / "README.md").read_text() assert readme.index("### Launcher Path") < readme.index("### Server Path") - assert 'uv tool install "nemo-switchyard[cli,server]"' in readme + assert 'uv tool install --python 3.12 "nemo-switchyard[cli,server]"' in readme assert "switchyard launch claude --model switchyard" in readme assert "cargo build --locked --release -p switchyard-server" in readme assert "./target/release/switchyard-server --config routes.toml --dry-run" in readme diff --git a/tests/test_cli_version.py b/tests/test_cli_version.py index ddcd17052..44bde1c43 100644 --- a/tests/test_cli_version.py +++ b/tests/test_cli_version.py @@ -3,14 +3,10 @@ """Tests for the top-level ``switchyard --version`` flag.""" -from __future__ import annotations - -from importlib.metadata import PackageNotFoundError, version -from unittest.mock import patch - import pytest -from switchyard.cli.switchyard_cli import _build_parser, _switchyard_version +from switchyard import __version__ +from switchyard.cli.switchyard_cli import _build_parser def test_version_flag_prints_version_and_exits(capsys: pytest.CaptureFixture[str]) -> None: @@ -20,18 +16,4 @@ def test_version_flag_prints_version_and_exits(capsys: pytest.CaptureFixture[str parser.parse_args(["--version"]) assert exc.value.code == 0 - assert capsys.readouterr().out.strip() == f"switchyard {_switchyard_version()}" - - -def test_switchyard_version_matches_installed_metadata() -> None: - assert _switchyard_version() == version("nemo-switchyard") - - -def test_switchyard_version_falls_back_to_dunder_when_uninstalled() -> None: - from switchyard import __version__ - - with patch( - "switchyard.cli.switchyard_cli.version", - side_effect=PackageNotFoundError("nemo-switchyard"), - ): - assert _switchyard_version() == __version__ + assert capsys.readouterr().out.strip() == f"switchyard {__version__}" diff --git a/tests/test_dev_wheel_versioning.py b/tests/test_dev_wheel_versioning.py index b17feb275..3f5501379 100644 --- a/tests/test_dev_wheel_versioning.py +++ b/tests/test_dev_wheel_versioning.py @@ -1,8 +1,6 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -from __future__ import annotations - import importlib.util import sys from pathlib import Path @@ -29,7 +27,7 @@ def test_parse_dev_wheel_version(raw_version: str, normalized: str) -> None: version = set_dev_wheel_version.parse_dev_wheel_version(raw_version) - assert version.version == normalized + assert version == normalized @pytest.mark.parametrize( @@ -50,18 +48,8 @@ def test_parse_dev_wheel_version_rejects_non_dev_versions(raw_version: str) -> N def test_metadata_file_updates(tmp_path: Path) -> None: pyproject = tmp_path / "pyproject.toml" pyproject.write_text( - '[build-system]\nrequires = []\n\n[project]\nname = "switchyard"\nversion = "0.1.0"\n' - ) - init = tmp_path / "__init__.py" - init.write_text('__all__ = []\n\n__version__ = "0.1.0"\n') - - assert set_dev_wheel_version.update_pyproject( - pyproject, - package_name="nemo-switchyard", - version="0.0.1.dev0", + '[build-system]\nrequires = []\n\n[project]\nname = "nemo-switchyard"\nversion = "0.1.0"\n' ) - assert set_dev_wheel_version.update_python_init(init, "0.0.1.dev0") + assert set_dev_wheel_version.update_pyproject(pyproject, "0.0.1.dev0") - assert 'name = "nemo-switchyard"' in pyproject.read_text() assert 'version = "0.0.1.dev0"' in pyproject.read_text() - assert '__version__ = "0.0.1.dev0"' in init.read_text() diff --git a/tests/test_version_package_name.py b/tests/test_version_package_name.py new file mode 100644 index 000000000..9add4a904 --- /dev/null +++ b/tests/test_version_package_name.py @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from importlib.metadata import version + +import switchyard + + +def test_dunder_version_matches_installed_metadata() -> None: + assert switchyard.__version__ == version("nemo-switchyard")