feat(client): add get_collection_names() shortcut method to all 6 client classes - #1305
feat(client): add get_collection_names() shortcut method to all 6 client classes#1305Harsh23Kashyap wants to merge 1 commit into
get_collection_names() shortcut method to all 6 client classes#1305Conversation
…lient classes `QdrantClient`, `AsyncQdrantClient`, `QdrantRemote`, `AsyncQdrantRemote`, `QdrantLocal`, and `AsyncQdrantLocal` all gain a public `get_collection_names()` method that returns a `list[str]` of all collection names. The common case -- 'I just want the names' -- currently requires `[c.name for c in client.get_collections().collections]`. The new method is a one-liner shortcut. - Remote mode: delegates to `get_collections()` and extracts `.name` from each `CollectionDescription`. No new network round-trip. - Local mode: reads `self.collections.keys()` directly. No intermediate `CollectionsResponse` model construction. - The async facade uses `inspect.iscoroutine` to handle both the sync (local) and async (remote) inner clients. The AST-based post-regen step in `generate_async_client.sh` re-injects `async def get_collection_names` into `async_qdrant_client.py` and `async_qdrant_remote.py` after every regen. The facade's injected method uses `inspect.iscoroutine`; the regen output lacks the import, so the sed step also adds `import inspect` at the top of the facade file (idempotent: skips if already present). The remote method is plain await + return -- no `inspect` needed, no spurious import. `AsyncQdrantLocal.get_collection_names()` stays a sync `def` (no `async def`) because it is a no-I/O `list(self.collections.keys())`. The transformer keeps it sync; no sed step needed. Fixes #1302
✅ Deploy Preview for poetic-froyo-8baba7 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughAdds Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
tools/generate_async_client.sh (1)
37-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNo re-format pass after the injection.
The script re-injects raw, hand-written text into the generated file at lines 37-108, but the earlier
ruff formatpass (lines 25-26) runs before injection, not after. If the project's formatting rules change later (line length, quote style, blank-line conventions), the injected text can silently drift out of format and fail a CI formatting check without an obvious cause tied to this script.Run
ruff format --line-length 99again on the two target files after the injection loop, mirroring lines 25-26, to guarantee the reinjected code always matches the project's current formatting rules regardless of future style changes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tools/generate_async_client.sh` around lines 37 - 108, Run the same ruff format --line-length 99 command used earlier on both target files after the injection loop in the generator script, ensuring reinjected get_collection_names methods are formatted before the script completes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@qdrant_client/local/async_qdrant_local.py`:
- Around line 686-690: Update the closed-state error raised by
get_collection_names to use the file-wide message “QdrantLocal instance is
closed. Please create a new instance.”, removing the Async prefix while
preserving the existing exception type and behavior.
In `@tests/test_get_collection_names.py`:
- Around line 158-160: Replace the hasattr-based check around
client.get_collection_names with a reliable async validation: use callable()
only to verify the member can be invoked, and rely on the existing
_inspect.iscoroutine(coro) assertion below to verify async behavior. Remove the
__await__/__call__ hasattr assertion without changing the surrounding test flow.
In `@tools/generate_async_client.sh`:
- Around line 73-85: The import insertion logic for inspect in the async client
generator must handle parenthesized multi-line imports safely. Replace the
line-based first_import_idx scan with AST parsing of text, find the end of the
last top-level ast.Import or ast.ImportFrom node, and insert import inspect
after that node, following the established close() insertion approach.
---
Nitpick comments:
In `@tools/generate_async_client.sh`:
- Around line 37-108: Run the same ruff format --line-length 99 command used
earlier on both target files after the injection loop in the generator script,
ensuring reinjected get_collection_names methods are formatted before the script
completes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 58919514-9312-49d7-96ff-eab7cb29edf8
📒 Files selected for processing (10)
qdrant_client/async_qdrant_client.pyqdrant_client/async_qdrant_remote.pyqdrant_client/local/async_qdrant_local.pyqdrant_client/local/qdrant_local.pyqdrant_client/qdrant_client.pyqdrant_client/qdrant_remote.pytests/test_get_collection_names.pytools/async_client_generator/client_generator.pytools/async_client_generator/remote_generator.pytools/generate_async_client.sh
| def get_collection_names(self) -> list[str]: | ||
| if self.closed: | ||
| raise RuntimeError("AsyncQdrantLocal instance is closed. Please create a new instance.") | ||
| return list(self.collections.keys()) | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the inconsistent closed-instance error message.
This method raises "AsyncQdrantLocal instance is closed. Please create a new instance." Every other closed-instance check in this same file uses "QdrantLocal instance is closed. Please create a new instance." without the Async prefix. Use the same text as the rest of the file.
✏️ Proposed fix
def get_collection_names(self) -> list[str]:
if self.closed:
- raise RuntimeError("AsyncQdrantLocal instance is closed. Please create a new instance.")
+ raise RuntimeError("QdrantLocal instance is closed. Please create a new instance.")
return list(self.collections.keys())📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def get_collection_names(self) -> list[str]: | |
| if self.closed: | |
| raise RuntimeError("AsyncQdrantLocal instance is closed. Please create a new instance.") | |
| return list(self.collections.keys()) | |
| def get_collection_names(self) -> list[str]: | |
| if self.closed: | |
| raise RuntimeError("QdrantLocal instance is closed. Please create a new instance.") | |
| return list(self.collections.keys()) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@qdrant_client/local/async_qdrant_local.py` around lines 686 - 690, Update the
closed-state error raised by get_collection_names to use the file-wide message
“QdrantLocal instance is closed. Please create a new instance.”, removing the
Async prefix while preserving the existing exception type and behavior.
| assert hasattr(client.get_collection_names, "__await__") or hasattr( | ||
| client.get_collection_names, "__call__" | ||
| ) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Replace unreliable hasattr callable check.
hasattr(x, "__call__") is an unreliable way to test callability. Bound methods always have __call__, so this assertion passes regardless of whether the method is async. Use callable(), or rely on the _inspect.iscoroutine(coro) check below, which already validates the async behavior.
✏️ Proposed fix
- assert hasattr(client.get_collection_names, "__await__") or hasattr(
- client.get_collection_names, "__call__"
- )
+ assert callable(client.get_collection_names)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| assert hasattr(client.get_collection_names, "__await__") or hasattr( | |
| client.get_collection_names, "__call__" | |
| ) | |
| assert callable(client.get_collection_names) |
🧰 Tools
🪛 Ruff (0.16.0)
[warning] 158-160: Using hasattr(x, "__call__") to test if x is callable is unreliable. Use callable(x) for consistent results.
Replace with callable()
(B004)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/test_get_collection_names.py` around lines 158 - 160, Replace the
hasattr-based check around client.get_collection_names with a reliable async
validation: use callable() only to verify the member can be invoked, and rely on
the existing _inspect.iscoroutine(coro) assertion below to verify async
behavior. Remove the __await__/__call__ hasattr assertion without changing the
surrounding test flow.
Source: Linters/SAST tools
| # Ensure `import inspect` is present at the top of the file when | ||
| # the injected method uses `inspect.iscoroutine` (the facade only; | ||
| # the remote method just awaits and returns). | ||
| if needs_inspect and "\nimport inspect\n" not in text and not text.startswith("import inspect\n"): | ||
| first_import_idx = 0 | ||
| for i, line in enumerate(text.splitlines(keepends=True)): | ||
| stripped = line.strip() | ||
| if stripped.startswith("import ") or stripped.startswith("from "): | ||
| first_import_idx = i + 1 | ||
| elif stripped and not stripped.startswith("#") and first_import_idx > 0: | ||
| break | ||
| lines = text.splitlines(keepends=True) | ||
| text = "".join(lines[:first_import_idx]) + "import inspect\n" + "".join(lines[first_import_idx:]) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check for multi-line parenthesized imports in the target files' leading import block.
rg -n '^(from|import) .*\($' qdrant_client/async_qdrant_client.py qdrant_client/async_qdrant_remote.py
rg -n -B2 -A5 '^from typing import|^from qdrant_client' qdrant_client/async_qdrant_client.py | head -60Repository: qdrant/qdrant-client
Length of output: 1104
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== script relevant section =="
sed -n '30,115p' tools/generate_async_client.sh
echo
echo "== target file import blocks =="
python3 - <<'PY'
from pathlib import Path
p = Path('qdrant_client/async_qdrant_remote.py')
lines = p.read_text().splitlines(keepends=True)
start=0
for i,l in enumerate(lines):
if l.startswith('class '):
start=i
break
for j,l in enumerate(lines[:start]):
print(f"{j+1}: {l!r}")
PY
echo
echo "== search ruff/black config =="
rg -n 'ruff|black|line-length|format' pyproject.toml ruff.toml .ruff.toml setup.cfg tox.ini tools/generate_async_client.sh README.md 2>/dev/null || true
echo
echo "== deterministic heuristic probe with multiline import and inspect iscoroutine usage =="
python3 - <<'PY'
text = 'from typing import (\n Any,\n Callable,\n)\nimport inspect\n\nclass Q:\n pass\n'
original_text = text
has_inspect = "\nimport inspect\n" in text or text.startswith("import inspect\n")
needs_inspect = True # equivalent to injected method using inspect.iscoroutine
if needs_inspect and "\nimport inspect\n" not in original_text and not original_text.startswith("import inspect\n"):
first_import_idx = 0
for i, line in enumerate(original_text.splitlines(keepends=True)):
stripped = line.strip()
if stripped.startswith("import ") or stripped.startswith("from "):
first_import_idx = i + 1
elif stripped and not stripped.startswith("#") and first_import_idx > 0:
break
lines = original_text.splitlines(keepends=True)
text = "".join(lines[:first_import_idx]) + "import inspect\n" + "".join(lines[first_import_idx:])
print("insertion_index_by_heuristic:", first_import_idx)
print("produced_lines:")
for i, line in enumerate(text.splitlines(keepends=True), 1):
print(f"{i}: {line!r}")
try:
compile(text, '<probe>', 'exec')
print("compiles: True")
except SyntaxError as e:
print("compiles: False")
print(f"SyntaxError: {e.msg} at line {e.lineno}: {e.text!r}")
PYRepository: qdrant/qdrant-client
Length of output: 6094
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== tool generator before insertion =="
sed -n '1,30p' tools/generate_async_client.sh
echo
echo "== deterministic heuristic probe from generator state =="
python3 - <<'PY'
from pathlib import Path
script = Path('tools/generate_async_client.sh').read_text()
import re
m = re.search(r'if needs_inspect and.*?text = "".join\(lines\[:first_import_idx\]\)', script, re.S)
print("Contains manual import-scanning block:", bool(m))
if m:
print(m.group(0)[:800].replace('\n', '\\n'))
PY
echo
echo "== deterministic heuristic probe with multiline import and inspect iscoroutine usage =="
python3 - <<'PY'
text = 'from typing import (\n Any,\n Callable,\n)\nimport inspect\n\nclass Q:\n pass\n'
needs_inspect = True
original_text = text
first_import_idx = 0
for i, line in enumerate(original_text.splitlines(keepends=True)):
stripped = line.strip()
if stripped.startswith("import ") or stripped.startswith("from "):
first_import_idx = i + 1
elif stripped and not stripped.startswith("#") and first_import_idx > 0:
break
lines = original_text.splitlines(keepends=True)
text = "".join(lines[:first_import_idx]) + "import inspect\n" + "".join(lines[first_import_idx:])
print("insertion_index_by_heuristic:", first_import_idx)
print("produced_lines:")
for i, line in enumerate(text.splitlines(keepends=True), 1):
print(f"{i}: {line!r}")
try:
compile(text, '<probe>', 'exec')
print("compiles: True")
except SyntaxError as e:
print("compiles: False")
print(f"SyntaxError: {e.msg} at line {e.lineno}: {e.text!r}")
PYRepository: qdrant/qdrant-client
Length of output: 2462
Use AST to insert import inspect after the last top-level import.
The loop only recognizes continuation lines that start with import or from . A parenthesized multi-line import has continuation lines that break this logic, so import inspect can be inserted inside the import statement and make the regenerated file fail to parse. Use ast.parse and insert after the last top-level Import/ImportFrom node, like the existing close() insertion path already does.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tools/generate_async_client.sh` around lines 73 - 85, The import insertion
logic for inspect in the async client generator must handle parenthesized
multi-line imports safely. Replace the line-based first_import_idx scan with AST
parsing of text, find the end of the last top-level ast.Import or ast.ImportFrom
node, and insert import inspect after that node, following the established
close() insertion approach.
Summary
Add a public
get_collection_names() -> list[str]method to all 6 client classes. The current workaround is a list comprehension over the existingget_collections().collections. The new method is the obvious one-liner shortcut for "I just want the names".Fixes #1302
Changes
qdrant_client/qdrant_client.py— facade sync method that delegates to the inner clientqdrant_client/async_qdrant_client.py— facade async method that usesinspect.iscoroutineto handle both sync (local) and async (remote) inner clientsqdrant_client/qdrant_remote.py— remote sync method,return [c.name for c in self.get_collections().collections]qdrant_client/async_qdrant_remote.py— remote async method,return [c.name for c in (await self.get_collections()).collections]qdrant_client/local/qdrant_local.py— local sync method, readsself.collections.keys()directly (noget_collections()round-trip, noCollectionsResponsemodel)qdrant_client/local/async_qdrant_local.py— local async method (kept as syncdefsince no I/O; the local transformer preserves it as sync)tools/async_client_generator/client_generator.py—get_collection_namesadded toexclude_methodstools/async_client_generator/remote_generator.py—get_collection_namesadded toexclude_methodstools/generate_async_client.sh— AST-based post-regen sed step re-injectsasync def get_collection_namesintoasync_qdrant_client.pyandasync_qdrant_remote.py. Also addsimport inspectto the facade (the remote method doesn't use it, so no spurious import). Fails loudly with non-zero exit ifclose()cannot be found.tests/test_get_collection_names.py— 14 new tests: sync + async × in-memory / path / remote × empty / after-create × list-not-model, plus local-direct-access verification (noget_collections()round-trip in local mode) and closed-state testsBehavior
Async equivalent (the async facade method is
asyncand must be awaited):Remote mode delegates to the existing
get_collections()call. Local mode readsself.collections.keys()directly — no intermediateCollectionsResponsemodel, no round-trip through the publicget_collections()API.Verification
.ossvenv/bin/python -m pytest tests/test_get_collection_names.py -v— 14/14 pass.ossvenv/bin/python -m ruff checkon the 9 changed source files — clean (the 3 pre-existing F541 errors onmaster/devare in the__init__of the remote classes, not in my added code).ossvenv/bin/python -m ruff format --check --line-length 99on the changed files — clean.ossvenv/bin/python -m mypyon the 6 source files — cleanbash tools/generate_async_client.shend-to-end with the new sed step:async def get_collection_namesis correctly re-injected intoasync_qdrant_client.pyandasync_qdrant_remote.py, andimport inspectis added to the facade (idempotent). Re-running the sed step is a no-op.Notes
dev(per the PR template and maintainer joein's 2026-07-21 guidance on PRs pointing the wrong base)generate_async_client.shis the only fragile part. Same AST-based pattern as feat(client): addqdrant_version()method to all 6 client classes #1294 / feat(client): addserver_info()method returning the fullVersionInfomodel #1296 / feat(client): add server_info() method returning the full VersionInfo model #1297, with one addition: the sed step also insertsimport inspectat the top ofasync_qdrant_client.py(the facade only — the remote method is plain await + return and doesn't need it). Idempotent: ifimport inspectis already present, the sed step skips it.AsyncQdrantLocal.get_collection_namesis a syncdef(no I/O), so the local transformer keeps it as sync without intervention.