You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(client): support with block on QdrantClient and async with on AsyncQdrantClient #1285
feat(client): support with block on QdrantClient and async with on AsyncQdrantClient
Problem
QdrantClient and AsyncQdrantClient own external resources (gRPC channels, the underlying httpx client, and in local mode the SQLite database plus the portalocker lockfile). Users have to call .close() manually, so a missed call or an unhandled exception leaks the gRPC channel and the httpx client.
The Python protocol for resource-owning objects is __enter__/__exit__ (sync) and __aenter__/__aexit__ (async). Neither is implemented on either client class. No open or closed issue mentions context-manager support; verified with gh search issues "context manager OR __enter__ OR __aenter__" against the repository.
Motivation
Every long-running qdrant-client process in a service needs a clean shutdown path. Right now the standard pattern is:
That is the literal code with exists to remove. A with QdrantClient(...) as client: block makes the cleanup path impossible to forget and matches the convention every other resource-owning client in the Python ecosystem uses (httpx.Client, boto3.client, redis.Redis, motor.AsyncIOMotorClient).
Background
QdrantClient.close() and AsyncQdrantClient.close() already exist (sync and async, both delegate to self._client.close()). The methods are idempotent — QdrantRemote.close() sets self._closed = True and QdrantLocal.close() sets self._closed = True and unlocks the portalocker file. The context manager just needs to call close() on exit.
The async file qdrant_client/async_qdrant_client.py is generated from the sync qdrant_client/qdrant_client.py by tools/async_client_generator/. The transformer pipeline converts def → async def for everything not in keep_sync and not in exclude_methods. To add __aenter__/__aexit__ to the async mirror, we need a regen-script hook (same pattern PR #1283 used for the http_client annotation remap).
Current behavior
with QdrantClient("localhost") as c: raises AttributeError: __enter__. async with AsyncQdrantClient("localhost") as c: raises TypeError: object async with can't be used in 'await' expression (or AttributeError: __aenter__). Users manage cleanup manually.
Expected behavior
Sync:
withQdrantClient("localhost") asclient:
client.upsert(...)
# client.close() called here, regardless of whether the block raised
Async:
asyncwithAsyncQdrantClient("localhost") asclient:
awaitclient.upsert(...)
# client.close() awaited here, regardless of whether the block raised
Works for both remote and local modes (QdrantRemote and QdrantLocal already implement idempotent close()).
Proposed solution
Add __enter__/__exit__ to QdrantClient in qdrant_client/qdrant_client.py (sync, one-liners).
Add __aenter__/__aexit__ to AsyncQdrantClient in qdrant_client/async_qdrant_client.py (async, one-liners).
Update tools/async_client_generator/client_generator.py to exclude __enter__ and __exit__ from the sync→async transformation (we don't want a sync __enter__ in the async mirror — Python's with would call it but the user expects the async one).
Add tests: tests/test_context_manager.py covering sync and async for both :memory: (local) and a mock-remote client. Tests cover the success path, the exception path, and idempotent re-entry.
Alternatives considered
Add __enter__/__exit__ only, async clients use aclose() manually: rejects the async context manager request. Goes against httpx.AsyncClient / motor.AsyncIOMotorClient convention.
Use @contextmanager decorator on a method: doesn't work because __enter__/__exit__ need to be defined on the class, not returned from a method.
Refactor to a ClientPool that handles lifecycle: out of scope. The existing close() is already correct; we just need to wire it to the context manager protocol.
Add __enter__/__exit__ to both sync and async facades (not just sync): rejected. Adding sync context manager methods to the async client is non-idiomatic — httpx.AsyncClient only has __aenter__/__aexit__. Users who need sync semantics on an async client are doing something unusual; the await client.close() call is fine for them.
Scope
Out of scope:
__enter__/__exit__ on the lower-level QdrantRemote/AsyncQdrantRemote/QdrantLocal (no public demand; users interact with the facade).
Async iterators / async generators on the client.
Changes to close() semantics.
In scope:
4 small methods on the two facade classes.
1 line in client_generator.py (add names to exclude_methods).
1 sed step in generate_async_client.sh.
1 test file.
Acceptance criteria
with QdrantClient(":memory:") as c: calls c.close() on exit (success and exception).
async with AsyncQdrantClient(":memory:") as c: awaits c.close() on exit (success and exception).
Calling close() manually before entering the with block is safe (idempotent).
with block works for both local and remote clients.
12+12 new tests in tests/test_context_manager.py, all pass alongside the existing suite.
mypy clean on the two facade files.
ruff check + format clean on the new test file.
Running bash tools/generate_async_client.sh regenerates __aenter__/__aexit__ correctly in the async mirror (regen test).
Backward compatibility
Fully additive. No public API change. The 4 new methods are all special-method names that don't conflict with anything.
Risks
The qdrant_client/async_qdrant_client.py is autogenerated; a future maintainer running bash tools/generate_async_client.sh without the new sed step would lose the __aenter__/__aexit__ methods. The sed step needs to be added in the same commit, and a regen test added so the loss is caught in CI.
Low risk of name collision: __enter__/__exit__/__aenter__/__aexit__ are all reserved special-method names; no user code can override them on the client instances.
Notes for the implementer
The base branch for this work is upstream/dev (not master). Maintainer joein closed docs(client): correct gRPC default timeout in QdrantClient docstring #1269 on 2026-07-21 with "All the PRs should point dev branch, not master", and the PR template at .github/PULL_REQUEST_TEMPLATE.md:4 says the same.
The test file should follow the pattern in tests/test_http_client_injection.py and tests/test_tracing.py (pytest, no live server, monkeypatch where needed).
feat(client): support
withblock on QdrantClient andasync withon AsyncQdrantClientProblem
QdrantClientandAsyncQdrantClientown external resources (gRPC channels, the underlying httpx client, and in local mode the SQLite database plus the portalocker lockfile). Users have to call.close()manually, so a missed call or an unhandled exception leaks the gRPC channel and the httpx client.The Python protocol for resource-owning objects is
__enter__/__exit__(sync) and__aenter__/__aexit__(async). Neither is implemented on either client class. No open or closed issue mentions context-manager support; verified withgh search issues "context manager OR __enter__ OR __aenter__"against the repository.Motivation
Every long-running qdrant-client process in a service needs a clean shutdown path. Right now the standard pattern is:
That is the literal code
withexists to remove. Awith QdrantClient(...) as client:block makes the cleanup path impossible to forget and matches the convention every other resource-owning client in the Python ecosystem uses (httpx.Client,boto3.client,redis.Redis,motor.AsyncIOMotorClient).Background
QdrantClient.close()andAsyncQdrantClient.close()already exist (sync and async, both delegate toself._client.close()). The methods are idempotent —QdrantRemote.close()setsself._closed = TrueandQdrantLocal.close()setsself._closed = Trueand unlocks the portalocker file. The context manager just needs to callclose()on exit.The async file
qdrant_client/async_qdrant_client.pyis generated from the syncqdrant_client/qdrant_client.pybytools/async_client_generator/. The transformer pipeline convertsdef→async deffor everything not inkeep_syncand not inexclude_methods. To add__aenter__/__aexit__to the async mirror, we need a regen-script hook (same pattern PR #1283 used for thehttp_clientannotation remap).Current behavior
with QdrantClient("localhost") as c:raisesAttributeError: __enter__.async with AsyncQdrantClient("localhost") as c:raisesTypeError: object async with can't be used in 'await' expression(orAttributeError: __aenter__). Users manage cleanup manually.Expected behavior
Sync:
Async:
Works for both remote and local modes (QdrantRemote and QdrantLocal already implement idempotent
close()).Proposed solution
__enter__/__exit__toQdrantClientinqdrant_client/qdrant_client.py(sync, one-liners).__aenter__/__aexit__toAsyncQdrantClientinqdrant_client/async_qdrant_client.py(async, one-liners).tools/async_client_generator/client_generator.pyto exclude__enter__and__exit__from the sync→async transformation (we don't want a sync__enter__in the async mirror — Python'swithwould call it but the user expects the async one).tools/generate_async_client.shthat re-injects the__aenter__/__aexit__block from a template after regen (mirrors the existingsedstep for thehttp_clientannotation remap from PR feat(remote): accept pre-built httpx.Client and httpx.AsyncClient #1283).tests/test_context_manager.pycovering sync and async for both:memory:(local) and a mock-remote client. Tests cover the success path, the exception path, and idempotent re-entry.Alternatives considered
__enter__/__exit__only, async clients useaclose()manually: rejects the async context manager request. Goes againsthttpx.AsyncClient/motor.AsyncIOMotorClientconvention.@contextmanagerdecorator on a method: doesn't work because__enter__/__exit__need to be defined on the class, not returned from a method.ClientPoolthat handles lifecycle: out of scope. The existingclose()is already correct; we just need to wire it to the context manager protocol.__enter__/__exit__to both sync and async facades (not just sync): rejected. Adding sync context manager methods to the async client is non-idiomatic —httpx.AsyncClientonly has__aenter__/__aexit__. Users who need sync semantics on an async client are doing something unusual; theawait client.close()call is fine for them.Scope
Out of scope:
__enter__/__exit__on the lower-levelQdrantRemote/AsyncQdrantRemote/QdrantLocal(no public demand; users interact with the facade).close()semantics.In scope:
client_generator.py(add names toexclude_methods).generate_async_client.sh.Acceptance criteria
with QdrantClient(":memory:") as c:callsc.close()on exit (success and exception).async with AsyncQdrantClient(":memory:") as c:awaitsc.close()on exit (success and exception).close()manually before entering thewithblock is safe (idempotent).withblock works for both local and remote clients.tests/test_context_manager.py, all pass alongside the existing suite.bash tools/generate_async_client.shregenerates__aenter__/__aexit__correctly in the async mirror (regen test).Backward compatibility
Fully additive. No public API change. The 4 new methods are all special-method names that don't conflict with anything.
Risks
qdrant_client/async_qdrant_client.pyis autogenerated; a future maintainer runningbash tools/generate_async_client.shwithout the new sed step would lose the__aenter__/__aexit__methods. The sed step needs to be added in the same commit, and a regen test added so the loss is caught in CI.__enter__/__exit__/__aenter__/__aexit__are all reserved special-method names; no user code can override them on the client instances.Notes for the implementer
upstream/dev(notmaster). Maintainer joein closed docs(client): correct gRPC default timeout in QdrantClient docstring #1269 on 2026-07-21 with "All the PRs should pointdevbranch, not master", and the PR template at.github/PULL_REQUEST_TEMPLATE.md:4says the same.tests/test_http_client_injection.pyandtests/test_tracing.py(pytest, no live server, monkeypatch where needed).