Skip to content

Commit 3b465db

Browse files
refactor: stores register cleanup callbacks directly with exit stack
Previously, stores returned clients via `_get_client_for_context()` and the base class managed them. Now stores directly register their cleanup callbacks with the exit stack via `_register_cleanup_callbacks(stack)`. This gives stores more control over their cleanup registration and simplifies the base class by removing an abstraction layer. Also updated codegen to map AsyncExitStack -> ExitStack and enter_async_context -> enter_context for sync code generation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent ab49e98 commit 3b465db

File tree

21 files changed

+53
-87
lines changed

21 files changed

+53
-87
lines changed

key-value/key-value-aio/src/key_value/aio/stores/base.py

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -434,14 +434,13 @@ class BaseContextManagerStore(BaseStore, ABC):
434434
the constructor. This ensures the store does not manage the lifecycle of user-provided
435435
clients (i.e., does not close them).
436436
437-
Stores that have clients requiring context manager entry can override `_get_client_for_context()`
438-
to return the client that needs to be entered. The base class will automatically enter the client's
439-
context using an exit stack, which handles cleanup automatically.
437+
Stores that have clients requiring context manager entry should override
438+
`_register_cleanup_callbacks(stack)` to register their cleanup with the provided exit stack.
439+
This method is only called when the store owns the client (client_provided_by_user=False).
440440
"""
441441

442442
_client_provided_by_user: bool
443443
_exit_stack: AsyncExitStack | None
444-
_entered_client_result: Any
445444

446445
def __init__(self, *, client_provided_by_user: bool = False, **kwargs: Any) -> None:
447446
"""Initialize the context manager store with client ownership configuration.
@@ -454,32 +453,27 @@ def __init__(self, *, client_provided_by_user: bool = False, **kwargs: Any) -> N
454453
"""
455454
self._client_provided_by_user = client_provided_by_user
456455
self._exit_stack = None
457-
self._entered_client_result = None
458456
super().__init__(**kwargs)
459457

460-
def _get_client_for_context(self) -> Any | None:
461-
"""Return the client that needs context manager entry, or None if not applicable.
458+
async def _register_cleanup_callbacks(self, stack: AsyncExitStack) -> None:
459+
"""Register cleanup callbacks with the exit stack.
462460
463-
Stores with clients that require context manager entry (e.g., MongoDB's AsyncMongoClient,
464-
DynamoDB's aioboto3 client) should override this method to return the raw client object
465-
that needs to be entered.
461+
Stores should override this method to register their cleanup callbacks with the exit stack.
462+
This method is only called when the store owns the client (client_provided_by_user=False).
466463
467-
Returns:
468-
The client object that has __aenter__/__aexit__ methods, or None if no context
469-
management is needed.
470-
"""
471-
return None
464+
Examples:
465+
# For context manager clients:
466+
await stack.enter_async_context(self._client)
472467
473-
def _get_entered_client(self) -> Any | None:
474-
"""Get the result of entering the client's context manager.
468+
# For clients with close() methods:
469+
stack.push_async_callback(self._client.aclose)
475470
476-
Returns:
477-
The entered client (typically the client itself), or None if context wasn't entered.
471+
Args:
472+
stack: The AsyncExitStack to register cleanup callbacks with.
478473
"""
479-
return self._entered_client_result
480474

481475
async def _ensure_exit_stack(self) -> AsyncExitStack:
482-
"""Ensure the exit stack exists and enter client context if needed.
476+
"""Ensure the exit stack exists and register cleanup callbacks if needed.
483477
484478
Returns:
485479
The exit stack instance.
@@ -490,12 +484,9 @@ async def _ensure_exit_stack(self) -> AsyncExitStack:
490484
self._exit_stack = AsyncExitStack()
491485
await self._exit_stack.__aenter__()
492486

493-
# Enter client context if we own it
487+
# Register cleanup callbacks if we own the client
494488
if not self._client_provided_by_user:
495-
client = self._get_client_for_context()
496-
if client is not None and hasattr(client, "__aenter__"):
497-
result = await self._exit_stack.enter_async_context(client)
498-
self._entered_client_result = result
489+
await self._register_cleanup_callbacks(self._exit_stack)
499490

500491
return self._exit_stack
501492

key-value/key-value-aio/src/key_value/aio/stores/disk/multi_store.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ def _sync_close(self) -> None:
148148
for cache in self._cache.values():
149149
cache.close()
150150

