Skip to content

feat(cli): migrate from argparse to typer#447

Merged
yeldarby merged 31 commits intomainfrom
apr-2026/cli-typer
Apr 3, 2026
Merged

feat(cli): migrate from argparse to typer#447
yeldarby merged 31 commits intomainfrom
apr-2026/cli-typer

Conversation

@yeldarby
Copy link
Copy Markdown
Contributor

@yeldarby yeldarby commented Apr 3, 2026

Summary

Migrates the CLI framework from argparse to typer for better help output, built-in shell completion, simpler testing, and consistency with the inference CLI.

Benefits realized

Benefit Status Notes
Shell completion ✅ Fully realized roboflow completion bash/zsh/fish generates real scripts. No longer a stub.
Hidden commands ✅ Fully realized Typer's hidden=True replaces our custom _CleanHelpFormatter
Simpler testing ✅ Fully realized typer.testing.CliRunner replaces manual argparse construction
Rich help output ✅ Fully realized Tables, boxes, colors in --help. Rich-formatted errors in text mode.
Consistency with inference CLI ✅ Fully realized Both CLIs now use typer with same patterns
Better help generation ✅ Fully realized From docstrings and type hints, not manual strings
JSON validation errors ✅ Bonus --json mode now emits structured JSON even for typer validation errors (missing args, bad options) — argparse couldn't do this
Type-safe parameters ⚠️ Partial Command signatures are typed, but ctx_to_args() bridge means business logic still uses args.project lookups. Full type safety is a future cleanup.
Less boilerplate ⚠️ Partial register(subparsers) replaced with @command() decorators, but ctx_to_args() bridge adds a small adapter layer. Net reduction is real but not as dramatic as hoped.
Global options via callback ⚠️ Partial @app.callback() is cleaner than argparse, but _reorder_argv still needed (see below)
No more _reorder_argv ❌ Not realized Typer/Click does NOT propagate parent options to subcommands. roboflow project list --json fails without the hack. Had to restore it.

What changed

  • Framework: argparse → typer (Click under the hood)
  • Python: 3.8+ → 3.10+ (typer requirement)
  • 18 handler modules migrated to typer.Typer() apps with @command() decorators
  • Shell completion: roboflow completion bash/zsh/fish generates real completion scripts
  • --json validation errors: Structured JSON on stderr for all error types (including typer/Click validation)
  • --json --version: Outputs {"version": "1.2.16"}
  • Rich-formatted help with tables, boxes, and colors
  • _compat.py bridge: ctx_to_args() converts typer Context to args namespace, keeping all business logic unchanged
  • _LegacyParserShim: parse_args() returns namespace with func (no longer executes); print_help() outputs text

Backwards compatibility preserved

  • roboflow=roboflow.roboflowpy:main entry point works
  • from roboflow.roboflowpy import main and _argparser imports work
  • _LegacyParserShim provides parse_args() (parse without executing) and print_help()
  • All 9 legacy aliases with original flag signatures (including repeatable -p on upload_model)
  • Hidden aliases don't appear in help but still function
  • --json works in any position (restored _reorder_argv)
  • All --json output schemas identical
  • All exit codes identical

What was removed

  • _CleanHelpFormatter → typer hidden=True
  • pkgutil auto-discovery → explicit app.add_typer() registration
  • argparse test patterns → typer.testing.CliRunner

Testing

  • 374 tests, all passing
  • make check_code_quality clean (ruff format, ruff check, mypy)
  • 28/28 regression checks (all command groups + aliases + hidden aliases)
  • Fresh QA: found and fixed --json positioning bug
  • Code review + security review: all findings addressed
  • Codex review: 3 findings (legacy shim, print_help, repeatable -p) all fixed

Dependency changes

  • Added: typer>=0.12.0 (brings click, rich, shellingham)
  • Bumped: python_requires=">=3.10", ruff/mypy targets to py310

yeldarby and others added 13 commits April 2, 2026 17:26
Add typer dependency, bump Python to >=3.10, create _compat.py bridge
helper (ctx_to_args), rewrite __init__.py with typer app + global
callback, and migrate project.py as the reference handler implementation.

The pattern: each handler module exports a typer.Typer() app with
@command() decorators. Business logic is unchanged — only the parameter
wiring changes. ctx_to_args() bridges typer.Context to the args namespace
that output()/output_error() expect.

NOTE: CLI is broken until all handlers are migrated (the __init__.py
imports all handler apps). This commit establishes the pattern; the
remaining 17 handlers will be migrated in parallel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…yper

