Skip to content

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

Merged
soustruh merged 5 commits into
mainfrom
fix/typer-vendored-click
Jun 19, 2026
Merged

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

Conversation

@soustruh

@soustruh soustruh commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Contributor 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.

Resolve the uv.lock conflict (keep cryptography 49.0.0, already ahead of
main's 48.0.1). Also fix typer 0.26.7 fallout that only the whole-tree
`make check` catches (per-file checks missed it):
- test helpers annotate typer.testing.Result (0.26 stopped reusing
  click.testing.Result) -> fixes `ty check`.
- isinstance(_, click.Group) is False under vendored Click, so
  scripts/generate_skill.py uses the structural _is_group, and reads
  param_type_name instead of isinstance(click.Argument/Option) so the
  SKILL.md table keeps its arg/option hints.
- scripts/benchmark.py casts stdio_total to float for the subtraction.
@soustruh soustruh force-pushed the fix/typer-vendored-click branch from 4eb4273 to 4ff12cc Compare June 18, 2026 22:28

@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 of #443 — fix: REPL help and choice-option crashes under Click-vendoring Typer (>=0.25)

Generated by kbagent-pr-reviewer subagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed via make check, not duplicated here.

Summary

The PR fixes two independent crash paths introduced when Typer >=0.25 began vendoring its own
copy of Click (typer._click): (1) the REPL command tree collapsed to empty because
_build_command_tree used isinstance(x, click.Group) against a TyperGroup that is not a
subclass of standalone click.Group; (2) seven click.Choice-backed CLI options raised an
uncaught traceback on invalid input because click.BadParameter from the vendored namespace
is not caught by Typer's error handler. Both are fixed correctly. Additionally, the PR caps
fastapi<0.137 (security: reopening of GHSA-ffpq-prmh-3gx2) and bumps the dependency lock
to ship Typer 0.26.7 / Click 8.4.1.

make check passes (4137 tests). No new CLI commands are added, so no plugin synchronization
map entries are required. No layer violations, no token discipline issues, no security concerns
in the new code.

Verdict: APPROVE. Zero blocking findings. Two non-blocking observations and one NIT noted below.

Verdict

  • Verdict: APPROVE
  • Blocking findings: 0
  • Non-blocking findings: 2
  • Nits: 1

Blocking findings

(none)

Non-blocking findings

[NB-1] pyproject.toml:12typer[all] extra no longer exists in Typer 0.26+

The dependency spec "typer[all]>=0.12" references an extra that Typer removed in 0.25+:
rich and shellingham are now unconditional dependencies, not an optional group. The PR
description acknowledges this produces a harmless uv warning but defers the fix. A dangling
extra spec silently widens the "known good" envelope: if a future Typer release reintroduces
[all] with different semantics, the spec would silently opt in. Suggested fix (can be a
follow-up): change "typer[all]>=0.12" to "typer>=0.12" and verify make check still
passes (it will, since rich + shellingham are now unconditional).

[NB-2] src/keboola_agent_cli/commands/repl.py:27,36_is_group duck-types around vendored Click, but click.Context (standalone) is still used to walk the tree

_is_group correctly avoids isinstance(_, click.Group) by checking for the list_commands
callable. However, _build_command_tree still passes the TyperGroup into a standalone
click.Context(click_group) (line 36). This works in practice today because Typer's
vendored Context and standalone Click's Context share the same protocol; both accept a
command-like object with a list_commands method. If a future Typer version's vendored Click
diverges further (e.g., modifies the Context.__init__ signature), this call site would be
the next breakage point. The correct long-term fix is typer.Context(click_group) (use
Typer's own context builder) — but this is lower priority than the shipped fix, since the
current behavior is verified working on 0.26.7. Documenting this latent coupling with a
one-line comment would suffice if a full refactor is deferred.

Nits

  • [NIT-1] src/keboola_agent_cli/scripts/benchmark.py:372 — the float() cast on
    stdio_results[i]["total"] is a correct ty type-narrowing fix but is unrelated to the
    Typer vendoring issue. It is a harmless inclusion; consider a separate commit note next time
    so reviewers can distinguish incidental churn from the structural fix.

Verification log

  • gh pr view 443 --json title,body,files → 21 files, +744/-508, no new CLI commands, fix: prefix. PR description matches diff scope. ✓
  • git rev-parse --abbrev-ref HEADfix/typer-vendored-click (worktree nifty-ishizaka-2f768b) ✓
  • make check → 4137 passed, 8 skipped, 125 deselected; ruff, ty, skill-check, command-sync-check all clean ✓
  • Layer violation grep (typer/click in services, httpx in commands, formatter in clients) → empty ✓
  • isinstance(g, click.Group) on TyperGroup (vendored) → False; _is_group(g)True — root bug confirmed and fixed ✓
  • _build_command_tree(click_app) with Typer 0.26.7 → tree size 304 (project list and job run present); was previously empty due to the bug ✓
  • isinstance(g, click.Group) check removed from scripts/check_command_sync.py, scripts/generate_skill.py, tests/test_permissions.py — all now import _is_group from repl.py
  • param.param_type_name attribute verified present on both click.Argument ("argument") and click.Option ("option") in Click 8.4.1 ✓
  • ProjectRole member order (admin, guest, readOnly, share) matches PROJECT_ROLES tuple in constants.py; drift guard test_project_role_enum_matches_constant asserts order ✓
  • StrEnum is a subclass of str, so JobMode.run passes mode: str at the service layer without a change to job_service.py::run_job
  • typer[all] extra: Typer 0.26.7 dist-info has no Provides-Extra entries — the extra is gone ✓
  • make skill-gen → no diff in plugins/kbagent/skills/kbagent/SKILL.md (no new commands to register) ✓
  • No new commands → OPERATION_REGISTRY, AGENT_CONTEXT, commands-reference.md, keboola-expert.md, gotchas.md do not require updates ✓
  • fastapi<0.137 cap in pyproject.toml with inline comment referencing GHSA-ffpq-prmh-3gx2; test_serve_ui.py already guards the auth requirement on protected endpoints ✓
  • No bare except:, no print() in src/, no raw error_code="..." strings, no magic number timeouts in new code ✓

Open questions for the author

(none)

@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.

Approved. The fix correctly addresses both crash paths under Typer >=0.25 (Click-vendoring): the empty REPL command tree and the uncaught BadParameter traceback on choice-options. make check is green (4137 tests), no 3-layer architecture violations, and no plugin/agent/docs sync drift (the PR adds no new commands).

See the detailed review comment for two non-blocking follow-ups: cleaning up the typer[all] extra in pyproject.toml (NB-1) and the latent click.Context coupling in repl.py (NB-2). Neither blocks merge.

@soustruh soustruh merged commit c026fff into main Jun 19, 2026
4 checks passed
@soustruh soustruh deleted the fix/typer-vendored-click branch June 19, 2026 12:56
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