Skip to content

Commit e010156

Browse files
authored
Merge pull request #447 from roboflow/apr-2026/cli-typer
feat(cli): migrate from argparse to typer
2 parents 0b3e1c5 + b6ce59a commit e010156

48 files changed

Lines changed: 2657 additions & 2533 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
strategy:
1212
matrix:
1313
os: ["ubuntu-latest", "windows-latest"]
14-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
14+
python-version: ["3.10", "3.11", "3.12", "3.13"]
1515
runs-on: ${{ matrix.os }}
1616
env:
1717
PYTHONUTF8: 1

CLAUDE.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,22 +86,25 @@ The Roboflow Python SDK follows a hierarchical object model that mirrors the Rob
8686

8787
### CLI Package (`roboflow/cli/`)
8888

89-
The CLI is a modular package with auto-discovered handler modules. `roboflow/roboflowpy.py` is a backwards-compatibility shim that delegates to `roboflow.cli.main`.
89+
The CLI is built on [typer](https://typer.tiangolo.com/) (which uses Click under the hood). `roboflow/roboflowpy.py` is a backwards-compatibility shim that delegates to `roboflow.cli.main`.
9090

9191
**Package structure:**
92-
- `__init__.py` — Root parser with global flags (`--json`, `--workspace`, `--api-key`, `--quiet`), auto-discovery via `pkgutil.iter_modules`, custom `_CleanHelpFormatter`, and `_reorder_argv` for flexible flag positioning
92+
- `__init__.py` — Root `typer.Typer()` app with global `@app.callback()` for `--json`, `--workspace`, `--api-key`, `--quiet`. Explicitly registers all handler apps via `app.add_typer()`.
9393
- `_output.py``output(args, data, text)` for JSON/text output, `output_error(args, msg, hint, exit_code)` for structured errors, `suppress_sdk_output()` to silence SDK noise, `stub()` for unimplemented commands
94+
- `_compat.py``ctx_to_args(ctx, **kwargs)` bridge that converts `typer.Context` to the `SimpleNamespace` that output helpers expect
9495
- `_table.py``format_table(rows, columns)` for columnar list output
9596
- `_resolver.py``resolve_resource(shorthand)` for parsing `project`, `ws/project`, `ws/project/3`
96-
- `handlers/` — One file per command group (auto-discovered). `_aliases.py` registers backwards-compat top-level commands (loaded last)
97+
- `handlers/` — One file per command group, each exporting a `typer.Typer()` app. `_aliases.py` registers backwards-compat top-level commands via `register_aliases(app)`.
9798

9899
**Adding a new command:**
99100
1. Create `roboflow/cli/handlers/mycommand.py`
100-
2. Export `register(subparsers)` — it will be auto-discovered
101-
3. Use lazy imports for heavy dependencies (inside handler functions, not at module top level)
102-
4. Use `output()` for all output, `output_error()` for all errors
103-
5. Wrap SDK calls in `with suppress_sdk_output():` to prevent "loading..." noise
104-
6. Add tests in `tests/cli/test_mycommand_handler.py`
101+
2. Create a module-level `mycommand_app = typer.Typer(help="...", no_args_is_help=True)`
102+
3. Add commands with `@mycommand_app.command("verb")` decorators
103+
4. Each command takes `ctx: typer.Context` + typed params, calls `ctx_to_args(ctx, **params)` to create args namespace
104+
5. Use `output()` for all output, `output_error()` for all errors
105+
6. Wrap SDK calls in `with suppress_sdk_output():` to prevent "loading..." noise
106+
7. Register in `roboflow/cli/__init__.py`: `app.add_typer(mycommand_app, name="mycommand")`
107+
8. Add tests using `typer.testing.CliRunner` in `tests/cli/test_mycommand_handler.py`
105108

106109
**Agent experience requirements for all CLI commands:**
107110
- Support `--json` for structured output (stable schema)
@@ -119,16 +122,16 @@ The CLI is a modular package with auto-discovered handler modules. `roboflow/rob
119122
3. **Format Flexibility**: Supports multiple dataset formats (YOLO, COCO, Pascal VOC, etc.)
120123
4. **Batch Operations**: Upload and download operations support concurrent processing
121124
5. **CLI Noun-Verb Pattern**: Commands follow `roboflow <noun> <verb>` (e.g. `roboflow project list`). Common operations have top-level aliases (`login`, `upload`, `download`)
122-
6. **CLI Auto-Discovery**: Handler modules in `roboflow/cli/handlers/` are loaded automatically — no registration list to maintain
125+
6. **CLI Explicit Registration**: Handler apps are explicitly imported and registered via `app.add_typer()` in `__init__.py` — clear dependency chain, no runtime discovery
123126
7. **Backwards Compatibility**: Legacy command names and flag signatures are preserved as hidden aliases
124127

125128
## Project Configuration
126129

127-
- **Python Version**: 3.8+
128-
- **Main Dependencies**: See `requirements.txt`
130+
- **Python Version**: 3.10+
131+
- **Main Dependencies**: See `requirements.txt` (includes `typer>=0.12.0`)
129132
- **Entry Point**: `roboflow=roboflow.roboflowpy:main` (shim delegates to `roboflow.cli.main`)
130133
- **Code Style**: Enforced by ruff with Google docstring convention
131-
- **Type Checking**: mypy configured for Python 3.8
134+
- **Type Checking**: mypy configured for Python 3.10
132135

133136
## Important Notes
134137

CLI-COMMANDS.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,19 @@ roboflow video infer -p my-project -v 3 -f video.mp4 --fps 10
122122
roboflow video status <job-id>
123123
```
124124

125+
### Shell completion
126+
127+
```bash
128+
# Zsh
129+
eval "$(roboflow completion zsh)"
130+
131+
# Bash (requires bash >= 4.4)
132+
eval "$(roboflow completion bash)"
133+
134+
# Fish
135+
roboflow completion fish | source
136+
```
137+
125138
## JSON output for agents
126139

127140
Every command supports `--json` for structured output that's safe to pipe:
@@ -167,7 +180,7 @@ Version numbers are always numeric — that's how `x/y` is disambiguated between
167180
| `universe` | Search Roboflow Universe |
168181
| `video` | Video inference |
169182
| `batch` | Batch processing jobs *(coming soon)* |
170-
| `completion` | Shell completion scripts *(coming soon)* |
183+
| `completion` | Generate shell completion scripts (bash, zsh, fish) |
171184

172185
Run `roboflow <command> --help` for details on any command.
173186

CONTRIBUTING.md

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -78,28 +78,29 @@ python -m pip install mkdocs mkdocs-material mkdocstrings mkdocstrings[python]
7878

7979
### CLI Development
8080

81-
The CLI lives in `roboflow/cli/` with auto-discovered handler modules. To add a new command:
81+
The CLI is built on [typer](https://typer.tiangolo.com/). Each command group is a separate `typer.Typer()` app registered in `roboflow/cli/__init__.py`. To add a new command:
8282

8383
1. Create `roboflow/cli/handlers/mycommand.py`:
8484

8585
```python
8686
"""My command description."""
8787
from __future__ import annotations
88-
from typing import TYPE_CHECKING
89-
if TYPE_CHECKING:
90-
import argparse
91-
92-
def register(subparsers: argparse._SubParsersAction) -> None:
93-
parser = subparsers.add_parser("mycommand", help="Do something")
94-
sub = parser.add_subparsers(title="mycommand commands")
95-
96-
p = sub.add_parser("list", help="List things")
97-
p.add_argument("-p", "--project", required=True, help="Project ID")
98-
p.set_defaults(func=_list)
99-
100-
parser.set_defaults(func=lambda args: parser.print_help())
101-
102-
def _list(args: argparse.Namespace) -> None:
88+
from typing import Annotated, Optional
89+
import typer
90+
from roboflow.cli._compat import ctx_to_args
91+
92+
mycommand_app = typer.Typer(help="Do something", no_args_is_help=True)
93+
94+
@mycommand_app.command("list")
95+
def list_things(
96+
ctx: typer.Context,
97+
project: Annotated[str, typer.Option("-p", "--project", help="Project ID")],
98+
) -> None:
99+
"""List things in a project."""
100+
args = ctx_to_args(ctx, project=project)
101+
_list(args)
102+
103+
def _list(args) -> None:
103104
from roboflow.cli._output import output, output_error, suppress_sdk_output
104105

105106
with suppress_sdk_output():
@@ -113,8 +114,14 @@ def _list(args: argparse.Namespace) -> None:
113114
output(args, data, text="Found 1 result.")
114115
```
115116

116-
2. Add tests in `tests/cli/test_mycommand_handler.py`
117-
3. Run `make check_code_quality` and `python -m unittest`
117+
2. Register in `roboflow/cli/__init__.py`:
118+
```python
119+
from roboflow.cli.handlers.mycommand import mycommand_app
120+
app.add_typer(mycommand_app, name="mycommand")
121+
```
122+
123+
3. Add tests using `typer.testing.CliRunner` in `tests/cli/test_mycommand_handler.py`
124+
4. Run `make check_code_quality` and `python -m unittest`
118125

119126
**Agent experience checklist** (every command must satisfy):
120127
- [ ] Supports `--json` via `output()` helper
@@ -123,7 +130,7 @@ def _list(args: argparse.Namespace) -> None:
123130
- [ ] SDK calls wrapped in `with suppress_sdk_output():`
124131
- [ ] Exit codes: 0=success, 1=error, 2=auth, 3=not found
125132

126-
**Documentation policy:** `CLI-COMMANDS.md` in this repo is a quickstart only. The comprehensive command reference lives in [`roboflow-product-docs`](https://github.com/roboflow/roboflow-product-docs) and is published to docs.roboflow.com. When adding a new command, update both: add a quick example to `CLI-COMMANDS.md` and the full reference to the product docs CLI page.
133+
**Documentation policy:** `CLI-COMMANDS.md` in this repo is a quickstart only. The comprehensive command reference lives in [`roboflow-dev-reference`](https://github.com/roboflow/roboflow-dev-reference) and is published to docs.roboflow.com/developer/command-line-interface. When adding a new command, update both: add a quick example to `CLI-COMMANDS.md` and the full reference to the dev-reference CLI page.
127134

128135
### Pre-commit Hooks
129136

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ target = ["test", "roboflow"]
77
tests = ["B201", "B301"]
88

99
[tool.ruff]
10-
target-version = "py39"
10+
target-version = "py310"
1111
line-length = 120
1212

1313
[tool.ruff.lint]
@@ -104,7 +104,7 @@ banned-module-level-imports = [
104104
]
105105

106106
[tool.mypy]
107-
python_version = "3.9"
107+
python_version = "3.10"
108108
exclude = ["^build/"]
109109

110110
[[tool.mypy.overrides]]

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ tqdm>=4.41.0
1919
PyYAML>=5.3.1
2020
requests_toolbelt
2121
filetype
22+
typer>=0.12.0

roboflow/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from roboflow.models import CLIPModel, GazeModel # noqa: F401
1616
from roboflow.util.general import write_line
1717

18-
__version__ = "1.2.16"
18+
__version__ = "1.3.0"
1919

2020

2121
def check_key(api_key, model, notebook, num_retries=0):

0 commit comments

Comments
 (0)