From 6aaf9fa53a05d5c2f868d4afe4854a0dfc41c6be Mon Sep 17 00:00:00 2001 From: Rodrigo Barbosa Date: Mon, 25 May 2026 14:12:30 -0300 Subject: [PATCH 1/2] fix(cli): scope 'image search -p' to the project via RoboQL filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The -p/--project flag was silently ignored: _handle_search passed the project nowhere, so every search returned workspace-wide results. The search/v1 endpoint only honors a project filter expressed inside the RoboQL query (project:, which the API resolves to a project id) — body-level project/dataset params are ignored. Prepend project: to the user's query with a leading space (implicit AND) so it stays compatible with free-text/semantic queries; an explicit 'AND (...)' wrapper 500s on free text. --- roboflow/cli/handlers/image.py | 15 +++++++++++++-- tests/cli/test_image_handler.py | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/roboflow/cli/handlers/image.py b/roboflow/cli/handlers/image.py index 382a3f18..ed7bad71 100644 --- a/roboflow/cli/handlers/image.py +++ b/roboflow/cli/handlers/image.py @@ -104,7 +104,7 @@ def search_images( Use --export to download matching results as a dataset. """ if project: - # Project-scoped search (legacy behavior) + # Project-scoped search: _handle_search injects a `project:` RoboQL filter. args = ctx_to_args(ctx, query=query, project=project, limit=limit, cursor=cursor) _handle_search(args) elif export: @@ -422,10 +422,21 @@ def _handle_search(args): # noqa: ANN001 output_error(args, "No workspace specified", hint="Use --workspace or run 'roboflow auth login'") return + # Scope the search to a single project when -p/--project is given. The search/v1 + # endpoint only honors a project filter expressed inside the RoboQL query + # (`project:`, which the API resolves to a project id) — body params like + # `project`/`dataset` are ignored. We prepend it with a leading space so it ANDs + # with the user's query while staying compatible with free-text/semantic queries + # (an explicit `AND (...)` wrapper 500s on free text). + query = args.query + project = getattr(args, "project", None) + if project: + query = f"project:{project} {args.query}" + result = rfapi.workspace_search( api_key=api_key, workspace_url=workspace_url, - query=args.query, + query=query, page_size=args.limit, continuation_token=args.cursor, ) diff --git a/tests/cli/test_image_handler.py b/tests/cli/test_image_handler.py index 3c386a04..650a3a61 100644 --- a/tests/cli/test_image_handler.py +++ b/tests/cli/test_image_handler.py @@ -462,9 +462,31 @@ def test_search(self, mock_workspace_search): sys.stdout = old mock_workspace_search.assert_called_once() + # -p/--project must scope the search via a `project:` RoboQL filter, + # combined with the user's query (the API ignores body-level project params). + called_query = mock_workspace_search.call_args.kwargs["query"] + self.assertEqual(called_query, "project:proj tag:test") result = json.loads(buf.getvalue()) self.assertEqual(result["total"], 0) + @patch("roboflow.adapters.rfapi.workspace_search") + def test_search_without_project_is_unscoped(self, mock_workspace_search): + from roboflow.cli.handlers.image import _handle_search + + mock_workspace_search.return_value = {"results": [], "total": 0} + args = _make_args(json=True, query="tag:test", project=None, limit=10, cursor=None) + + buf = io.StringIO() + old = sys.stdout + sys.stdout = buf + try: + _handle_search(args) + finally: + sys.stdout = old + + called_query = mock_workspace_search.call_args.kwargs["query"] + self.assertEqual(called_query, "tag:test") + class TestImageAnnotate(unittest.TestCase): """Test the annotate handler.""" From 77dd48e62ea941d70d401f1fe34dc3a47e2780a9 Mon Sep 17 00:00:00 2001 From: Rodrigo Barbosa Date: Mon, 25 May 2026 14:16:27 -0300 Subject: [PATCH 2/2] docs(cli): shorten inline comments on search-scoping change --- roboflow/cli/handlers/image.py | 10 +++------- tests/cli/test_image_handler.py | 3 +-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/roboflow/cli/handlers/image.py b/roboflow/cli/handlers/image.py index ed7bad71..ae421860 100644 --- a/roboflow/cli/handlers/image.py +++ b/roboflow/cli/handlers/image.py @@ -104,7 +104,7 @@ def search_images( Use --export to download matching results as a dataset. """ if project: - # Project-scoped search: _handle_search injects a `project:` RoboQL filter. + # _handle_search scopes by injecting a `project:` RoboQL filter. args = ctx_to_args(ctx, query=query, project=project, limit=limit, cursor=cursor) _handle_search(args) elif export: @@ -422,12 +422,8 @@ def _handle_search(args): # noqa: ANN001 output_error(args, "No workspace specified", hint="Use --workspace or run 'roboflow auth login'") return - # Scope the search to a single project when -p/--project is given. The search/v1 - # endpoint only honors a project filter expressed inside the RoboQL query - # (`project:`, which the API resolves to a project id) — body params like - # `project`/`dataset` are ignored. We prepend it with a leading space so it ANDs - # with the user's query while staying compatible with free-text/semantic queries - # (an explicit `AND (...)` wrapper 500s on free text). + # search/v1 only scopes via a `project:` RoboQL filter (body params are + # ignored). Leading space = implicit AND; `AND (...)` 500s on free-text queries. query = args.query project = getattr(args, "project", None) if project: diff --git a/tests/cli/test_image_handler.py b/tests/cli/test_image_handler.py index 650a3a61..9497630a 100644 --- a/tests/cli/test_image_handler.py +++ b/tests/cli/test_image_handler.py @@ -462,8 +462,7 @@ def test_search(self, mock_workspace_search): sys.stdout = old mock_workspace_search.assert_called_once() - # -p/--project must scope the search via a `project:` RoboQL filter, - # combined with the user's query (the API ignores body-level project params). + # -p must scope via a `project:` filter prepended to the query. called_query = mock_workspace_search.call_args.kwargs["query"] self.assertEqual(called_query, "project:proj tag:test") result = json.loads(buf.getvalue())