From 234be7ee8104d446ffc0616ff6993389242dbdd0 Mon Sep 17 00:00:00 2001 From: Aryan Pardeshi Date: Tue, 11 Aug 2026 01:16:28 +0530 Subject: [PATCH 1/2] feat(client): add __repr__ to the six client classes repr() on any of the client classes returned the default , 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 #1287 --- qdrant_client/async_qdrant_client.py | 7 +++ qdrant_client/async_qdrant_remote.py | 10 +++- qdrant_client/local/async_qdrant_local.py | 3 + qdrant_client/local/qdrant_local.py | 3 + qdrant_client/qdrant_client.py | 13 +++++ qdrant_client/qdrant_remote.py | 14 ++++- tests/test_repr.py | 68 +++++++++++++++++++++++ 7 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 tests/test_repr.py diff --git a/qdrant_client/async_qdrant_client.py b/qdrant_client/async_qdrant_client.py index 4e58bd04d..b02dba085 100644 --- a/qdrant_client/async_qdrant_client.py +++ b/qdrant_client/async_qdrant_client.py @@ -155,6 +155,13 @@ def __init__( is_local_mode=isinstance(self._client, AsyncQdrantLocal), ) + 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}>" + async def close(self, grpc_grace: float | None = None, **kwargs: Any) -> None: """Closes the connection to Qdrant diff --git a/qdrant_client/async_qdrant_remote.py b/qdrant_client/async_qdrant_remote.py index ea139e182..db49fc27c 100644 --- a/qdrant_client/async_qdrant_remote.py +++ b/qdrant_client/async_qdrant_remote.py @@ -165,8 +165,7 @@ def __init__( "grpc.Compression.Deflate is not supported. Try grpc.Compression.Gzip or grpc.Compression.NoCompression" ) self._grpc_compression = grpc_compression - address = f"{self._host}:{self._port}" if self._port is not None else self._host - base_url = f"{self._scheme}://{address}" + base_url = f"{self._scheme}://{self._address}" self.rest_uri = urljoin(base_url, self._prefix) self._rest_args = {"headers": self._rest_headers, "http2": http2, **kwargs} if limits is not None: @@ -234,6 +233,13 @@ def _check_compatibility( stacklevel=2, ) + def __repr__(self) -> str: + return f"<{type(self).__name__} scheme={self._scheme} host={self._address!r} prefer_grpc={self._prefer_grpc}>" + + @property + def _address(self) -> str: + return f"{self._host}:{self._port}" if self._port is not None else self._host + @property def closed(self) -> bool: return self._closed diff --git a/qdrant_client/local/async_qdrant_local.py b/qdrant_client/local/async_qdrant_local.py index 971ad5e87..6462af0fb 100644 --- a/qdrant_client/local/async_qdrant_local.py +++ b/qdrant_client/local/async_qdrant_local.py @@ -88,6 +88,9 @@ def __init__(self, location: str, force_disable_check_same_thread: bool = False) self._load() self._closed: bool = False + def __repr__(self) -> str: + return f"<{type(self).__name__} location={self.location!r}>" + @property def closed(self) -> bool: return self._closed diff --git a/qdrant_client/local/qdrant_local.py b/qdrant_client/local/qdrant_local.py index 9e02df825..0daddc2f3 100644 --- a/qdrant_client/local/qdrant_local.py +++ b/qdrant_client/local/qdrant_local.py @@ -86,6 +86,9 @@ def __init__(self, location: str, force_disable_check_same_thread: bool = False) self._load() self._closed: bool = False + def __repr__(self) -> str: + return f"<{type(self).__name__} location={self.location!r}>" + @property def closed(self) -> bool: return self._closed diff --git a/qdrant_client/qdrant_client.py b/qdrant_client/qdrant_client.py index 5ee0f5c24..6cb2e9922 100644 --- a/qdrant_client/qdrant_client.py +++ b/qdrant_client/qdrant_client.py @@ -168,6 +168,19 @@ def __init__( def __del__(self) -> None: self.close() + def __repr__(self) -> str: + if not hasattr(self, "_client"): + # __init__ raised before the inner client was built. + return f"<{type(self).__name__} uninitialized>" + + if isinstance(self._client, QdrantLocal): + return f"<{type(self).__name__} mode=local location={self._client.location!r}>" + + return ( + f"<{type(self).__name__} mode=remote host={self._client._address!r} " + f"prefer_grpc={self._client._prefer_grpc}>" + ) + def close(self, grpc_grace: float | None = None, **kwargs: Any) -> None: """Closes the connection to Qdrant diff --git a/qdrant_client/qdrant_remote.py b/qdrant_client/qdrant_remote.py index 96789697d..607ee970d 100644 --- a/qdrant_client/qdrant_remote.py +++ b/qdrant_client/qdrant_remote.py @@ -204,8 +204,7 @@ def __init__( ) self._grpc_compression = grpc_compression - address = f"{self._host}:{self._port}" if self._port is not None else self._host - base_url = f"{self._scheme}://{address}" + base_url = f"{self._scheme}://{self._address}" self.rest_uri = urljoin(base_url, self._prefix) self._rest_args = {"headers": self._rest_headers, "http2": http2, **kwargs} @@ -294,6 +293,17 @@ def _check_compatibility( stacklevel=2, ) + 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}>" + ) + + @property + def _address(self) -> str: + return f"{self._host}:{self._port}" if self._port is not None else self._host + @property def closed(self) -> bool: return self._closed diff --git a/tests/test_repr.py b/tests/test_repr.py new file mode 100644 index 000000000..fb6d912fb --- /dev/null +++ b/tests/test_repr.py @@ -0,0 +1,68 @@ +import pytest + +from qdrant_client import AsyncQdrantClient, QdrantClient +from qdrant_client.async_qdrant_remote import AsyncQdrantRemote +from qdrant_client.local.async_qdrant_local import AsyncQdrantLocal +from qdrant_client.local.qdrant_local import QdrantLocal +from qdrant_client.qdrant_remote import QdrantRemote + + +def test_local_client_repr_shows_location(): + assert repr(QdrantClient(":memory:")) == "" + + +def test_async_local_client_repr_shows_location(): + assert ( + repr(AsyncQdrantClient(":memory:")) == "" + ) + + +def test_persistent_local_client_repr_shows_path(tmp_path): + path = str(tmp_path / "storage") + assert repr(QdrantClient(path=path)) == f"" + + +def test_remote_client_repr_shows_host_and_grpc_preference(): + client = QdrantClient("localhost", port=6333, prefer_grpc=True) + assert repr(client) == "" + + +def test_async_remote_client_repr_shows_host_and_grpc_preference(): + client = AsyncQdrantClient("localhost", port=6333, prefer_grpc=True) + assert repr(client) == "" + + +def test_remote_repr_shows_scheme(): + remote = QdrantRemote(url="https://api.qdrant.example:443") + assert ( + repr(remote) + == "" + ) + + +def test_async_remote_repr_shows_scheme(): + remote = AsyncQdrantRemote(url="https://api.qdrant.example:443") + assert ( + repr(remote) + == "" + ) + + +def test_local_repr(): + assert repr(QdrantLocal(":memory:")) == "" + + +def test_async_local_repr(): + assert repr(AsyncQdrantLocal(":memory:")) == "" + + +@pytest.mark.parametrize( + "client", + [ + QdrantClient("localhost", port=6333, api_key="super-secret-key"), + QdrantClient(url="https://api.qdrant.example:443", api_key="super-secret-key"), + ], +) +def test_api_key_never_appears_in_repr(client): + assert "super-secret-key" not in repr(client) + assert "super-secret-key" not in repr(client._client) From c615bc3086e5c647a69ae2cdf9042be410f51e84 Mon Sep 17 00:00:00 2001 From: Aryan Pardeshi Date: Sat, 15 Aug 2026 23:26:18 +0530 Subject: [PATCH 2/2] fix(repr): narrow to QdrantRemote before reading its private attrs _client is typed QdrantBase, which has no _address or _prefer_grpc, so mypy failed with attr-defined on both the sync and async clients. Branch on QdrantRemote/AsyncQdrantRemote and fall back to naming the client class for any other implementation. --- qdrant_client/async_qdrant_client.py | 4 +++- qdrant_client/qdrant_client.py | 11 +++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/qdrant_client/async_qdrant_client.py b/qdrant_client/async_qdrant_client.py index b02dba085..74a0125fd 100644 --- a/qdrant_client/async_qdrant_client.py +++ b/qdrant_client/async_qdrant_client.py @@ -160,7 +160,9 @@ def __repr__(self) -> str: 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}>" + if isinstance(self._client, AsyncQdrantRemote): + return f"<{type(self).__name__} mode=remote host={self._client._address!r} prefer_grpc={self._client._prefer_grpc}>" + return f"<{type(self).__name__} client={type(self._client).__name__}>" async def close(self, grpc_grace: float | None = None, **kwargs: Any) -> None: """Closes the connection to Qdrant diff --git a/qdrant_client/qdrant_client.py b/qdrant_client/qdrant_client.py index 6cb2e9922..23fe4f00d 100644 --- a/qdrant_client/qdrant_client.py +++ b/qdrant_client/qdrant_client.py @@ -176,10 +176,13 @@ def __repr__(self) -> str: if isinstance(self._client, QdrantLocal): return f"<{type(self).__name__} mode=local location={self._client.location!r}>" - return ( - f"<{type(self).__name__} mode=remote host={self._client._address!r} " - f"prefer_grpc={self._client._prefer_grpc}>" - ) + if isinstance(self._client, QdrantRemote): + return ( + f"<{type(self).__name__} mode=remote host={self._client._address!r} " + f"prefer_grpc={self._client._prefer_grpc}>" + ) + + return f"<{type(self).__name__} client={type(self._client).__name__}>" def close(self, grpc_grace: float | None = None, **kwargs: Any) -> None: """Closes the connection to Qdrant