diff --git a/qdrant_client/async_qdrant_client.py b/qdrant_client/async_qdrant_client.py index 4e58bd04d..74a0125fd 100644 --- a/qdrant_client/async_qdrant_client.py +++ b/qdrant_client/async_qdrant_client.py @@ -155,6 +155,15 @@ 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}>" + 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/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..23fe4f00d 100644 --- a/qdrant_client/qdrant_client.py +++ b/qdrant_client/qdrant_client.py @@ -168,6 +168,22 @@ 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}>" + + 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 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)