Skip to content

feat(client): add get_collection_names() shortcut method to all 6 client classes - #1305

Closed
Harsh23Kashyap wants to merge 1 commit into
qdrant:devfrom
Harsh23Kashyap:feat/client-get-collection-names
Closed

feat(client): add get_collection_names() shortcut method to all 6 client classes#1305
Harsh23Kashyap wants to merge 1 commit into
qdrant:devfrom
Harsh23Kashyap:feat/client-get-collection-names

Conversation

@Harsh23Kashyap

Copy link
Copy Markdown

Summary

Add a public get_collection_names() -> list[str] method to all 6 client classes. The current workaround is a list comprehension over the existing get_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 client
  • qdrant_client/async_qdrant_client.py — facade async method that uses inspect.iscoroutine to handle both sync (local) and async (remote) inner clients
  • qdrant_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, reads self.collections.keys() directly (no get_collections() round-trip, no CollectionsResponse model)
  • qdrant_client/local/async_qdrant_local.py — local async method (kept as sync def since no I/O; the local transformer preserves it as sync)
  • tools/async_client_generator/client_generator.pyget_collection_names added to exclude_methods
  • tools/async_client_generator/remote_generator.pyget_collection_names added to exclude_methods
  • tools/generate_async_client.sh — AST-based post-regen sed step re-injects async def get_collection_names into async_qdrant_client.py and async_qdrant_remote.py. Also adds import inspect to the facade (the remote method doesn't use it, so no spurious import). Fails loudly with non-zero exit if close() 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 (no get_collections() round-trip in local mode) and closed-state tests

Behavior

client = QdrantClient(":memory:")
client.get_collection_names()  # -> []

client.create_collection("a", vectors_config=...)
client.create_collection("b", vectors_config=...)
client.get_collection_names()  # -> ['a', 'b']

Async equivalent (the async facade method is async and must be awaited):

client = AsyncQdrantClient(":memory:")
names = await client.get_collection_names()  # -> []

Remote mode delegates to the existing get_collections() call. Local mode reads self.collections.keys() directly — no intermediate CollectionsResponse model, no round-trip through the public get_collections() API.

Verification

  • .ossvenv/bin/python -m pytest tests/test_get_collection_names.py -v — 14/14 pass
  • .ossvenv/bin/python -m ruff check on the 9 changed source files — clean (the 3 pre-existing F541 errors on master/dev are in the __init__ of the remote classes, not in my added code)
  • .ossvenv/bin/python -m ruff format --check --line-length 99 on the changed files — clean
  • .ossvenv/bin/python -m mypy on the 6 source files — clean
  • bash tools/generate_async_client.sh end-to-end with the new sed step: async def get_collection_names is correctly re-injected into async_qdrant_client.py and async_qdrant_remote.py, and import inspect is added to the facade (idempotent). Re-running the sed step is a no-op.

Notes

…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
@netlify

netlify Bot commented Aug 2, 2026

Copy link
Copy Markdown

Deploy Preview for poetic-froyo-8baba7 ready!

Name Link
🔨 Latest commit 2bf6b9c
🔍 Latest deploy log https://app.netlify.com/projects/poetic-froyo-8baba7/deploys/6a6eec20c6fa380008f24646
😎 Deploy Preview https://deploy-preview-1305--poetic-froyo-8baba7.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds get_collection_names() to all synchronous and asynchronous client variants. Remote clients derive names from get_collections(). Local clients read collection keys and reject calls after closure. The asynchronous facade supports both coroutine-based remote clients and synchronous local clients. Async generation excludes and reinjects the custom methods. Regression tests cover local, remote, synchronous, asynchronous, and lifecycle behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested reviewers: joein, generall

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The implementation meets the functional objectives, but the issue requires 20+ tests and the PR adds only 14. Add the missing regression tests, or obtain approval to revise the linked issue's 20+ test requirement.
Out of Scope Changes check ⚠️ Warning The PR reformats two existing assertion and error messages in qdrant_local.py without functional relevance to the requested method. Revert the unrelated formatting changes in qdrant_local.py.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the addition of get_collection_names to all six client classes.
Description check ✅ Passed The description explains the new method, affected clients, implementation details, tests, and linked issue.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tools/generate_async_client.sh (1)

37-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No 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 format pass (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 99 again 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

📥 Commits

Reviewing files that changed from the base of the PR and between 399449a and 2bf6b9c.

📒 Files selected for processing (10)
  • qdrant_client/async_qdrant_client.py
  • qdrant_client/async_qdrant_remote.py
  • qdrant_client/local/async_qdrant_local.py
  • qdrant_client/local/qdrant_local.py
  • qdrant_client/qdrant_client.py
  • qdrant_client/qdrant_remote.py
  • tests/test_get_collection_names.py
  • tools/async_client_generator/client_generator.py
  • tools/async_client_generator/remote_generator.py
  • tools/generate_async_client.sh

Comment on lines +686 to +690
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())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
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.

Comment on lines +158 to +160
assert hasattr(client.get_collection_names, "__await__") or hasattr(
client.get_collection_names, "__call__"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
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

Comment on lines +73 to +85
# 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:])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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 -60

Repository: 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}")
PY

Repository: 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}")
PY

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant