Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand Down
2 changes: 2 additions & 0 deletions INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/cli_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion docs/internal/release_workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
74 changes: 12 additions & 62 deletions scripts/release/set_dev_wheel_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,32 @@

"""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<release>\d+\.\d+\.\d+)\.dev(?P<number>\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)
if match is None:
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] = []

Expand All @@ -53,74 +40,37 @@ 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

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:
path.write_text("".join(output))
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:
"""CLI entry point."""

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",
Expand All @@ -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
Expand Down
7 changes: 6 additions & 1 deletion switchyard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
19 changes: 3 additions & 16 deletions switchyard/cli/switchyard_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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")

Expand Down
2 changes: 1 addition & 1 deletion tests/getting_started/test_getting_started.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/readme/test_readme.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 3 additions & 21 deletions tests/test_cli_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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__}"
18 changes: 3 additions & 15 deletions tests/test_dev_wheel_versioning.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
Expand All @@ -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()
10 changes: 10 additions & 0 deletions tests/test_version_package_name.py
Original file line number Diff line number Diff line change
@@ -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")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading