Skip to content

Commit 1d8ac14

Browse files
yeldarbyclaude
andcommitted
fix(cli): address review feedback -- alias bug, stubs, output consistency
- Fix download alias crash: use url_or_id as dest with datasetUrl metavar - Add return after output_error in image.py for static analysis safety - Replace bare print in version create stub with output_error - Standardize SDK suppression to suppress_sdk_output() everywhere - Extract 7 identical _stub functions to shared stub() in _output.py - De-duplicate redundant os.getenv("ROBOFLOW_API_KEY") in workspace.py Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f213c87 commit 1d8ac14

14 files changed

Lines changed: 77 additions & 107 deletions

File tree

roboflow/cli/_output.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ def output_error(
112112
sys.exit(exit_code)
113113

114114

115+
def stub(args: Any) -> None:
116+
"""Placeholder handler for not-yet-implemented commands."""
117+
output_error(args, "This command is not yet implemented.", hint="Coming soon.", exit_code=1)
118+
119+
115120
@contextlib.contextmanager
116121
def suppress_sdk_output(args: Any = None) -> Iterator[None]:
117122
"""Suppress SDK stdout noise (e.g. 'loading Roboflow workspace...').

roboflow/cli/handlers/_aliases.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[ty
6464
from roboflow.cli.handlers.version import _download
6565

6666
download_p = subparsers.add_parser("download", help="Download a dataset version (alias for 'version download')")
67-
download_p.add_argument("datasetUrl", help="Dataset URL (e.g. workspace/project/version)")
67+
download_p.add_argument("url_or_id", metavar="datasetUrl", help="Dataset URL (e.g. workspace/project/version)")
6868
download_p.add_argument("-f", "--format", dest="format", default="voc", help="Export format")
6969
download_p.add_argument("-l", "--location", dest="location", help="Download location")
7070
download_p.set_defaults(func=_download)

roboflow/cli/handlers/annotation.py

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
if TYPE_CHECKING:
88
import argparse
99

10+
from roboflow.cli._output import stub
11+
1012

1113
def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[type-arg]
1214
"""Register the ``annotation`` command group."""
@@ -31,13 +33,13 @@ def _add_batch(sub: argparse._SubParsersAction) -> None: # type: ignore[type-ar
3133
# batch list
3234
p = batch_sub.add_parser("list", help="List annotation batches")
3335
p.add_argument("-p", "--project", required=True, help="Project ID")
34-
p.set_defaults(func=_stub)
36+
p.set_defaults(func=stub)
3537

3638
# batch get
3739
p = batch_sub.add_parser("get", help="Get annotation batch details")
3840
p.add_argument("batch_id", help="Batch ID")
3941
p.add_argument("-p", "--project", required=True, help="Project ID")
40-
p.set_defaults(func=_stub)
42+
p.set_defaults(func=stub)
4143

4244
batch_parser.set_defaults(func=lambda args: batch_parser.print_help())
4345

@@ -54,32 +56,20 @@ def _add_job(sub: argparse._SubParsersAction) -> None: # type: ignore[type-arg]
5456
# job list
5557
p = job_sub.add_parser("list", help="List annotation jobs")
5658
p.add_argument("-p", "--project", required=True, help="Project ID")
57-
p.set_defaults(func=_stub)
59+
p.set_defaults(func=stub)
5860

5961
# job get
6062
p = job_sub.add_parser("get", help="Get annotation job details")
6163
p.add_argument("job_id", help="Job ID")
6264
p.add_argument("-p", "--project", required=True, help="Project ID")
63-
p.set_defaults(func=_stub)
65+
p.set_defaults(func=stub)
6466

6567
# job create
6668
p = job_sub.add_parser("create", help="Create an annotation job")
6769
p.add_argument("-p", "--project", required=True, help="Project ID")
6870
p.add_argument("--name", required=True, help="Job name")
6971
p.add_argument("--batch", default=None, help="Batch ID to assign")
7072
p.add_argument("--assignees", default=None, help="Comma-separated assignee emails")
71-
p.set_defaults(func=_stub)
73+
p.set_defaults(func=stub)
7274

7375
job_parser.set_defaults(func=lambda args: job_parser.print_help())
74-
75-
76-
# ---------------------------------------------------------------------------
77-
# stub handler
78-
# ---------------------------------------------------------------------------
79-
80-
81-
def _stub(args: argparse.Namespace) -> None:
82-
"""Placeholder for not-yet-implemented annotation commands."""
83-
from roboflow.cli._output import output_error
84-
85-
output_error(args, "This command is not yet implemented.", hint="Coming soon.", exit_code=1)

roboflow/cli/handlers/batch.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,10 @@
88
import argparse
99

1010

11-
def _stub(args: argparse.Namespace) -> None:
12-
from roboflow.cli._output import output_error
13-
14-
output_error(args, "This command is not yet implemented.", hint="Coming soon.", exit_code=1)
15-
16-
1711
def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[type-arg]
1812
"""Register the ``batch`` command group."""
13+
from roboflow.cli._output import stub
14+
1915
batch_parser = subparsers.add_parser("batch", help="Batch processing operations")
2016
batch_subs = batch_parser.add_subparsers(title="batch commands", dest="batch_command")
2117

@@ -25,25 +21,25 @@ def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[ty
2521
create_p.add_argument("--input", dest="input", required=True, help="Input path (image directory or video file)")
2622
create_p.add_argument("--model", dest="model", default=None, help="Model ID override (default: workflow model)")
2723
create_p.add_argument("--output", dest="output", default=None, help="Output directory for results")
28-
create_p.set_defaults(func=_stub)
24+
create_p.set_defaults(func=stub)
2925

3026
# --- batch status ---
3127
status_p = batch_subs.add_parser("status", help="Check batch job status")
3228
status_p.add_argument("job_id", help="Batch job ID")
33-
status_p.set_defaults(func=_stub)
29+
status_p.set_defaults(func=stub)
3430

3531
# --- batch list ---
3632
list_p = batch_subs.add_parser("list", help="List batch jobs")
3733
list_p.add_argument(
3834
"--status", dest="status", default=None, help="Filter by status (pending, running, completed, failed)"
3935
)
40-
list_p.set_defaults(func=_stub)
36+
list_p.set_defaults(func=stub)
4137

4238
# --- batch results ---
4339
results_p = batch_subs.add_parser("results", help="Get batch job results")
4440
results_p.add_argument("job_id", help="Batch job ID")
4541
results_p.add_argument("--format", dest="format", default=None, help="Output format (json, csv)")
46-
results_p.set_defaults(func=_stub)
42+
results_p.set_defaults(func=stub)
4743

4844
# Default
4945
batch_parser.set_defaults(func=lambda args: batch_parser.print_help())

roboflow/cli/handlers/completion.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,24 @@
88
import argparse
99

1010

11-
def _stub(args: argparse.Namespace) -> None:
12-
from roboflow.cli._output import output_error
13-
14-
output_error(args, "This command is not yet implemented.", hint="Coming soon.", exit_code=1)
15-
16-
1711
def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[type-arg]
1812
"""Register the ``completion`` command group."""
13+
from roboflow.cli._output import stub
14+
1915
comp_parser = subparsers.add_parser("completion", help="Generate shell completions")
2016
comp_subs = comp_parser.add_subparsers(title="completion commands", dest="completion_command")
2117

2218
# --- completion bash ---
2319
bash_p = comp_subs.add_parser("bash", help="Generate bash completions")
24-
bash_p.set_defaults(func=_stub)
20+
bash_p.set_defaults(func=stub)
2521

2622
# --- completion zsh ---
2723
zsh_p = comp_subs.add_parser("zsh", help="Generate zsh completions")
28-
zsh_p.set_defaults(func=_stub)
24+
zsh_p.set_defaults(func=stub)
2925

3026
# --- completion fish ---
3127
fish_p = comp_subs.add_parser("fish", help="Generate fish completions")
32-
fish_p.set_defaults(func=_stub)
28+
fish_p.set_defaults(func=stub)
3329

3430
# Default
3531
comp_parser.set_defaults(func=lambda args: comp_parser.print_help())

roboflow/cli/handlers/folder.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,37 @@
88
import argparse
99

1010

11-
def _stub(args: argparse.Namespace) -> None:
12-
from roboflow.cli._output import output_error
13-
14-
output_error(args, "This command is not yet implemented.", hint="Coming soon.", exit_code=1)
15-
16-
1711
def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[type-arg]
1812
"""Register the ``folder`` command group."""
13+
from roboflow.cli._output import stub
14+
1915
folder_parser = subparsers.add_parser("folder", help="Manage workspace folders")
2016
folder_subs = folder_parser.add_subparsers(title="folder commands", dest="folder_command")
2117

2218
# --- folder list ---
2319
list_p = folder_subs.add_parser("list", help="List folders")
24-
list_p.set_defaults(func=_stub)
20+
list_p.set_defaults(func=stub)
2521

2622
# --- folder get ---
2723
get_p = folder_subs.add_parser("get", help="Show folder details")
2824
get_p.add_argument("folder_id", help="Folder ID")
29-
get_p.set_defaults(func=_stub)
25+
get_p.set_defaults(func=stub)
3026

3127
# --- folder create ---
3228
create_p = folder_subs.add_parser("create", help="Create a folder")
3329
create_p.add_argument("name", help="Folder name")
34-
create_p.set_defaults(func=_stub)
30+
create_p.set_defaults(func=stub)
3531

3632
# --- folder update ---
3733
update_p = folder_subs.add_parser("update", help="Update a folder")
3834
update_p.add_argument("folder_id", help="Folder ID")
3935
update_p.add_argument("--name", help="New folder name")
40-
update_p.set_defaults(func=_stub)
36+
update_p.set_defaults(func=stub)
4137

4238
# --- folder delete ---
4339
delete_p = folder_subs.add_parser("delete", help="Delete a folder")
4440
delete_p.add_argument("folder_id", help="Folder ID")
45-
delete_p.set_defaults(func=_stub)
41+
delete_p.set_defaults(func=stub)
4642

4743
# Default
4844
folder_parser.set_defaults(func=lambda args: folder_parser.print_help())

roboflow/cli/handlers/image.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def _handle_upload(args: argparse.Namespace) -> None:
5454
api_key = args.api_key or load_roboflow_api_key(args.workspace)
5555
if not api_key:
5656
output_error(args, "No API key found", hint="Set ROBOFLOW_API_KEY or run 'roboflow auth login'", exit_code=2)
57+
return
5758

5859
path = args.path
5960
if os.path.isdir(path):
@@ -62,13 +63,12 @@ def _handle_upload(args: argparse.Namespace) -> None:
6263
_handle_upload_single(args, api_key, path)
6364
else:
6465
output_error(args, f"Path not found: {path}", hint="Provide a valid file or directory path")
66+
return
6567

6668

6769
def _handle_upload_single(args: argparse.Namespace, api_key: str, path: str) -> None:
68-
import contextlib
69-
import io
70-
7170
import roboflow
71+
from roboflow.cli._output import suppress_sdk_output
7272

7373
metadata_raw = getattr(args, "metadata", None)
7474
metadata = json.loads(metadata_raw) if metadata_raw else None
@@ -77,7 +77,7 @@ def _handle_upload_single(args: argparse.Namespace, api_key: str, path: str) ->
7777
retries = getattr(args, "retries", None) or getattr(args, "num_retries", 0) or 0
7878

7979
# Always suppress SDK "loading..." noise during workspace/project init
80-
with contextlib.redirect_stdout(io.StringIO()):
80+
with suppress_sdk_output():
8181
try:
8282
rf = roboflow.Roboflow(api_key)
8383
workspace = rf.workspace(args.workspace)
@@ -111,13 +111,11 @@ def _handle_upload_single(args: argparse.Namespace, api_key: str, path: str) ->
111111

112112

113113
def _handle_upload_directory(args: argparse.Namespace, api_key: str, path: str) -> None:
114-
import contextlib
115-
import io
116-
117114
import roboflow
115+
from roboflow.cli._output import suppress_sdk_output
118116

119117
# Always suppress SDK "loading..." noise during workspace init
120-
with contextlib.redirect_stdout(io.StringIO()):
118+
with suppress_sdk_output():
121119
try:
122120
rf = roboflow.Roboflow(api_key)
123121
workspace = rf.workspace(args.workspace)
@@ -169,15 +167,18 @@ def _handle_get(args: argparse.Namespace) -> None:
169167
api_key = args.api_key or load_roboflow_api_key(args.workspace)
170168
if not api_key:
171169
output_error(args, "No API key found", hint="Set ROBOFLOW_API_KEY or run 'roboflow auth login'", exit_code=2)
170+
return
172171

173172
workspace_url = args.workspace or _default_workspace()
174173
if not workspace_url:
175174
output_error(args, "No workspace specified", hint="Use --workspace or run 'roboflow auth login'")
175+
return
176176

177177
url = f"{API_URL}/{workspace_url}/{args.project}/images/{args.image_id}?api_key={api_key}"
178178
response = requests.get(url)
179179
if response.status_code != 200:
180180
output_error(args, f"Failed to get image: {response.text}", exit_code=3)
181+
return
181182

182183
data = response.json()
183184
output(args, data, text=json.dumps(data, indent=2))
@@ -201,10 +202,12 @@ def _handle_search(args: argparse.Namespace) -> None:
201202
api_key = args.api_key or load_roboflow_api_key(args.workspace)
202203
if not api_key:
203204
output_error(args, "No API key found", hint="Set ROBOFLOW_API_KEY or run 'roboflow auth login'", exit_code=2)
205+
return
204206

205207
workspace_url: str = args.workspace or _default_workspace() or ""
206208
if not workspace_url:
207209
output_error(args, "No workspace specified", hint="Use --workspace or run 'roboflow auth login'")
210+
return
208211

209212
result = rfapi.workspace_search(
210213
api_key=api_key,
@@ -235,14 +238,17 @@ def _handle_tag(args: argparse.Namespace) -> None:
235238

236239
if not args.add_tags and not args.remove_tags:
237240
output_error(args, "Nothing to do", hint="Specify --add and/or --remove with comma-separated tags")
241+
return
238242

239243
api_key = args.api_key or load_roboflow_api_key(args.workspace)
240244
if not api_key:
241245
output_error(args, "No API key found", hint="Set ROBOFLOW_API_KEY or run 'roboflow auth login'", exit_code=2)
246+
return
242247

243248
workspace_url = args.workspace or _default_workspace()
244249
if not workspace_url:
245250
output_error(args, "No workspace specified", hint="Use --workspace or run 'roboflow auth login'")
251+
return
246252

247253
base = f"{API_URL}/{workspace_url}/{args.project}/images/{args.image_id}/tags"
248254
added = []
@@ -292,10 +298,12 @@ def _handle_delete(args: argparse.Namespace) -> None:
292298
api_key = args.api_key or load_roboflow_api_key(args.workspace)
293299
if not api_key:
294300
output_error(args, "No API key found", hint="Set ROBOFLOW_API_KEY or run 'roboflow auth login'", exit_code=2)
301+
return
295302

296303
workspace_url: str = args.workspace or _default_workspace() or ""
297304
if not workspace_url:
298305
output_error(args, "No workspace specified", hint="Use --workspace or run 'roboflow auth login'")
306+
return
299307

300308
ids = [i.strip() for i in args.image_ids.split(",") if i.strip()]
301309
result = rfapi.workspace_delete_images(
@@ -329,10 +337,12 @@ def _handle_annotate(args: argparse.Namespace) -> None:
329337
api_key = args.api_key or load_roboflow_api_key(args.workspace)
330338
if not api_key:
331339
output_error(args, "No API key found", hint="Set ROBOFLOW_API_KEY or run 'roboflow auth login'", exit_code=2)
340+
return
332341

333342
annotation_path = args.annotation_file
334343
if not os.path.isfile(annotation_path):
335344
output_error(args, f"Annotation file not found: {annotation_path}")
345+
return
336346

337347
with open(annotation_path) as f:
338348
annotation_string = f.read()

roboflow/cli/handlers/search.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,11 @@ def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[ty
3333

3434

3535
def _search(args: argparse.Namespace) -> None:
36-
import contextlib
37-
import io
38-
3936
import roboflow
40-
from roboflow.cli._output import output_error
37+
from roboflow.cli._output import output_error, suppress_sdk_output
4138

4239
try:
43-
# Suppress "loading Roboflow workspace..." messages that corrupt --json output
44-
quiet = getattr(args, "json", False) or getattr(args, "quiet", False)
45-
if quiet:
46-
with contextlib.redirect_stdout(io.StringIO()):
47-
rf = roboflow.Roboflow()
48-
workspace = rf.workspace(args.workspace)
49-
else: # noqa: PLR5501
40+
with suppress_sdk_output():
5041
rf = roboflow.Roboflow()
5142
workspace = rf.workspace(args.workspace)
5243
except Exception as exc:

roboflow/cli/handlers/universe.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,10 @@
88
import argparse
99

1010

11-
def _stub(args: argparse.Namespace) -> None:
12-
from roboflow.cli._output import output_error
13-
14-
output_error(args, "This command is not yet implemented.", hint="Coming soon.", exit_code=1)
15-
16-
1711
def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[type-arg]
1812
"""Register the ``universe`` command group."""
13+
from roboflow.cli._output import stub
14+
1915
uni_parser = subparsers.add_parser("universe", help="Browse Roboflow Universe")
2016
uni_subs = uni_parser.add_subparsers(title="universe commands", dest="universe_command")
2117

@@ -24,7 +20,7 @@ def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[ty
2420
search_p.add_argument("query", help="Search query")
2521
search_p.add_argument("--type", dest="type", choices=["dataset", "model"], default=None, help="Filter by type")
2622
search_p.add_argument("--limit", type=int, default=20, help="Max results (default: 20)")
27-
search_p.set_defaults(func=_stub)
23+
search_p.set_defaults(func=stub)
2824

2925
# Default
3026
uni_parser.set_defaults(func=lambda args: uni_parser.print_help())

0 commit comments

Comments
 (0)