Skip to content

Fix missing click dependency in container image; release 0.2.8 - #127

Merged
ptuan5 merged 1 commit into
mainfrom
release-0.2.8
Aug 21, 2026
Merged

ptuan5 merged 1 commit into
mainfrom
release-0.2.8

Conversation

@bergsalex

Copy link
Copy Markdown
Collaborator

Fixes the v0.2.7 container regression where every mouse-tracking invocation
aborted with ModuleNotFoundError: No module named 'click', and bumps to 0.2.8.

Root cause

Three conditions combined:

  1. Undeclared direct dependency. src/mouse_tracking/cli/infer.py imports
    click and uses click.Choice in 18 places, but click was never declared
    in [project.dependencies] — it was only satisfied transitively via typer.
  2. Upstream dependency-graph change. Typer 0.26.0 (2026-05-26) dropped
    click from its runtime requirements and vendored it as typer._click.
  3. Non-reproducible image build. The Dockerfile installed dependencies
    twice: uv sync --frozen into .venv, then uv pip install --system ..
    uv pip install does not read uv.lock, so the packages that actually
    executed at runtime (/usr/local/lib/python3.10/site-packages/) came from an
    unlocked re-resolution against PyPI — which picked up typer >= 0.26 and
    therefore no click.

Only (3) changed in effect between v0.2.6 and v0.2.7. Dockerfile, uv.lock,
and [project.dependencies] were byte-identical; v0.2.6 pointed
image_version at the v0.2.5 image, which had been built before typer 0.26.0
was published. Bumping image_version to 0.2.7 triggered a rebuild after that
date.

Because main.py imports the infer submodule unconditionally, the failure hit
every subcommand — 272 failed tasks across PREDICT_SINGLE_MOUSE_KEYPOINTS
(67/67) and EXTRACT_FECAL_BOLI_BINS (1/1), retried and then absorbed by
errorStrategy.

Changes

pyproject.toml — declare click>=8.1.7,<8.3, constrain
typer>=0.12.4,<0.26, version 0.2.8.

The typer upper bound is not optional. Installing click alongside
typer >= 0.26 replaces the crash with a silent defect: the vendored parser no
longer recognises an external click.Choice, and the option renders a
<function> metavar instead of its values. Reproduced locally:

typer --runtime metavar
0.16.0 [tfs]
0.27.1 <function>

Note the default value still appears in help text under 0.27.1, so choice
presence alone does not detect this — the metavar is the tell. Migrating the 18
click_type=click.Choice([...]) call sites to a native Enum/Literal
annotation is a separate work item.

uv.lock — relocked. click is promoted to a direct dependency of the
project; typer stays 0.16.0 and click 8.1.7, so no installed version moves. The
lock also still claimed version = "0.2.6" — it was never relocked for 0.2.7.
Incidental churn: the torch/torchvision/torchaudio cu126 wheel URLs now point
at download-r2.pytorch.org with identical sha256 hashes (upstream index
metadata change, same artifacts).

Dockerfile — make the locked resolution the one that executes:

RUN uv export --frozen --no-group dev --no-group test --no-group lint \
      --no-emit-project -o /tmp/requirements.txt \
 && uv pip install --system -r /tmp/requirements.txt
COPY src ./src
RUN uv pip install --system --no-deps .

--no-deps on the project install is what prevents the unlocked re-resolution.
The exported set is 43 packages, identical to what the old uv sync --frozen --no-group dev/test/lint produced, plus click. Also adds
RUN mouse-tracking --help as a build-time smoke test, so import-time
regressions fail the image build rather than a production batch, and pins the uv
image to 0.8.17 instead of :latest so the resolver reading uv.lock is the
one that wrote it.

nextflow.config — manifest.version and params.image_version → 0.2.8.

tests/cli/test_click_choice_options.py — new regression guard. Walks every
registered command, finds options whose type is a click.Choice, and asserts
their values appear in --help and that no metavar renders as <function>.
Fails on both failure modes above.

Verification

  • docker build --platform linux/amd64 with the shipping Dockerfile: exit 0.
    The in-container smoke test passed and rendered the full command tree — the
    exact invocation that aborted in v0.2.7.
  • Inside the built image: click 8.1.7
    (/usr/local/lib/python3.10/site-packages/click/__init__.py), typer 0.16.0,
    and mouse-tracking infer single-pose --help renders [gait-paper] /
    [pytorch] rather than <function>.
  • pytest tests --ignore=tests/nextflow: 2325 passed, 10 skipped.
  • ruff check src/ tests/ clean for all tracked files.

