Skip to content

fix(deps): a PEP 735 dev group, so uv run stops stripping the toolchain - #504

Merged
chris-dare-dev merged 1 commit into
mainfrom
fix/uv-dependency-group
Sep 19, 2026
Merged

chris-dare-dev merged 1 commit into
mainfrom
fix/uv-dependency-group

Conversation

@chris-dare-dev

Copy link
Copy Markdown
Owner

The call-site half of this already landed — tests/test_daily_metrics_report.py uses sys.executable rather than uv run. The other half did not, and the defect it closes is still live on main. It is simply no longer reachable from the suite.

The defect

pytest and ruff are declared only in [project.optional-dependencies].dev. uv installs groups by default and extras never, so any uv run fired from inside this repository syncs the project venv down to the default dependency set and prunes them out of it.

Measured on a Windows box against the real .venv, uv sync --dry-run:

Would uninstall
before 9 — pytest, ruff, iniconfig, pluggy, arxmcp, pyinstaller, pyinstaller-hooks-contrib, pefile, altgraph, pywin32-ctypes
after 6 — arxmcp + the PyInstaller stack only

pytest, ruff, iniconfig and pluggy are preserved.

The residual is a different thing and deliberately not addressed here: the PyInstaller build stack is installed from requirements-build.txt, kept outside pyproject.toml/uv.lock by apps/desktop/pyinstaller/desktop_package.py so the build venv stays throwaway. uv correctly regards those as foreign to the project environment.

This matters because roughly twenty uv run python -m tools.… commands are documented across tools/ docstrings and docs/ops/ runbooks. The failure mode is the nasty kind: the in-flight command survives on already-imported modules and appears to succeed, and the next make test dies with No module named pytest.

The duplication between the extra and the group is load-bearing. pip install -e ".[dev]" (Makefile bootstrap, docs/install.md) reads the extra; uv sync reads the group; neither table can reference the other. tests/test_dev_toolchain_guard.py fails if they drift apart.

The guard was narrowed — and the narrowing is tested

As originally written the guard banned every string constant naming the uv binary anywhere under tests/. On current main that flags tests/test_desktop_package.py:512, which is shutil.which("uv") — a PATH lookup that starts no process, syncs nothing and cannot prune a package. It mirrors the precondition the packaging driver itself checks.

Editing working packaging code to satisfy an over-broad detector is the wrong direction, so the detector now exempts arguments to which() while still catching argv lists, Windows uv.exe, and hardcoded absolute paths.

Weakening a guard is only safe if the weakening is itself tested. TestDetectorItself drives the detector over synthetic source: presence checks pass, invocation forms are still caught. One of those tests records a limit rather than a capability — a which() result bound to a variable and then passed to run() is invisible to any string-constant detector, with or without the exemption — so the guard does not imply cover it does not have.

Lockfile

Regenerated. Beyond the dev-group rows it also picks up the contract extra (jsonschema + mfc), which pyproject.toml declares and the lockfile did not carry at all. That is pre-existing drift, repaired here because leaving it out would mean knowingly committing a lock that does not match its manifest. No pin changes.

Worth noting for whoever hits it next: mfc is exactly the module missing in the 16 test_formal_release_pin.py failures below. uv sync --extra contract should clear them.

Verification

  • 5406 passed, 188 skipped, 1 xfailed, 20 failed. All 20 are pre-existing on origin/main and none are in the changed surface:
    • 16 × test_formal_release_pin.py — ModuleNotFoundError: No module named 'mfc'
    • 3 × test_desktop_package.py — WinError 1314, symlink creation needs Developer Mode
    • 1 × test_textbook_parser.py — signal.SIGKILL does not exist on Windows; ingest/textbook_parser.py:551 is byte-identical to origin/main
  • Ruff clean.
  • tests/test_dev_toolchain_guard.py: 6/6.

Not fixed here — environmental, not code

uv lock cannot run on this machine without an explicit --python. C:\Users\cedar\bin\python3.exe is a stub: no python.exe beside it, no pyvenv.cfg, no stdlib. uv selects it because that directory is early on PATH, and any build backend that spawns the sibling interpreter dies with:

No Python at '"C:\Users\cedar\bin\python.exe'

