Skip to content

Commit 3472339

Browse files
authored
Added LRU cache to WireupTask to prevent recompilation (#127)
* Added LRU cache to WireupTask to prevent recompilation * Fixed ruff formatting * Added the recommended changes * Added the recommended changes * Updated the recommended changes in the .md file and also added tests * Fixed error; moved helper functions to module level for caching * Made the ruff formatting changes
1 parent 865e72b commit 3472339

4 files changed

Lines changed: 83 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
## wireup.integration.fastapi
22

33
::: wireup.integration.fastapi
4+
5+
!!! note
6+
For best performance with `WireupTask`, prefer regular top-level functions for background task callbacks.
7+
Wireup creates cached injection wrappers for top-level functions, but callable objects and nested
8+
functions/closures do not benefit from that caching.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
## wireup.integration.flask
22

33
::: wireup.integration.starlette
4+
5+
!!! note
6+
For best performance with `WireupTask`, prefer regular top-level functions for background task callbacks.
7+
Wireup creates cached injection wrappers for top-level functions, but callable objects and nested
8+
functions/closures do not benefit from that caching.

test/integration/starlette/test_starlette_integration.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import types
12
from typing import Iterator
23
from uuid import uuid4
34

@@ -278,3 +279,68 @@ def test_setup_allows_reusing_container_across_apps() -> None:
278279

279280
wireup.integration.starlette.setup(container, app_one)
280281
wireup.integration.starlette.setup(container, app_two)
282+
283+
284+
def write_logs_a(greeter: Injected[GreeterService]) -> None:
285+
pass
286+
287+
288+
def write_logs_b(greeter: Injected[GreeterService]) -> None:
289+
pass
290+
291+
292+
def test_wireup_task_caches_regular_functions() -> None:
293+
container = wireup.create_async_container(injectables=[shared_services, wireup.integration.starlette])
294+
task = WireupTask(container)
295+
task._get_injected_wrapper.cache_clear()
296+
297+
task(write_logs_a)
298+
task(write_logs_a) # should hit cache
299+
task(write_logs_b)
300+
301+
info = task._get_injected_wrapper.cache_info()
302+
assert info.hits == 1
303+
assert info.misses == 2
304+
assert info.currsize == 2
305+
306+
307+
def test_wireup_task_does_not_cache_closures() -> None:
308+
def make_closure():
309+
def write_logs(greeter: Injected[GreeterService]) -> None:
310+
pass
311+
312+
return write_logs
313+
314+
closure_fn = make_closure()
315+
assert "<locals>" in closure_fn.__qualname__
316+
317+
container = wireup.create_async_container(injectables=[shared_services, wireup.integration.starlette])
318+
task = WireupTask(container)
319+
task._get_injected_wrapper.cache_clear()
320+
321+
task(closure_fn)
322+
task(closure_fn)
323+
324+
info = task._get_injected_wrapper.cache_info()
325+
assert info.hits == 0
326+
assert info.misses == 0
327+
328+
329+
def test_wireup_task_does_not_cache_callable_instances() -> None:
330+
class CallableTask:
331+
def __call__(self, greeter: Injected[GreeterService]) -> None:
332+
pass
333+
334+
callable_instance = CallableTask()
335+
assert not isinstance(callable_instance, types.FunctionType)
336+
337+
container = wireup.create_async_container(injectables=[shared_services, wireup.integration.starlette])
338+
task = WireupTask(container)
339+
task._get_injected_wrapper.cache_clear()
340+
341+
task(callable_instance)
342+
task(callable_instance)
343+
344+
info = task._get_injected_wrapper.cache_info()
345+
assert info.hits == 0
346+
assert info.misses == 0

wireup/integration/starlette.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import contextlib
2+
import types
23
from contextvars import ContextVar
4+
from functools import lru_cache
35
from typing import Any, AsyncIterator
46

57
from starlette.applications import Starlette
@@ -113,12 +115,16 @@ def get_request_container() -> ScopedAsyncContainer:
113115

114116

115117
class WireupTask:
116-
__slots__ = ("container",)
118+
__slots__ = ("_get_injected_wrapper", "container")
117119

118120
def __init__(self, container: AsyncContainer) -> None:
119121
self.container = container
122+
self._get_injected_wrapper = lru_cache(maxsize=128)(inject_from_container(self.container))
120123

121124
def __call__(self, fn: AnyCallable) -> Any:
125+
should_cache = isinstance(fn, types.FunctionType) and "<locals>" not in fn.__qualname__
126+
if should_cache:
127+
return self._get_injected_wrapper(fn)
122128
return inject_from_container(self.container)(fn)
123129

124130

0 commit comments

Comments
 (0)