Before running 0.2.8 in production

params.image_version keys five container paths in
nextflow/configs/profiles/sumner2.config: runtime, RBase, sleap-1.4.1,
sleap-io-0.2.0, and rclone. Merging this triggers only the Docker Hub build;
nothing in this repo places v0.2.8/latest.sif under
/projects/kumar-lab/meta/images/mouse-tracking-runtime/. All five need to be
built and deployed to the cluster before any run uses the default profile
, or
jobs fail on a missing container — structurally the same "release points at an
artifact that isn't there yet" failure as this incident.

Follow-ups, not in this PR

  • --locked in CI. uv sync/export --frozen uses uv.lock as-is without
    checking it against pyproject.toml; that is why a lock stamped 0.2.6
    survived the 0.2.7 release. Switching CI to --locked would fail the build on
    a stale lock.
  • Migrate off click_type=click.Choice to Enum/Literal across the 18
    call sites, which is what allows the typer<0.26 bound to be lifted.
  • Digest-pin aberger4/mouse-tracking-base:python3.10-slim. Left floating
    here since it is first-party; pinning mainly shifts the cost to remembering to
    bump it.
  • errorStrategy in the sumner2 profile. { task.attempt <= 3 ? 'retry' : 'ignore' } retried a deterministic environment fault 201 times, then absorbed
    it — the workflow reported completion with 272 failed tasks and zero keypoint
    outputs. Distinguishing transient exit codes (137, 140) from deterministic
    ones, or gating on a failure-ratio threshold, would surface a systemic
    container defect instead of consuming the allocation.
  • Untracked WIP in the working tree (src/mouse_tracking/io/,
    matching/{algorithms,fragment,tracklet,video_observations}.py,
    cli/{args,validation}.py, verify_imports.py) currently has 7 ruff errors
    (F401, UP035, UP006). CI lint will fail whenever those are committed.

🤖 Generated with Claude Code

The v0.2.7 image aborted every `mouse-tracking` invocation with
`ModuleNotFoundError: No module named 'click'`, because:

1. `cli/infer.py` imports `click` and uses `click.Choice` in 18 places,
   but `click` was never declared in `[project.dependencies]` -- it was
   only satisfied transitively through typer.
2. Typer 0.26.0 (2026-05-26) dropped `click` from its runtime
   requirements and vendored it as `typer._click`.
3. The Dockerfile installed dependencies twice: `uv sync --frozen` into
   `.venv`, then `uv pip install --system .`. `uv pip install` does not
   read uv.lock, so the packages that actually executed at runtime came
   from an unlocked re-resolution against PyPI -- which picked up
   typer >= 0.26 and therefore no `click`.

Only (3) changed between v0.2.6 and v0.2.7; v0.2.6 pointed at the
v0.2.5 image, which had been built before typer 0.26.0 was published.

Changes:
- Declare `click>=8.1.7,<8.3` and bound `typer>=0.12.4,<0.26`. The typer
  upper bound is required: with typer >= 0.26 the vendored parser does
  not recognise an external `click.Choice`, so the option silently
  renders a `<function>` metavar instead of its values. Migrating the 18
  call sites to `Enum`/`Literal` is a separate work item.
- Install from the locked resolution in the Dockerfile: `uv export
  --frozen` to a requirements file, then `uv pip install --system
  --no-deps .` so the project install cannot re-resolve.
- Add `RUN mouse-tracking --help` as a build-time smoke test so
  import-time regressions fail the image build, not a production batch.
- Pin the uv image to 0.8.17 instead of :latest.
- Add tests/cli/test_click_choice_options.py, which fails on both the
  missing-import and the `<function>` metavar failure modes.
- Bump version to 0.2.8 in pyproject.toml, nextflow.config manifest, and
  params.image_version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bergsalex bergsalex self-assigned this Aug 21, 2026

@ptuan5 ptuan5 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the comments sounds like debugging processes for me, making it harder to read. But should not matter much.

@ptuan5
ptuan5 merged commit 3477dec into main Aug 21, 2026
2 checks passed
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.

2 participants