Skip to content

feat(client): add __repr__ to the six client classes - #1330

Closed
Aryan-Pardeshi wants to merge 424 commits into
qdrant:devfrom
Aryan-Pardeshi:feat/client-repr
Closed

feat(client): add __repr__ to the six client classes#1330
Aryan-Pardeshi wants to merge 424 commits into
qdrant:devfrom
Aryan-Pardeshi:feat/client-repr

Conversation

@Aryan-Pardeshi

Copy link
Copy Markdown

Closes #1287.

repr() on any of the six client classes returned the default <qdrant_client.qdrant_client.QdrantClient object at 0x10c4a8eb0>. That shows up in tracebacks, in notebooks, and in structured logging libraries that render objects with repr() by default — and none of those places tell you which cluster the client was pointed at.

After

>>> QdrantClient("localhost", port=6333, prefer_grpc=True)
<QdrantClient mode=remote host='localhost:6333' prefer_grpc=True>
>>> QdrantClient(":memory:")
<QdrantClient mode=local location=':memory:'>
>>> QdrantClient(path="./storage")
<QdrantClient mode=local location='./storage'>
>>> QdrantRemote(url="https://api.qdrant.example:443")
<QdrantRemote scheme=https host='api.qdrant.example:443' prefer_grpc=False>
>>> QdrantLocal(":memory:")
<QdrantLocal location=':memory:'>

Async classes render the same way with their own class name, since the repr uses type(self).__name__.

api_key is deliberately not in there. Reprs end up in logs and exception reports, which is exactly where a key should not be. There's a test that constructs a client with an api_key and asserts the value appears in neither the facade's repr nor the inner client's.

One refactor worth flagging

QdrantRemote.__init__ already built f"{self._host}:{self._port}" if self._port is not None else self._host inline to make the base URL. The repr needs the identical string, so rather than write it twice I pulled it into a small _address property and pointed both at it. Behaviour is unchanged — same expression, same call site — but the repr and the base URL can't drift apart later.

Generated files

__repr__ isn't on AsyncQdrantBase, so the AST transformer leaves it sync in the async mirrors, which is what we want — Python calls repr() synchronously. No generator changes were needed. I ran the full generator anyway and confirmed its output matches what's committed here; the only other diffs were pre-existing artifacts of my local ruff version (for (k, v) in vs for k, v in) and autoflake not being installed, so I applied the __repr__ blocks to the generated files by hand rather than committing that noise.

Tests

tests/test_repr.py — 11 cases covering local and remote, sync and async, all six classes, plus the api_key leak check. No server needed.

Note: tests/test_local_persistence.py has 4 failures on my machine, but they reproduce on a clean master checkout too (a Windows PermissionError on the storage teardown), so they're unrelated to this change.

coszio and others added 30 commits July 1, 2024 22:51
* new: add query interface tests, fix version checking

* fix: add missing file
* fix: fix grpc conversion bugs for sparse and multi vectors

* fix: fix mypy
* chore: Added init_options property

* refactor: @Property

* test: init_opts

* chore: regen async client again with Py 3.10

* chore: deepcopy kwargs
* new: set idf modifier in fastembed mixin for bm models

* fix: update poetry lock

* fix: tmp fix, restrict pyright version

* refactor: update bm model extraction, update fastembed

* fix: fix list of bm models

* fix: fix default bm models list value

* refactoring: remove redundant import
* updated test migrate

* removed second remote client

* added back multiple vector test

* added single multi vector

* modified random_multivectors to support single vector

* fix: add version check for backward compatibility tests

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
* new: retry mechanism in migrate

* fixed misdeletion during merging

* fix: minor type hint update

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
* fix: do not modify input structs in-place

* fix: regen async
estebany-qd and others added 23 commits August 4, 2026 21:10
* fix: update poetry lock

* fix: add type annotations, update poetry.lock

* fix: fix local persistence tests

* fix: replace del client with client.close in local mode persistence tests
…ing field (qdrant#1224)

* fix: check_match() raises TypeError when MatchText applied to non-string field

* tests: move non-string match test to test_nested_filter, cover MatchText and MatchTextAny

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
* fix: spurious async client tests failures

* skip cluster-only test when server is standalone

* increase timeout for unit test performing multiple snapshot operations

* clean up stale snapshots left by previous runs

* fix: remove deleted methods, add/update cluster checks

* fix: remove unused import

* fix: remove redundant indent

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
* fix: update poetry lock

* fix: add type annotations, update poetry.lock

* fix: fix local persistence tests

* fix: replace del client with client.close in local mode persistence tests

* new: update local mode values count filter behaviour
Co-authored-by: Hassan Zafar <hassanzafar619@gmail.com>
)

* Fix local mode filters cross-matching booleans and integers

