Skip to content

fix: REPL help and choice-option crashes under Click-vendoring Typer (>=0.25)#443

Open
soustruh wants to merge 6 commits into
mainfrom
fix/typer-vendored-click
Open

fix: REPL help and choice-option crashes under Click-vendoring Typer (>=0.25)#443
soustruh wants to merge 6 commits into
mainfrom
fix/typer-vendored-click

Conversation

@soustruh

@soustruh soustruh commented Jun 18, 2026

Copy link
Copy Markdown

Why

Typer >=0.25 vendors its own copy of Click (typer._click), which breaks code that
hands standalone-click objects to Typer or checks against standalone click types.
Users installing via uv tool install / the curl script get the latest Typer (0.26.7);
the lockfile pinned 0.24.1, so uv sync / CI never hit it.

  1. REPL help and tab-completion showed only help/exit: _build_command_tree
    gated its walk with isinstance(x, click.Group), False for a vendored TyperGroup,
    so the tree built empty (hidden by a bare except). Same bug also in
    scripts/check_command_sync.py and tests/test_permissions.py.
  2. Invalid click.Choice values (job run --mode/--poll-strategy, project invite --role/--default-role, project member-set-role --role, dev-portal identity --role-hint) raised an uncaught traceback instead of exit 2 — standalone
    click.BadParameter isn't caught by Typer's vendored-Click handler.

What changed

  • repl.py (+ check_command_sync.py, test_permissions.py): structural _is_group()
    TypeGuard replaces isinstance(_, click.Group); swallowed tree-build error now logged.
  • job.py/project.py/dev_portal.py: the seven click.Choice options become StrEnum
    types; drift-guarded against the constants by tests/test_choice_enums.py.
  • Dependencies bumped to latest (Typer 0.26.7, Click 8.4.1, …), except fastapi capped
    <0.137
    : with 0.137, serve --ui stops requiring a token on protected endpoints
    (/doctor, /version, /changelog, /agents reachable unauthenticated), reopening
    GHSA-ffpq-prmh-3gx2.
  • Version 0.63.4.

Verification

Full suite (minus live-cred e2e) green on Typer 0.24.1 and 0.26.7; ruff + ty clean.
Adds a REPL help-output test and a parametrized invalid-choice -> exit-2 test across
all seven choice options.

Follow-ups (not in this PR)

  • typer[all] extra no longer exists in Typer 0.26 (harmless uv warning).
  • fastapi >=0.137 support needs the serve --ui route-aware auth check updated, then the
    cap can lift.

soustruh added 3 commits June 18, 2026 18:53
uv lock --upgrade -> Typer 0.26.7, Click 8.4.1, etc. fastapi is held at
<0.137 in the [server] extra: with 0.137, serve --ui stops requiring a token
on protected endpoints (/doctor, /version, /changelog, /agents reachable
unauthenticated), reopening GHSA-ffpq-prmh-3gx2.
typer.main.get_command returns a TyperGroup; Typer >=0.25 vendors its own
Click, so it is not a standalone click.Group subclass and
isinstance(_, click.Group) collapsed the REPL command tree to empty (help
and tab-completion showed only help/exit). Replace with a structural
_is_group() TypeGuard at the three vendored-Click isinstance sites (repl,
check_command_sync, test_permissions); stop silently swallowing the
tree-build error.
The --mode/--poll-strategy (job run), --role/--default-role (project invite),
--role (member-set-role) and --role-hint (dev-portal identity) options passed
a standalone click.Choice into Typer. Under a Click-vendoring Typer (>=0.25)
the BadParameter it raises is a different class than the one Typer's parser
catches, so an invalid value escaped as an uncaught traceback instead of a
clean exit-2 usage error. Replace the click.Choice options with StrEnum types
so Typer validates with its own Click. Valid values and --help are unchanged.
@soustruh soustruh force-pushed the fix/typer-vendored-click branch from ddd4632 to 328ae14 Compare June 18, 2026 16:56
Two gaps the fixes left untested:
- REPL `help` command output (not just the underlying tree): assert it lists
  command groups, guarding the empty-list regression at the command level.