151-
@override
152151
async def _close(self) -> None:
153152
self._sync_close()
154153

key-value/key-value-aio/src/key_value/aio/stores/disk/store.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ async def _delete_managed_entry(self, *, key: str, collection: str) -> bool:
126126

127127
return self._cache.delete(key=combo_key, retry=True)
128128

129-
@override
130129
async def _close(self) -> None:
131130
self._cache.close()
132131

key-value/key-value-aio/src/key_value/aio/stores/duckdb/store.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,6 @@ async def _delete_managed_entry(self, *, key: str, collection: str) -> bool:
362362
deleted_rows = result.fetchall()
363363
return len(deleted_rows) > 0
364364

365-
@override
366365
async def _close(self) -> None:
367366
"""Close the DuckDB connection."""
368367
if not self._is_closed:

key-value/key-value-aio/src/key_value/aio/stores/dynamodb/store.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from contextlib import AsyncExitStack
12
from datetime import datetime, timezone
23
from typing import TYPE_CHECKING, Any, overload
34

@@ -123,9 +124,10 @@ def __init__(
123124
)
124125

125126
@override
126-
def _get_client_for_context(self) -> Any | None:
127-
"""Return the raw client (context manager) for context management."""
128-
return self._raw_client if hasattr(self, "_raw_client") else None
127+
async def _register_cleanup_callbacks(self, stack: AsyncExitStack) -> None:
128+
"""Register DynamoDB client cleanup with the exit stack."""
129+
if hasattr(self, "_raw_client"):
130+
self._client = await stack.enter_async_context(self._raw_client)
129131

130132
@property
131133
def _connected_client(self) -> DynamoDBClient:
@@ -137,13 +139,6 @@ def _connected_client(self) -> DynamoDBClient:
137139
@override
138140
async def _setup(self) -> None:
139141
"""Setup the DynamoDB client and ensure table exists."""
140-
141-
# Get the entered client from the base class context management
142-
if not self._client:
143-
result = self._get_entered_client()
144-
if result is not None:
145-
self._client = result
146-
147142
try:
148143
await self._connected_client.describe_table(TableName=self._table_name) # pyright: ignore[reportUnknownMemberType]
149144
except self._connected_client.exceptions.ResourceNotFoundException: # pyright: ignore[reportUnknownMemberType]

key-value/key-value-aio/src/key_value/aio/stores/elasticsearch/store.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,5 @@ async def _cull(self) -> None:
552552
},
553553
)
554554

555-
@override
556555
async def _close(self) -> None:
557556
await self._client.close()

key-value/key-value-aio/src/key_value/aio/stores/memcached/store.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,5 @@ async def _delete_store(self) -> bool:
152152
_ = await self._client.flush_all()
153153
return True
154154

155-
@override
156155
async def _close(self) -> None:
157156
await self._client.close()

key-value/key-value-aio/src/key_value/aio/stores/mongodb/store.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from collections.abc import Sequence
2+
from contextlib import AsyncExitStack
23
from datetime import datetime, timezone
34
from typing import Any, overload
45

@@ -193,9 +194,9 @@ def __init__(
193194
)
194195

195196
@override
196-
def _get_client_for_context(self) -> Any | None:
197-
"""Return the MongoDB client for context management."""
198-
return self._client
197+
async def _register_cleanup_callbacks(self, stack: AsyncExitStack) -> None:
198+
"""Register MongoDB client cleanup with the exit stack."""
199+
await stack.enter_async_context(self._client)
199200

200201
@override
201202
async def _setup_collection(self, *, collection: str) -> None:

key-value/key-value-aio/src/key_value/aio/stores/redis/store.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,5 @@ async def _get_collection_keys(self, *, collection: str, limit: int | None = Non
222222
async def _delete_store(self) -> bool:
223223
return await self._client.flushdb() # pyright: ignore[reportUnknownMemberType, reportAny]
224224

225-
@override
226225
async def _close(self) -> None:
227226
await self._client.aclose()

key-value/key-value-aio/src/key_value/aio/stores/rocksdb/store.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ def __init__(
8585
client_provided_by_user=client_provided,
8686
)
8787

88-
@override
8988
async def _close(self) -> None:
9089
self._close_and_flush()
9190

0 commit comments

Comments
 (0)