Note the mangled quote. That same stub is what made test_daily_metrics_report fail on the July branch. No code change fixes it.

…hain

The call-site half of this landed already: tests/test_daily_metrics_report.py
uses sys.executable rather than `uv run`. The other half did not, and the
defect it closes is still live on main -- it just is not reachable from the
suite any more.

pytest and ruff are declared ONLY in [project.optional-dependencies].dev. uv
installs GROUPS by default and extras NEVER, so any `uv run` fired from inside
this repository syncs the project venv down to the default dependency set and
prunes them out of it. Measured on this box against the real .venv,
`uv sync --dry-run`:

  before   would uninstall 9: pytest, ruff, iniconfig, pluggy, arxmcp,
                              pyinstaller, pyinstaller-hooks-contrib,
                              pefile, altgraph, pywin32-ctypes
  after    would uninstall 6: arxmcp + the PyInstaller stack only

pytest, ruff, iniconfig and pluggy are preserved. The residual is a different
thing and deliberately not addressed here: the PyInstaller build stack is
installed from requirements-build.txt, kept OUTSIDE pyproject/uv.lock by
apps/desktop/pyinstaller/desktop_package.py so the build venv is throwaway, so
uv correctly regards those as foreign to the project environment.

This matters because roughly twenty `uv run python -m tools.…` commands are
documented across tools/ docstrings and docs/ops/ runbooks. The failure mode
is the nasty kind: the in-flight command survives on already-imported modules
and appears to succeed, and the NEXT `make test` dies with "No module named
pytest".

The duplication between the extra and the group is load-bearing rather than an
oversight. `pip install -e ".[dev]"` (Makefile bootstrap, docs/install.md)
reads the extra; `uv sync` reads the group; neither table can reference the
other. tests/test_dev_toolchain_guard.py fails if they drift apart.

GUARD NARROWED, AND WHY. The guard as originally written banned every string
constant naming the uv binary anywhere under tests/. On current main that
flags tests/test_desktop_package.py:512, which is `shutil.which("uv")` -- a
PATH lookup that starts no process, syncs nothing and cannot prune a package.
It mirrors the precondition the packaging driver itself checks. Editing
working packaging code to satisfy an over-broad detector would be the wrong
direction, so the detector now exempts arguments to `which()` while still
catching argv lists, Windows `uv.exe` and hardcoded absolute paths.

Weakening a guard is only safe if the weakening is itself tested, so
TestDetectorItself drives the detector over synthetic source: presence checks
pass, invocation forms are still caught. One of those tests records a LIMIT
rather than a capability -- a `which()` result bound to a variable and then
passed to `run()` is invisible to any string-constant detector, with or
without the exemption -- so the guard does not imply cover it does not have.

uv.lock regenerated. Beyond the dev-group rows it also picks up the `contract`
extra (jsonschema + mfc), which pyproject declares and the lockfile did not
carry at all: pre-existing drift, repaired here because leaving it would mean
knowingly committing a lock that does not match its manifest. No pin changes.

Suite: 5406 passed, 188 skipped, 1 xfailed, 20 failed. All 20 failures are
pre-existing on origin/main and none are in the changed surface -- 16 in
test_formal_release_pin.py (`No module named 'mfc'`), 3 in
test_desktop_package.py (WinError 1314, symlink creation needs Developer
Mode), 1 in test_textbook_parser.py (`signal.SIGKILL` does not exist on
Windows; ingest/textbook_parser.py:551 is byte-identical to origin/main).
Ruff clean.

NOT FIXED HERE, and it is environmental rather than code: `uv lock` cannot run
on this machine without an explicit --python. C:\Users\cedar\bin\python3.exe is
a stub with no python.exe beside it, no pyvenv.cfg and no stdlib; uv selects it
because that directory is early on PATH, and any build backend that spawns the
sibling interpreter dies with `No Python at '"C:\Users\cedar\bin\python.exe'`
-- note the mangled quote. That stub is also what made this same test fail on
the July branch. No code change fixes it.
@chris-dare-dev
chris-dare-dev merged commit 158fb38 into main Sep 19, 2026
1 check passed
@chris-dare-dev
chris-dare-dev deleted the fix/uv-dependency-group branch September 19, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant