headroomlabs-ai/headroom · error · ValueError

expected one locked Ruff package, found {versions!r}

Error message

expected one locked Ruff package, found {versions!r}

What it means

verify-ruff-version.py parses uv.lock and requires exactly one `package` entry named `ruff`. Zero entries means Ruff was dropped from the lock (stale lock after pyproject changes); multiple entries means the lock resolved two different Ruff versions, typically across conflicting environment markers or platforms.

Source

Thrown at scripts/verify-ruff-version.py:47

    ruff_requirements = [
        requirement for requirement in dependencies if requirement.startswith("ruff")
    ]
    if len(ruff_requirements) != 1:
        raise ValueError(f"expected one Ruff dev dependency, found {ruff_requirements!r}")

    match = re.fullmatch(r"ruff==([^;,\s]+)", ruff_requirements[0])
    if match is None:
        raise ValueError(
            "pyproject.toml must contain one exact Ruff pin in project.optional-dependencies.dev"
        )
    return match.group(1)


def _locked_version() -> str:
    packages = _load_toml(ROOT / "uv.lock")["package"]
    versions = [str(package["version"]) for package in packages if package["name"] == "ruff"]
    if len(versions) != 1:
        raise ValueError(f"expected one locked Ruff package, found {versions!r}")
    return versions[0]


def _pre_commit_version() -> str:
    lines = (ROOT / ".pre-commit-config.yaml").read_text(encoding="utf-8").splitlines()
    for index, line in enumerate(lines):
        if line.strip() != f"- repo: {RUFF_PRE_COMMIT_REPO}":
            continue
        for candidate in lines[index + 1 :]:
            stripped = candidate.strip()
            if stripped.startswith("- repo:"):
                break
            if stripped.startswith("rev:"):
                return stripped.removeprefix("rev:").strip().removeprefix("v")
        break
    raise ValueError(f"could not find a rev for {RUFF_PRE_COMMIT_REPO}")

View on GitHub (pinned to 322425c43b)

Solutions

  1. Regenerate the lock: `uv lock` from the repo root, then rerun the verify script.
  2. If duplicates persist, inspect `uv lock` output for why Ruff resolves twice (conflicting markers) and unify the constraints.
  3. Never hand-edit uv.lock; resolve merge conflicts by regenerating.
Defensive patterns

Strategy: validation

Validate before calling

import tomllib
from pathlib import Path

def locked_ruff_versions(uv_lock: Path) -> list[str]:
    packages = tomllib.loads(uv_lock.read_text())["package"]
    return [str(p["version"]) for p in packages if p["name"] == "ruff"]
# assert len(locked_ruff_versions(...)) == 1 after every `uv lock`

Prevention

When it happens

Trigger: Editing pyproject.toml without running `uv lock` afterward; the lock containing ruff twice under different resolution forks (multiple platform/marker branches); a hand-edited uv.lock.

Common situations: Dependabot bumping pyproject but not regenerating uv.lock; platform-specific resolution forking after adding markers; lockfile merge conflicts resolved by keeping both entries.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/df5be7b0fa287b0b. Report an issue: GitHub.