Skip to content

Commit 744a614

Browse files
chrisguidryclaude
andauthored
Share _signature_cache with uncalled-for (#357)
`docket.execution` had its own `_signature_cache` dict, separate from the one in `uncalled_for.introspection`. FastMCP clears `docket.execution._signature_cache` after mutating function signatures (via `transform_context_annotations`), but `get_dependency_parameters` reads from uncalled-for's copy — so the stale entry stuck around and dependency resolution silently returned `{}`. Now both `_signature_cache` and `_parameter_cache` are re-exported from uncalled-for, so there's one shared dict for each. Added contract tests asserting cache identity. 🤖 Generated with Claude Code Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d2fa250 commit 744a614

File tree

3 files changed

+36
-15
lines changed

3 files changed

+36
-15
lines changed

src/docket/dependencies/_functional.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
SharedContext as SharedContext,
1212
)
1313
from uncalled_for.functional import _Depends as _UncalledForDepends
14+
15+
# Re-export _parameter_cache from uncalled-for so that docket and uncalled-for
16+
# share one cache dict. FastMCP clears `docket.dependencies._parameter_cache`
17+
# after mutating function signatures, so this must be the same object that
18+
# uncalled-for's get_dependency_parameters uses internally.
1419
from uncalled_for.introspection import (
1520
_parameter_cache as _parameter_cache,
1621
get_dependency_parameters,

src/docket/execution.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@
2525
from ._telemetry import suppress_instrumentation
2626
from typing_extensions import Self
2727

28+
# Re-export _signature_cache from uncalled-for so that docket and uncalled-for
29+
# share one cache dict. FastMCP clears `docket.execution._signature_cache` after
30+
# mutating function signatures, so this must be the same object that
31+
# uncalled-for's get_dependency_parameters uses internally.
32+
from uncalled_for.introspection import (
33+
_signature_cache as _signature_cache,
34+
get_signature as _uncalled_for_get_signature,
35+
)
36+
2837
from ._execution_progress import ExecutionProgress, ProgressEvent, StateEvent
2938
from .annotations import Logged
3039
from .instrumentation import CACHE_SIZE, message_getter, message_setter
@@ -51,22 +60,8 @@ async def __call__(
5160
) -> str: ... # pragma: no cover
5261

5362

54-
_signature_cache: dict[Callable[..., Any], inspect.Signature] = {}
55-
56-
5763
def get_signature(function: Callable[..., Any]) -> inspect.Signature:
58-
if function in _signature_cache:
59-
CACHE_SIZE.set(len(_signature_cache), {"cache": "signature"})
60-
return _signature_cache[function]
61-
62-
signature_attr = getattr(function, "__signature__", None)
63-
if isinstance(signature_attr, inspect.Signature):
64-
_signature_cache[function] = signature_attr
65-
CACHE_SIZE.set(len(_signature_cache), {"cache": "signature"})
66-
return signature_attr
67-
68-
signature = inspect.signature(function)
69-
_signature_cache[function] = signature
64+
signature = _uncalled_for_get_signature(function)
7065
CACHE_SIZE.set(len(_signature_cache), {"cache": "signature"})
7166
return signature
7267

tests/test_uncalled_for_contract.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,24 @@ def test_failed_dependency_captures_parameter_and_error():
201201

202202
assert failed.parameter == "my_param"
203203
assert failed.error is error
204+
205+
206+
# --- Cache identity ---
207+
# Docket re-exports _signature_cache and _parameter_cache from uncalled-for.
208+
# FastMCP clears these caches after mutating function signatures (e.g.
209+
# transform_context_annotations), so docket's re-exports MUST be the same
210+
# dict objects that uncalled-for uses internally.
211+
212+
213+
def test_signature_cache_is_shared_with_uncalled_for():
214+
from docket.execution import _signature_cache # pyright: ignore[reportPrivateUsage]
215+
from uncalled_for.introspection import _signature_cache as uf_cache # pyright: ignore[reportPrivateUsage]
216+
217+
assert _signature_cache is uf_cache
218+
219+
220+
def test_parameter_cache_is_shared_with_uncalled_for():
221+
from docket.dependencies import _parameter_cache # pyright: ignore[reportPrivateUsage]
222+
from uncalled_for.introspection import _parameter_cache as uf_cache # pyright: ignore[reportPrivateUsage]
223+
224+
assert _parameter_cache is uf_cache

0 commit comments

Comments
 (0)