Python treats bool as a subclass of int (True == 1, False == 0), but Qdrant
keeps booleans and integers as distinct payload value types. Local mode
compared them with a plain `==` / `in` / `isinstance(value, (int, float))`, so:

- MatchValue(value=1) matched a payload of True, and MatchValue(value=True)
  matched a payload of 1 (same for 0 / False)
- MatchAny / MatchExcept cross-matched the same way
- Range matched booleans as if they were 0 / 1

The server never cross-matches these (its ValueVariants keeps Integer and Bool
distinct, and booleans are not numeric for range conditions). Add a type-aware
equality helper used by the value-match conditions, and exclude booleans from
range checks. Adds an in-memory regression test.

* Cover MatchExcept in the bool/int cross-match test

MatchExcept also routes through values_match, so assert that except=[1]
keeps the True payload (bool is not the integer 1).

* Add isolated MatchAny and range asserts to the bool/int cross-match test

Lock the single-value MatchAny path and the check_range bool guard against
regressions, in addition to the existing combined-condition coverage.

* fix: handle floats in cross-match local mode filters, add congruence tests

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
…dropping it (qdrant#1083) (qdrant#1247)

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
* fix: fix embed paths

* tests: add local inference test for complex prefetch
…on (qdrant#1260)

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
* new: 1.19.0 updates

* fix: fix search params as a dict in local mode

* fix: update qdrant backward compatibility version

* fix: add version check to the test

* fix: add version check to the test
* fix: fix nested payload local mode

* test: update test data in complex filter
repr() on any of the client classes returned the default
<qdrant_client.qdrant_client.QdrantClient object at 0x...>, which is
useless in tracebacks, notebooks and structured logs that render objects
with repr() by default.

QdrantRemote/AsyncQdrantRemote report scheme, host:port and prefer_grpc;
QdrantLocal/AsyncQdrantLocal report the location; the two facades report
the mode plus the inner client's connection details. api_key is left out
on purpose, since reprs reach logs.

The host:port formatting QdrantRemote.__init__ already did inline is now
a small _address property, so the repr and the base URL cannot drift.

Closes qdrant#1287
@netlify

netlify Bot commented Aug 10, 2026

Copy link
Copy Markdown

Deploy Preview for poetic-froyo-8baba7 ready!

Name Link
🔨 Latest commit 8e5c538
🔍 Latest deploy log https://app.netlify.com/projects/poetic-froyo-8baba7/deploys/6a7a2ab102586a000827e475
😎 Deploy Preview https://deploy-preview-1330--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 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Added __repr__ methods to synchronous and asynchronous facade, remote, and local clients. Facades report initialization state and connection or storage details. Remote clients centralize host-and-port formatting for REST URLs and representations. Representations omit API keys. Added tests for local paths, remote schemes, ports, gRPC preferences, and secret omission.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: joein

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The implementation meets the functional requirements, but the issue requires 12+ tests and the summary reports only 11 cases. Add at least one more test so tests/test_repr.py contains 12 or more cases, then rerun the relevant test suite.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The changes remain within scope: six repr methods, a shared address refactor, and focused representation tests.
Title check ✅ Passed The title clearly and concisely identifies the addition of repr to all six client classes.
Description check ✅ Passed The description directly explains the repr changes, secret redaction, refactor, generated files, and tests.
✨ 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)
tests/test_repr.py (1)

59-68: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Extend secret-redaction coverage to the asynchronous classes.

The parameterization contains only QdrantClient instances. It does not verify AsyncQdrantClient or AsyncQdrantRemote. Add asynchronous cases and assert that neither the secret value nor the api_key field appears in each representation.

🤖 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_repr.py` around lines 59 - 68, Extend
test_api_key_never_appears_in_repr to parameterize AsyncQdrantClient and
AsyncQdrantRemote instances alongside the existing QdrantClient cases. For every
client, assert that repr(client) and repr(client._client) contain neither the
secret value nor the literal "api_key".
🤖 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/async_qdrant_client.py`:
- Around line 158-163: Update AsyncQdrantClient.__repr__ remote formatting to
include the remote scheme using the same scheme value exposed by
AsyncQdrantRemote.__repr__, while preserving the existing host and prefer_grpc
details. Add an assertion covering the HTTPS representation so HTTP and HTTPS
endpoints remain distinguishable.

In `@qdrant_client/qdrant_remote.py`:
- Around line 296-301: Update __repr__ in qdrant_client/qdrant_remote.py (lines
296-301) and the equivalent representation in
qdrant_client/async_qdrant_remote.py (lines 236-237) to include _grpc_port when
_prefer_grpc is true, while preserving the current REST-only output otherwise.
- Around line 303-305: Update the _address and base_url builders in
qdrant_client/qdrant_remote.py (lines 303-305 and 207) and
qdrant_client/async_qdrant_remote.py (lines 239-241 and 168) to detect
unbracketed IPv6 literals and wrap them in brackets before composing the
authority, including when no port is configured. Preserve already-bracketed
hosts and existing IPv4/hostname behavior.

---

Nitpick comments:
In `@tests/test_repr.py`:
- Around line 59-68: Extend test_api_key_never_appears_in_repr to parameterize
AsyncQdrantClient and AsyncQdrantRemote instances alongside the existing
QdrantClient cases. For every client, assert that repr(client) and
repr(client._client) contain neither the secret value nor the literal "api_key".
🪄 Autofix

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: 0243daa6-a038-41fc-94a2-470ede9d8651

📥 Commits

Reviewing files that changed from the base of the PR and between 550484d and 8e5c538.

📒 Files selected for processing (7)
  • 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_repr.py

Comment on lines +158 to +163
def __repr__(self) -> str:
if not hasattr(self, "_client"):
return f"<{type(self).__name__} uninitialized>"
if isinstance(self._client, AsyncQdrantLocal):
return f"<{type(self).__name__} mode=local location={self._client.location!r}>"
return f"<{type(self).__name__} mode=remote host={self._client._address!r} prefer_grpc={self._client._prefer_grpc}>"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include the remote scheme in the facade representation.

Line 163 emits the host and port through _address, but it omits the scheme. The direct remote representation keeps scheme separate from host, so HTTP and HTTPS endpoints with the same host and port are indistinguishable through AsyncQdrantClient.

Add the same scheme value used by AsyncQdrantRemote.__repr__, and add an HTTPS facade assertion.

Proposed change
-        return f"<{type(self).__name__} mode=remote host={self._client._address!r} prefer_grpc={self._client._prefer_grpc}>"
+        return f"<{type(self).__name__} mode=remote scheme={self._client._scheme} host={self._client._address!r} prefer_grpc={self._client._prefer_grpc}>"
🤖 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/async_qdrant_client.py` around lines 158 - 163, Update
AsyncQdrantClient.__repr__ remote formatting to include the remote scheme using
the same scheme value exposed by AsyncQdrantRemote.__repr__, while preserving
the existing host and prefer_grpc details. Add an assertion covering the HTTPS
representation so HTTP and HTTPS endpoints remain distinguishable.

Comment on lines +296 to +301
def __repr__(self) -> str:
# api_key is deliberately absent: reprs end up in logs and tracebacks.
return (
f"<{type(self).__name__} scheme={self._scheme} host={self._address!r} "
f"prefer_grpc={self._prefer_grpc}>"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Expose the configured gRPC port in both remote representations.

When prefer_grpc=True, the current output shows only the REST port and omits the gRPC endpoint.

  • qdrant_client/qdrant_remote.py#L296-L301: include _grpc_port when gRPC is preferred.
  • qdrant_client/async_qdrant_remote.py#L236-L237: include _grpc_port with equivalent output semantics.
📍 Affects 2 files
  • qdrant_client/qdrant_remote.py#L296-L301 (this comment)
  • qdrant_client/async_qdrant_remote.py#L236-L237
🤖 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/qdrant_remote.py` around lines 296 - 301, Update __repr__ in
qdrant_client/qdrant_remote.py (lines 296-301) and the equivalent representation
in qdrant_client/async_qdrant_remote.py (lines 236-237) to include _grpc_port
when _prefer_grpc is true, while preserving the current REST-only output
otherwise.

Comment on lines +303 to +305
@property
def _address(self) -> str:
return f"{self._host}:{self._port}" if self._port is not None else self._host

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
set -eu

printf '%s\n' '--- qdrant_remote.py relevant definitions and usages ---'
rg -n -C 8 'def _address|base_url =|_host|_port|grpc_port|prefer_grpc' qdrant_client/qdrant_remote.py

printf '%s\n' '--- async_qdrant_remote.py relevant definitions and usages ---'
rg -n -C 8 'def _address|base_url =|_host|_port|grpc_port|prefer_grpc' qdrant_client/async_qdrant_remote.py

printf '%s\n' '--- IPv6-related tests and documentation ---'
rg -n -i -C 3 'ipv6|bracket|grpc_port|prefer_grpc' . -g '*.py' -g '*.md' -g '*.yaml' -g '*.yml' || true

Repository: qdrant/qdrant-client

Length of output: 1929


🌐 Web query:

qdrant-client qdrant_remote.py _address base_url IPv6

💡 Result:

In the qdrant-client library, the qdrant_remote.py module handles connection initialization, including the construction of the base_url used for REST requests [1][2]. The internal logic generally constructs the address by concatenating the host and port: address = f"{self._host}:{self._port}" if self._port is not None else self._host [1][2]. Because this construction simply joins these strings, providing a raw IPv6 address (which contains colons) as the host—for example,::1 or 2001:db8::1—without proper delimiters can cause the library to incorrectly parse the address and port, as the colons in the IPv6 address will be misinterpreted as separators [2][3]. To use an IPv6 address with the qdrant-client: 1. Wrap the IPv6 address in square brackets [ ] when passing it to the client, for example: [::1] [3]. 2. When configuring a Qdrant server to listen on IPv6, you may need to set the host to [::] in your configuration file or via the QDRANTSERVICEHOST environment variable [4]. In summary, the qdrant-client relies on string formatting that is not inherently IPv6-aware. Using square brackets around the IPv6 host address is the standard requirement to ensure the address and port are correctly disambiguated by the underlying URI and connection-handling utilities [3].

Citations:


🌐 Web query:

site:github.com/qdrant/qdrant-client "def _address" "qdrant_remote.py"

💡 Result:

In the context of the Qdrant Python client library, there is no method or function explicitly named _address defined in qdrant_remote.py [1][2][3][4]. The code inside qdrant_remote.py (and its asynchronous counterpart async_qdrant_remote.py) manages connection details by processing host, port, and URL parameters during object initialization [1][2][4]. Specifically, it constructs the target server address internally using local variables—often named address within the init method—to combine the host and port into a string that is subsequently used to form the base URL (base_url) and the final REST URI (rest_uri) for the Qdrant service [1][2][4]. If you encountered a reference to _address, it is likely an internal local variable used during the initialization logic rather than a public-facing method [1][2][4]. Accessing or relying on internal variables starting with an underscore is generally discouraged as they are subject to change in future versions of the client [1][2].

Citations:


🌐 Web query:

site:raw.githubusercontent.com/qdrant/qdrant-client cd5eb259 qdrant_client/qdrant_remote.py "_address"

💡 Result:

In the qdrant-client library, the qdrant_remote.py file does not contain a member named _address. Based on the source code provided in the search results for qdrant_remote.py [1], the class QdrantRemote manages connection information using attributes such as self._host, self._port, self._scheme, and self._prefix, which are derived from the url or host arguments provided during initialization [1][2]. There is no reference to an _address attribute in the provided definitions of QdrantRemote [1] or AsyncQdrantRemote [2].

Citations:


🌐 Web query:

site:raw.githubusercontent.com/qdrant/qdrant-client e7101dc5 qdrant_client/qdrant_remote.py "_address"

💡 Result:

The identifier e7101dc refers to a specific commit or branch reference in the qdrant/qdrant-client GitHub repository [1][2]. In the file qdrant_client/qdrant_remote.py at this commit, the _address attribute is not explicitly defined as a class variable or instance attribute in the provided source code [1]. The QdrantRemote class instead uses individual instance variables such as self._host, self._port, and self._scheme to manage connection details [1]. These values are derived during initialization from the provided url or host arguments [1]. If a url is provided, the client parses it to extract the host and port; otherwise, it defaults to localhost and the specified port [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from urllib.parse import urlsplit

for host in ("::1", "[::1]", "2001:db8::1", "[2001:db8::1]"):
    for port in (None, 6333):
        address = f"{host}:{port}" if port is not None else host
        url = f"http://{address}"
        try:
            parsed = urlsplit(url)
            print(f"{url!r} -> hostname={parsed.hostname!r}, port={parsed.port!r}")
        except ValueError as exc:
            print(f"{url!r} -> ValueError: {exc}")
PY

Repository: qdrant/qdrant-client

Length of output: 1929


Preserve IPv6 brackets in both remote address builders.

When _host is an unbracketed IPv6 literal, _address produces an invalid authority such as ::1:6333, and base_url becomes http://::1:6333. Bracket IPv6 literals before composing _address in both qdrant_remote.py and async_qdrant_remote.py. Cover the no-port case as well.

📍 Affects 2 files
  • qdrant_client/qdrant_remote.py#L303-L305 (this comment)
  • qdrant_client/qdrant_remote.py#L207-L207
  • qdrant_client/async_qdrant_remote.py#L239-L241
  • qdrant_client/async_qdrant_remote.py#L168-L168
🤖 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/qdrant_remote.py` around lines 303 - 305, Update the _address
and base_url builders in qdrant_client/qdrant_remote.py (lines 303-305 and 207)
and qdrant_client/async_qdrant_remote.py (lines 239-241 and 168) to detect
unbracketed IPv6 literals and wrap them in brackets before composing the
authority, including when no port is configured. Preserve already-bracketed
hosts and existing IPv4/hostname behavior.

@Aryan-Pardeshi

Copy link
Copy Markdown
Author

Closing in favour of #1346 — same change, branched cleanly off dev. This one came off master, which this repo doesn't take PRs against, and showed as conflicting once retargeted.

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.

feat(client): add __repr__ to QdrantClient, AsyncQdrantClient, QdrantRemote, AsyncQdrantRemote, QdrantLocal, AsyncQdrantLocal