Repository navigation
Fix missing click dependency in container image; release 0.2.8 - #127
Merged
Merged
Conversation
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>
ptuan5
approved these changes
Aug 21, 2026
ptuan5
left a comment
Collaborator
There was a problem hiding this comment.
Some of the comments sounds like debugging processes for me, making it harder to read. But should not matter much.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the v0.2.7 container regression where every
mouse-trackinginvocationaborted with
ModuleNotFoundError: No module named 'click', and bumps to 0.2.8.Root cause
Three conditions combined:
src/mouse_tracking/cli/infer.pyimportsclickand usesclick.Choicein 18 places, butclickwas never declaredin
[project.dependencies]— it was only satisfied transitively via typer.clickfrom its runtime requirements and vendored it astyper._click.twice:
uv sync --frozeninto.venv, thenuv pip install --system ..uv pip installdoes not readuv.lock, so the packages that actuallyexecuted at runtime (
/usr/local/lib/python3.10/site-packages/) came from anunlocked 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 pointedimage_versionat the v0.2.5 image, which had been built before typer 0.26.0was published. Bumping
image_versionto0.2.7triggered a rebuild after thatdate.
Because
main.pyimports theinfersubmodule unconditionally, the failure hitevery subcommand — 272 failed tasks across
PREDICT_SINGLE_MOUSE_KEYPOINTS(67/67) and
EXTRACT_FECAL_BOLI_BINS(1/1), retried and then absorbed byerrorStrategy.Changes
pyproject.toml— declareclick>=8.1.7,<8.3, constraintyper>=0.12.4,<0.26, version0.2.8.The typer upper bound is not optional. Installing
clickalongsidetyper >= 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:--runtimemetavar[tfs]<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 nativeEnum/Literalannotation is a separate work item.
uv.lock— relocked.clickis promoted to a direct dependency of theproject; 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.orgwith identicalsha256hashes (upstream indexmetadata change, same artifacts).
Dockerfile— make the locked resolution the one that executes:--no-depson 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/lintproduced, plusclick. Also addsRUN mouse-tracking --helpas a build-time smoke test, so import-timeregressions fail the image build rather than a production batch, and pins the uv
image to
0.8.17instead of:latestso the resolver readinguv.lockis theone that wrote it.
nextflow.config—manifest.versionandparams.image_version→0.2.8.tests/cli/test_click_choice_options.py— new regression guard. Walks everyregistered command, finds options whose type is a
click.Choice, and assertstheir values appear in
--helpand that no metavar renders as<function>.Fails on both failure modes above.
Verification
docker build --platform linux/amd64with 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.
click 8.1.7(
/usr/local/lib/python3.10/site-packages/click/__init__.py),typer 0.16.0,and
mouse-tracking infer single-pose --helprenders[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_versionkeys five container paths innextflow/configs/profiles/sumner2.config:runtime,RBase,sleap-1.4.1,sleap-io-0.2.0, andrclone. Merging this triggers only the Docker Hub build;nothing in this repo places
v0.2.8/latest.sifunder/projects/kumar-lab/meta/images/mouse-tracking-runtime/. All five need to bebuilt 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
--lockedin CI.uv sync/export --frozenusesuv.lockas-is withoutchecking it against
pyproject.toml; that is why a lock stamped0.2.6survived the 0.2.7 release. Switching CI to
--lockedwould fail the build ona stale lock.
click_type=click.ChoicetoEnum/Literalacross the 18call sites, which is what allows the
typer<0.26bound to be lifted.aberger4/mouse-tracking-base:python3.10-slim. Left floatinghere since it is first-party; pinning mainly shifts the cost to remembering to
bump it.
errorStrategyin thesumner2profile.{ task.attempt <= 3 ? 'retry' : 'ignore' }retried a deterministic environment fault 201 times, then absorbedit — 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.
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