Replace `register(subparsers)` with module-level `typer.Typer()` apps
(`auth_app`, `workspace_app`) following the project.py reference pattern.
Thin typer command wrappers call `ctx_to_args()` then delegate to
unchanged business logic functions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…to typer

- version.py: exports version_app Typer with list/get/download/export/create subcommands
- infer.py: exports infer_command() for top-level registration
- search.py: exports search_command() for top-level registration
- All business logic unchanged; uses ctx_to_args bridge from _compat

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rse to typer

Follows the same pattern as project.py: export typer apps, use ctx_to_args
bridge, lazy imports in business logic. annotation.py uses 3-level nesting
with batch_app and job_app sub-typers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e to typer

Follows the same pattern as project.py: typer.Typer app with
ctx_to_args bridge, business logic unchanged. Train uses
invoke_without_command=True so bare `roboflow train -p X -v Y`
works. Deployment legacy aliases use hidden=True.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…parse to typer

Migrate workflow, folder, universe, video, batch, completion handlers
to export typer apps. Rewrite _aliases.py to export register_aliases(app)
using typer command decorators instead of argparse subparsers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All CLI handlers migrated from argparse to typer:
- 17 command group handlers export typer.Typer() apps
- 2 top-level commands (infer, search) export registration functions
- _aliases.py rewritten with register_aliases() using hidden=True
- _compat.py bridge (ctx_to_args) enables unchanged business logic
- _CleanHelpFormatter and _reorder_argv deleted (typer handles natively)
- Shell completion built-in via typer (--install-completion, --show-completion)

CLI is fully functional: roboflow --help shows clean Rich-formatted output,
all commands work, hidden aliases work but don't appear in help.

All linting clean (ruff format, ruff check, mypy).
Tests need migration to typer.testing.CliRunner (159 argparse-specific
test errors expected — next step).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nner

Replace build_parser().parse_args() patterns with CliRunner.invoke(app, ...)
across all 18 test files. Registration tests now verify commands via --help
exit code instead of inspecting argparse internals. Behavior tests that call
handler functions directly with mocked APIs are preserved unchanged.

Also adds _LegacyParserShim to build_parser() so backwards-compat tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace argparse references with typer architecture
- Replace argparse handler template with typer pattern
- Update Python version to 3.10+

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Typer/Click doesn't propagate parent-level options (--json, --api-key,
--workspace, --quiet) to subcommand parsers. Users and agents naturally
write 'roboflow project list --json' which fails without reordering.

Restore the _reorder_argv helper from the argparse era — it moves known
global flags to before the subcommand in sys.argv before typer processes
them. This is essential for agent-friendliness.

374 tests pass, all linting clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…dead code

1. _reorder_argv: don't reorder value flags (--api-key) that have no
   following value. Leaves them in place so typer shows a proper error.
2. _version_callback: check sys.argv for --json/-j to output JSON version.
   Removes dead _json_version_callback function.

374 tests pass, all linting clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion

1. In --json mode, typer/Click validation errors (missing args, bad options)
   now emit structured JSON on stderr instead of Rich-formatted text.
   Uses standalone_mode=False to catch click.exceptions.UsageError.

2. Shell completion commands (bash, zsh, fish) now generate real completion
   scripts using Click's shell_completion API. No longer stubs.
   Usage: eval "$(roboflow completion zsh)"

374 tests pass, all linting clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yeldarby yeldarby requested a review from tonylampada April 3, 2026 02:45
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 696e5d4b93

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

yeldarby and others added 11 commits April 2, 2026 21:53
1. _LegacyParserShim.parse_args() no longer executes commands — returns
   a namespace with func that can be called separately, matching the
   old argparse parse-then-execute pattern.

2. print_help() now outputs the help text (was discarding CliRunner output).

3. upload_model alias -p flag is now Optional[list[str]] to preserve the
   repeatable action='append' behavior for multi-project deployments.

374 tests pass, all linting clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Bump version to 1.3.0
- Better CLI description: 'Build and deploy computer vision models...'
- Hide noisy aliases from --help (login, whoami, upload, import) — keep download
- Hide --install-completion/--show-completion (we have 'completion' subcommand)
- Add 'roboflow help' as alias for --help
- workspace get defaults to current workspace when no ID given
- Commands alphabetized in help output