- invalid choice value -> clean exit 2 (no uncaught traceback) across all seven
  choice options (job run --mode/--poll-strategy, project invite
  --role/--default-role, member-set-role --role, dev-portal identity add/edit
  --role-hint).
@soustruh soustruh marked this pull request as ready for review June 18, 2026 17:40

@padak padak left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review summary

Clean, well-tested, root-cause fix (not a workaround). All three changes trace to a single cause — Typer >=0.25 vendors its own Click (typer._click), so isinstance(x, click.Group) / standalone click.Choice objects misbehave against the vendored copy. Bundling them is the right call. LGTM after the uv.lock rebase below. (Comment only — not approving/requesting changes.)

What I verified

Area Result
Dropping import click from job.py / project.py / dev_portal.py ✅ Safe — the only click references in those files were the click.Choice calls this PR removes.
StrEnum vs. the service/client validation (mode not in VALID_JOB_MODES, poll_strategy not in VALID_POLL_STRATEGIES, client.py:2622) ✅ No regression — a StrEnum member behaves identically to a plain str under in frozenset[str], ==, json.dumps, and f-strings. The defense-in-depth checks in job_service.py / client.py keep working unchanged.
_is_group() TypeGuard (duck-typing via list_commands) ✅ Correct fix for the vendoring; TypeGuard gives ty proper narrowing.
Drift guards in test_choice_enums.py ProjectRole is correctly order-sensitive (the --role help text renders `"
Bare except: pass → stderr log in _build_command_tree ✅ Removes the silent-degrade-to-empty-tree path that hid the original bug.
fastapi<0.137 cap ✅ Legitimate — 0.137 reopens GHSA-ffpq-prmh-3gx2 (unauthenticated /doctor, /version, /changelog, /agents under serve --ui).
Version sync (marketplace.json + plugin.json + pyproject.toml) + changelog ✅ All on 0.63.4; all three fixes documented.

Nice touch keeping the default as typer.Option(JobMode(DEFAULT_JOB_MODE), ...) rather than JobMode.run — the default stays bound to the constant (single source of truth), and a drift between the constant and the enum would fail fast at import time, on top of the drift test.

Findings (all non-blocking)

🟡 Rebase needed — uv.lock conflict. PR is currently CONFLICTING. The only real conflict is uv.lock (main bumped cryptography in 0f9d184); pyproject.toml merges cleanly. A rebase + uv lock regen should clear it.

🟡 fastapi<0.137 cap has no tracking issue. The cap blocks future security updates to fastapi. The PR notes the follow-up ("serve --ui route-aware auth check needs updating"), but without an issue this risks becoming a silent long-term pin. Suggest filing one so the cap gets lifted deliberately rather than forgotten.

🟢 REPL direction (out of scope for this PR). Roughly half of this change invests in the REPL (help/tab-completion fix + two new REPL tests). There was an internal question on whether the REPL stays long-term. Flagging only so the decision is made deliberately — the choice-option fix and the fastapi cap are independently valuable and share the same root cause regardless of what happens to the REPL, so this does not affect mergeability here.

Verdict

Real user-facing bug (anyone installing via uv tool install / the curl script gets Typer 0.26.7 and hits both crashes; CI never saw it because the lockfile pinned 0.24.1). Mergeable after the uv.lock rebase.

soustruh added 2 commits June 18, 2026 21:52
…raction

typer 0.26 CliRunner.invoke returns typer.testing.Result (0.24 reused
click.testing.Result), so test helpers annotated `-> Result` imported from
click.testing failed `ty check`. Import Result from typer.testing instead.
Also cast benchmark.py stdio_total to float so the subtraction is numeric.
Surfaced by whole-tree `ty check` (make typecheck) after the 0.63.4 deps bump
-- the earlier per-file ty runs missed them.
@soustruh

Copy link
Copy Markdown
Author

The fastapi<0.137 cap here is a deliberate stopgap, not a permanent pin. The proper fix — making the serve --ui auth predicate robust to fastapi 0.137's lazy router tree (router-match resolution, fail-closed) and lifting the cap — is already prepared in #444, stacked on this branch.

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