Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
01b8d24
feat(cli): begin typer migration — foundation + project handler template
yeldarby Apr 2, 2026
8ba41c3
refactor(cli): migrate auth and workspace handlers from argparse to t…
yeldarby Apr 2, 2026
6fe3258
refactor(cli): migrate version, infer, search handlers from argparse …
yeldarby Apr 2, 2026
6b8f35d
refactor(cli): migrate image.py and annotation.py handlers from argpa…
yeldarby Apr 2, 2026
9e28b0f
refactor(cli): migrate model, train, deployment handlers from argpars…
yeldarby Apr 2, 2026
1cc8b69
refactor(cli): migrate remaining simple handlers and aliases from arg…
yeldarby Apr 2, 2026
b22cfbf
feat(cli): complete typer migration — all 18 handlers + aliases
yeldarby Apr 2, 2026
65e49c0
test(cli): migrate all CLI tests from argparse to typer.testing.CliRu…
yeldarby Apr 3, 2026
b09c600
docs: update CLAUDE.md and CONTRIBUTING.md for typer migration
yeldarby Apr 3, 2026
2c7400d
fix(cli): restore _reorder_argv for global flag positioning with typer
yeldarby Apr 3, 2026
920e2fd
fix(cli): address code review — reorder edge case, --json --version, …
yeldarby Apr 3, 2026
a125180
fix(cli): JSON validation errors in --json mode, working shell comple…
yeldarby Apr 3, 2026
898f268
docs: update CLI-COMMANDS.md — completion is implemented, add setup s…
yeldarby Apr 3, 2026
bc3cba7
fix(pre_commit): 🎨 auto format pre-commit hooks
pre-commit-ci[bot] Apr 3, 2026
696e5d4
Merge branch 'apr-2026/cli-phase2' into apr-2026/cli-typer
yeldarby Apr 3, 2026
3f87418
fix: remove unused variable in annotation test
yeldarby Apr 3, 2026
f2a893e
fix(cli): address Codex review — legacy shim, print_help, repeatable -p
yeldarby Apr 3, 2026
fbe9587
feat(cli): v1.3.0 — polish consistency, help, and description
yeldarby Apr 3, 2026
284937e
fix(cli): polish help output, error hints, consistency
yeldarby Apr 3, 2026
eedc6bc
feat(cli): flattened help, -h/-v shorthands, alphabetized everything
yeldarby Apr 3, 2026
3e5e732
fix(cli): consistent Rich-formatted flattened help across all help paths
yeldarby Apr 3, 2026
d6af60a
feat(cli): color-coded help matching typer's Rich styling
yeldarby Apr 3, 2026
c217b7a
feat(cli): pure noun-verb help — hide top-level aliases, fix truncation
yeldarby Apr 3, 2026
072be4c
feat(cli): add model infer, unify image search, fix description trunc…
yeldarby Apr 3, 2026
a495936
feat(cli): alphabetize subcommand options and commands via SortedGroup
yeldarby Apr 3, 2026
3a87e15
fix(cli): align option columns in custom help output
yeldarby Apr 3, 2026
faedf2d
fix(cli): hide stub commands from help (batch group, workflow build/r…
yeldarby Apr 3, 2026
a5ae886
Merge branch 'main' into apr-2026/cli-typer
yeldarby Apr 3, 2026
b493db3
fix(ci): drop Python 3.9 from test matrix
yeldarby Apr 3, 2026
7673bbf
ci: re-trigger workflow after transient 3.11 failure
yeldarby Apr 3, 2026
b6ce59a
fix(tests): strip ANSI codes from help output assertions
yeldarby Apr 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
os: ["ubuntu-latest", "windows-latest"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13"]
runs-on: ${{ matrix.os }}
env:
PYTHONUTF8: 1
Expand Down
27 changes: 15 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,25 @@ The Roboflow Python SDK follows a hierarchical object model that mirrors the Rob

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

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

**Package structure:**
- `__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
- `__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()`.
- `_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
- `_compat.py` — `ctx_to_args(ctx, **kwargs)` bridge that converts `typer.Context` to the `SimpleNamespace` that output helpers expect
- `_table.py` — `format_table(rows, columns)` for columnar list output
- `_resolver.py` — `resolve_resource(shorthand)` for parsing `project`, `ws/project`, `ws/project/3`
- `handlers/` — One file per command group (auto-discovered). `_aliases.py` registers backwards-compat top-level commands (loaded last)
- `handlers/` — One file per command group, each exporting a `typer.Typer()` app. `_aliases.py` registers backwards-compat top-level commands via `register_aliases(app)`.

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

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

## Project Configuration

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

## Important Notes

Expand Down
15 changes: 14 additions & 1 deletion CLI-COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,19 @@ roboflow video infer -p my-project -v 3 -f video.mp4 --fps 10
roboflow video status <job-id>
```

### Shell completion

```bash
# Zsh
eval "$(roboflow completion zsh)"

# Bash (requires bash >= 4.4)
eval "$(roboflow completion bash)"

# Fish
roboflow completion fish | source
```

## JSON output for agents

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

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

Expand Down
45 changes: 26 additions & 19 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,28 +78,29 @@ python -m pip install mkdocs mkdocs-material mkdocstrings mkdocstrings[python]

### CLI Development

The CLI lives in `roboflow/cli/` with auto-discovered handler modules. To add a new command:
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:

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

```python
"""My command description."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import argparse

def register(subparsers: argparse._SubParsersAction) -> None:
parser = subparsers.add_parser("mycommand", help="Do something")
sub = parser.add_subparsers(title="mycommand commands")

p = sub.add_parser("list", help="List things")
p.add_argument("-p", "--project", required=True, help="Project ID")
p.set_defaults(func=_list)

parser.set_defaults(func=lambda args: parser.print_help())

def _list(args: argparse.Namespace) -> None:
from typing import Annotated, Optional
import typer
from roboflow.cli._compat import ctx_to_args

mycommand_app = typer.Typer(help="Do something", no_args_is_help=True)

@mycommand_app.command("list")
def list_things(
ctx: typer.Context,
project: Annotated[str, typer.Option("-p", "--project", help="Project ID")],
) -> None:
"""List things in a project."""
args = ctx_to_args(ctx, project=project)
_list(args)

def _list(args) -> None:
from roboflow.cli._output import output, output_error, suppress_sdk_output

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

2. Add tests in `tests/cli/test_mycommand_handler.py`
3. Run `make check_code_quality` and `python -m unittest`
2. Register in `roboflow/cli/__init__.py`:
```python
from roboflow.cli.handlers.mycommand import mycommand_app
app.add_typer(mycommand_app, name="mycommand")
```

3. Add tests using `typer.testing.CliRunner` in `tests/cli/test_mycommand_handler.py`
4. Run `make check_code_quality` and `python -m unittest`

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

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

### Pre-commit Hooks

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ target = ["test", "roboflow"]
tests = ["B201", "B301"]

[tool.ruff]
target-version = "py39"
target-version = "py310"
line-length = 120

[tool.ruff.lint]
Expand Down Expand Up @@ -104,7 +104,7 @@ banned-module-level-imports = [
]

[tool.mypy]
python_version = "3.9"
python_version = "3.10"
exclude = ["^build/"]

[[tool.mypy.overrides]]
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ tqdm>=4.41.0
PyYAML>=5.3.1
requests_toolbelt
filetype
typer>=0.12.0
2 changes: 1 addition & 1 deletion roboflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from roboflow.models import CLIPModel, GazeModel # noqa: F401
from roboflow.util.general import write_line

__version__ = "1.2.16"
__version__ = "1.3.0"


def check_key(api_key, model, notebook, num_retries=0):
Expand Down
Loading
Loading