374 tests pass, all linting clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Hide login/whoami/upload/import aliases from --help (keep download visible)
- Commands fully alphabetized in help output
- Exit code 0 (not 2) when running roboflow with no args
- Translate raw API hints ('issuing a GET request') to CLI hints
  ('roboflow project list', 'roboflow workspace get')
- workspace get defaults to current workspace when no arg given
- Hide --install-completion/--show-completion (we have 'completion' subcommand)
- Add 'roboflow help' command as alias for --help

374 tests pass, all linting clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Flattened command listing: roboflow --help now shows all 65 visible
  commands as 'noun verb' (e.g. 'project list', 'project get') instead
  of collapsed groups. Fully alphabetized.
- Added -h shorthand for --help (works on all subcommands too)
- Added -v shorthand for --version
- Options sorted alphabetically in help output
- Hidden commands (legacy aliases) excluded from help but still functional

374 tests pass, all linting clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All four help paths (--help, -h, no args, 'help' command) now show the
same Rich-formatted flattened command listing. Subcommand --help still
uses typer's default grouped view.

Root help intercept happens in main() before typer processes --help,
so the flattened output is shown instead of typer's grouped view.
Rich panels and tables match the subcommand help styling.

374 tests pass, all linting clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Options use typer's color scheme: bold cyan for long flags, bold green
for short flags, bold yellow for metavar (TEXT). Commands use cyan for
group name and bold for verb, giving a visual indication that they're
tied together. Top-level commands (download, infer, search) are just
bold.

374 tests pass, all linting clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All commands in --help are now consistently 'noun verb' format:
- Hidden: download (→ version download), infer (→ video infer exists),
  search (→ image search / universe search exist)
- All hidden aliases still work, just not shown in help
- Fixed description truncation: use full short_help instead of
  get_short_help_str() which Click caps at ~45 chars

374 tests pass, all linting clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ation

1. Add 'model infer' command that delegates to the existing infer logic.
   Now inference lives in the noun-verb hierarchy where users expect it.

2. Make 'image search' project-optional: without -p searches the whole
   workspace (using the workspace-level RoboQL search). With -p searches
   within a specific project. Also adds --export for dataset export.
   This unifies the old 'search' and 'image search' into one command.

3. Fix help text truncation by using cmd.help (full docstring first line)
   instead of short_help which Click caps at ~45 chars.

Hidden aliases (roboflow infer, roboflow search, roboflow download) all
still work — they're just not shown in --help.

374 tests pass, all linting clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SortedGroup now sorts options on individual commands (not just groups)
by overriding get_command() to sort params before help rendering.

Sort order: required options first, then alphabetical by flag name,
--help always last. Commands within groups also alphabetized.

Applied to all 20 Typer() instances across 16 handler files.

374 tests pass, all linting clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split options into separate Rich table columns (long flag, short flag,
metavar, description) instead of concatenating into one text column.
Now --api-key/-k/TEXT and --json/-j align properly.

374 tests pass, all linting clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…un/deploy)

Don't advertise features that aren't implemented yet:
- batch group: hidden entirely (all 4 commands are stubs)
- workflow build/run/deploy: hidden individually (other workflow
  commands like list/get/create/update/fork are implemented)

Hidden commands still work and give clear 'not yet implemented' messages.

374 tests pass, all linting clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tonylampada
tonylampada previously approved these changes Apr 3, 2026
Copy link
Copy Markdown
Collaborator

@tonylampada tonylampada left a comment

Choose a reason for hiding this comment

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

works for me!!

@yeldarby yeldarby changed the base branch from apr-2026/cli-phase2 to main April 3, 2026 16:37
@yeldarby yeldarby dismissed tonylampada’s stale review April 3, 2026 16:37

The base branch was changed.

yeldarby and others added 4 commits April 3, 2026 11:37
python_requires was bumped to >=3.10 in setup.py as part of the typer
migration. The CI matrix still included 3.9, causing failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 374 tests pass locally on Python 3.11.6. Previous CI run
appears to have had a transient failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Typer's Rich-based help rendering emits ANSI escape codes in CI
(GitHub Actions), which breaks string containment checks for
hyphenated flags like --api-key. The escape codes split the text
across color boundaries, so "api-key" as a contiguous substring
is not found.

Add _strip_ansi() helper to affected test files and use it when
asserting on help output text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yeldarby yeldarby merged commit e010156 into main Apr 3, 2026
12 checks passed
@yeldarby yeldarby deleted the apr-2026/cli-typer branch April 3, 2026 21:22
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