Skip to content

Commit 9e93fcf

Browse files
yeldarbyclaude
andcommitted
fix(cli): address remaining review items -- URL params, exit codes, tests
- Use requests params dict for api_key instead of embedding in URLs (image.py) - Replace min(code, 3) with explicit exit code mapping in deployment.py - Add unit tests for _reorder_argv edge cases (12 tests) - Add backward-compat alias tests including download regression (11 tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1d8ac14 commit 9e93fcf

4 files changed

Lines changed: 143 additions & 5 deletions

File tree

roboflow/cli/handlers/deployment.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ def _wrapped(args: argparse.Namespace) -> None:
3232
except SystemExit as exc:
3333
sys.stdout = orig_stdout
3434
code = exc.code if isinstance(exc.code, int) else 1
35+
# Map legacy exit codes to CLI conventions: 1=general, 2=auth, 3=not-found
36+
exit_code = {0: 1, 1: 1, 2: 2, 3: 3}.get(code, 1) if code else 1
3537
text = captured.getvalue().strip()
3638
if text:
37-
output_error(args, text, exit_code=min(code, 3) if code else 1)
39+
output_error(args, text, exit_code=exit_code)
3840
else:
3941
output_error(args, "Deployment command failed.", exit_code=1)
4042
return

roboflow/cli/handlers/image.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@ def _handle_get(args: argparse.Namespace) -> None:
174174
output_error(args, "No workspace specified", hint="Use --workspace or run 'roboflow auth login'")
175175
return
176176

177-
url = f"{API_URL}/{workspace_url}/{args.project}/images/{args.image_id}?api_key={api_key}"
178-
response = requests.get(url)
177+
url = f"{API_URL}/{workspace_url}/{args.project}/images/{args.image_id}"
178+
response = requests.get(url, params={"api_key": api_key})
179179
if response.status_code != 200:
180180
output_error(args, f"Failed to get image: {response.text}", exit_code=3)
181181
return
@@ -259,7 +259,7 @@ def _handle_tag(args: argparse.Namespace) -> None:
259259
tag = tag.strip()
260260
if not tag:
261261
continue
262-
resp = requests.post(f"{base}?api_key={api_key}", json={"tag": tag})
262+
resp = requests.post(base, params={"api_key": api_key}, json={"tag": tag})
263263
if resp.status_code == 200:
264264
added.append(tag)
265265

@@ -268,7 +268,7 @@ def _handle_tag(args: argparse.Namespace) -> None:
268268
tag = tag.strip()
269269
if not tag:
270270
continue
271-
resp = requests.delete(f"{base}/{tag}?api_key={api_key}")
271+
resp = requests.delete(f"{base}/{tag}", params={"api_key": api_key})
272272
if resp.status_code == 200:
273273
removed.append(tag)
274274

tests/cli/test_aliases.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Tests for backward-compatibility aliases in _aliases.py."""
2+
3+
import unittest
4+
5+
6+
class TestAliases(unittest.TestCase):
7+
"""Verify top-level aliases parse correctly and delegate to the right handler."""
8+
9+
def _parse(self, argv: list[str]):
10+
from roboflow.cli import build_parser
11+
12+
parser = build_parser()
13+
return parser.parse_args(argv)
14+
15+
def test_login_alias_exists(self) -> None:
16+
args = self._parse(["login"])
17+
self.assertIsNotNone(args.func)
18+
19+
def test_login_alias_with_api_key(self) -> None:
20+
args = self._parse(["login", "--api-key", "test-key"])
21+
self.assertEqual(args.api_key_flag, "test-key")
22+
23+
def test_whoami_alias_exists(self) -> None:
24+
args = self._parse(["whoami"])
25+
self.assertIsNotNone(args.func)
26+
27+
def test_upload_alias_exists(self) -> None:
28+
args = self._parse(["upload", "img.jpg", "-p", "my-project"])
29+
self.assertIsNotNone(args.func)
30+
self.assertEqual(args.path, "img.jpg")
31+
self.assertEqual(args.project, "my-project")
32+
33+
def test_import_alias_exists(self) -> None:
34+
args = self._parse(["import", "/data/images", "-p", "my-project"])
35+
self.assertIsNotNone(args.func)
36+
self.assertEqual(args.path, "/data/images")
37+
self.assertEqual(args.project, "my-project")
38+
39+
def test_download_alias_parses_url(self) -> None:
40+
"""Regression: download alias must use url_or_id as dest, not datasetUrl."""
41+
args = self._parse(["download", "my-ws/my-proj/3"])
42+
self.assertIsNotNone(args.func)
43+
# The critical check: args.url_or_id must exist (not args.datasetUrl)
44+
self.assertEqual(args.url_or_id, "my-ws/my-proj/3")
45+
46+
def test_download_alias_with_format(self) -> None:
47+
args = self._parse(["download", "my-ws/my-proj/3", "-f", "yolov8"])
48+
self.assertEqual(args.format, "yolov8")
49+
50+
def test_download_alias_with_location(self) -> None:
51+
args = self._parse(["download", "my-ws/my-proj/3", "-l", "/tmp/out"])
52+
self.assertEqual(args.location, "/tmp/out")
53+
54+
def test_download_alias_delegates_to_version_download(self) -> None:
55+
"""The download alias should use the same handler as 'version download'."""
56+
from roboflow.cli.handlers.version import _download
57+
58+
args = self._parse(["download", "my-ws/my-proj/3"])
59+
self.assertIs(args.func, _download)
60+
61+
def test_upload_model_alias_hidden(self) -> None:
62+
"""upload_model is a hidden alias — it should still parse."""
63+
args = self._parse(["upload_model", "-p", "my-proj", "-t", "yolov8", "-m", "/weights"])
64+
self.assertIsNotNone(args.func)
65+
66+
67+
if __name__ == "__main__":
68+
unittest.main()

tests/cli/test_reorder_argv.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Tests for _reorder_argv — global flag reordering."""
2+
3+
import unittest
4+
5+
6+
class TestReorderArgv(unittest.TestCase):
7+
"""Verify _reorder_argv moves global flags before subcommands."""
8+
9+
def _reorder(self, argv: list[str]) -> list[str]:
10+
from roboflow.cli import _reorder_argv
11+
12+
return _reorder_argv(argv)
13+
14+
def test_no_flags(self) -> None:
15+
self.assertEqual(self._reorder(["project", "list"]), ["project", "list"])
16+
17+
def test_empty(self) -> None:
18+
self.assertEqual(self._reorder([]), [])
19+
20+
def test_bool_flag_after_subcommand(self) -> None:
21+
result = self._reorder(["project", "list", "--json"])
22+
self.assertEqual(result, ["--json", "project", "list"])
23+
24+
def test_bool_flag_already_first(self) -> None:
25+
result = self._reorder(["--json", "project", "list"])
26+
self.assertEqual(result, ["--json", "project", "list"])
27+
28+
def test_short_bool_flag(self) -> None:
29+
result = self._reorder(["project", "list", "-j"])
30+
self.assertEqual(result, ["-j", "project", "list"])
31+
32+
def test_value_flag_after_subcommand(self) -> None:
33+
result = self._reorder(["project", "list", "--api-key", "abc123"])
34+
self.assertEqual(result, ["--api-key", "abc123", "project", "list"])
35+
36+
def test_short_value_flag(self) -> None:
37+
result = self._reorder(["project", "list", "-k", "abc123"])
38+
self.assertEqual(result, ["-k", "abc123", "project", "list"])
39+
40+
def test_multiple_flags_mixed(self) -> None:
41+
result = self._reorder(["project", "list", "--json", "-w", "my-ws"])
42+
self.assertEqual(result, ["--json", "-w", "my-ws", "project", "list"])
43+
44+
def test_value_flag_at_end_without_value(self) -> None:
45+
"""A value flag at the very end with no following arg should still be moved."""
46+
result = self._reorder(["project", "list", "--api-key"])
47+
self.assertEqual(result, ["--api-key", "project", "list"])
48+
49+
def test_non_global_flags_preserved(self) -> None:
50+
"""Flags not in the global set stay in place."""
51+
result = self._reorder(["image", "upload", "--project", "my-proj", "--json"])
52+
self.assertEqual(result, ["--json", "image", "upload", "--project", "my-proj"])
53+
54+
def test_quiet_and_version_flags(self) -> None:
55+
result = self._reorder(["project", "list", "--quiet", "--version"])
56+
self.assertEqual(result, ["--quiet", "--version", "project", "list"])
57+
58+
def test_workspace_flag(self) -> None:
59+
result = self._reorder(["project", "list", "--workspace", "ws-1"])
60+
self.assertEqual(result, ["--workspace", "ws-1", "project", "list"])
61+
62+
def test_preserves_subcommand_positional_args(self) -> None:
63+
result = self._reorder(["version", "download", "ws/proj/3", "--json", "-f", "yolov8"])
64+
self.assertEqual(result, ["--json", "version", "download", "ws/proj/3", "-f", "yolov8"])
65+
66+
67+
if __name__ == "__main__":
68+
unittest.main()

0 commit comments

Comments
 (0)