fastapi-injected 0.3.4__tar.gz → 0.3.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fastapi-injected might be problematic. Click here for more details.
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/PKG-INFO +3 -3
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/README.md +2 -2
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/_bind.py +3 -2
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/_cache.py +17 -5
- fastapi_injected-0.3.5/fastapi_injected/_calls.py +192 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/_given.py +3 -2
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/deps.py +49 -22
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/resolve.py +7 -11
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/pyproject.toml +1 -1
- fastapi_injected-0.3.5/tests/test_calls.py +117 -0
- fastapi_injected-0.3.5/tests/test_dependant_cache.py +249 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/uv.lock +30 -30
- fastapi_injected-0.3.4/tests/test_dependant_cache.py +0 -92
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/.github/dependabot.yml +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/.github/workflows/automerge.yml +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/.github/workflows/lint.yml +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/.github/workflows/publish.yml +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/.github/workflows/test.yml +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/.gitignore +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/.pre-commit-config.yaml +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/LICENSE +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/__init__.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/_dataclass.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/_deps_tp.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/_fastapi_lifecycle.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/_injected.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/_overrides.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/_rlock.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/inject.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/overrides.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/scope.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/sign.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/fastapi_injected/types.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/__init__.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/__init__.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/annotations.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/arg.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/bind.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/dataclass.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/deps.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/errors.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/given.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/inject.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/injected.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/integration.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/markers.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/overrides.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/resolve.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/_typing/scope.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/deps.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/ext/__init__.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/ext/test_pydantic_ai.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/test_bind.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/test_cache.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/test_concurrency.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/test_dataclass.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/test_errors.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/test_fastapi.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/test_given.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/test_inject.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/test_injected.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/test_overrides.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/test_resolve.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/test_rlock.py +0 -0
- {fastapi_injected-0.3.4 → fastapi_injected-0.3.5}/tests/test_scope.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fastapi-injected
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Yet another library to reuse fastapi dependency injection
|
|
5
5
|
Project-URL: Repository, https://github.com/uriyyo/fastapi-injected
|
|
6
6
|
Author-email: Yurii Karabas <1998uriyyo@gmail.com>
|
|
@@ -164,7 +164,7 @@ async with push_inject_scope(app=app):
|
|
|
164
164
|
|
|
165
165
|
Without it, reading `request.app` raises a `KeyError` naming what is missing rather than a bare `'app'`.
|
|
166
166
|
|
|
167
|
-
Analysing a dependency is the expensive part of resolving one, so the result is cached — keyed by the dependency itself, not by whatever object carried it, so nothing that only passed through is kept alive. `clear_dependant_cache()` drops
|
|
167
|
+
Analysing a dependency is the expensive part of resolving one, so the result is cached — keyed by the dependency itself, not by whatever object carried it, so nothing that only passed through is kept alive. Only dependencies written in source code are cached this way: functions and classes, and annotations built from them. One made at runtime — `Given(...)`, a `MakeInjected` or `bind_deps` result, an object that brings its own `Depends` — is analysed again on every resolve, and FastAPI never gets to keep it: what it carries is released with the resolve instead of living as long as the process. `clear_dependant_cache()` drops the cache, for long-lived processes and test suites that want the memory back.
|
|
168
168
|
|
|
169
169
|
### Overriding dependencies
|
|
170
170
|
|
|
@@ -227,7 +227,7 @@ async with push_inject_scope({Session: fake}) as scope:
|
|
|
227
227
|
|
|
228
228
|
### Dependencies that are objects
|
|
229
229
|
|
|
230
|
-
|
|
230
|
+
Resolved values are cached in their scope by the callable that resolves them, so a dependency that is an object — a class holding configuration, a parametrized resolver — has to be hashable to get there. A plain dataclass is not, and a frozen one still refuses as soon as it holds a list or a dict.
|
|
231
231
|
|
|
232
232
|
`MakeDataclass` is a base class that makes its subclasses dataclasses with a hash that always answers: by fields when they can be hashed, by identity when they cannot.
|
|
233
233
|
|
|
@@ -143,7 +143,7 @@ async with push_inject_scope(app=app):
|
|
|
143
143
|
|
|
144
144
|
Without it, reading `request.app` raises a `KeyError` naming what is missing rather than a bare `'app'`.
|
|
145
145
|
|
|
146
|
-
Analysing a dependency is the expensive part of resolving one, so the result is cached — keyed by the dependency itself, not by whatever object carried it, so nothing that only passed through is kept alive. `clear_dependant_cache()` drops
|
|
146
|
+
Analysing a dependency is the expensive part of resolving one, so the result is cached — keyed by the dependency itself, not by whatever object carried it, so nothing that only passed through is kept alive. Only dependencies written in source code are cached this way: functions and classes, and annotations built from them. One made at runtime — `Given(...)`, a `MakeInjected` or `bind_deps` result, an object that brings its own `Depends` — is analysed again on every resolve, and FastAPI never gets to keep it: what it carries is released with the resolve instead of living as long as the process. `clear_dependant_cache()` drops the cache, for long-lived processes and test suites that want the memory back.
|
|
147
147
|
|
|
148
148
|
### Overriding dependencies
|
|
149
149
|
|
|
@@ -206,7 +206,7 @@ async with push_inject_scope({Session: fake}) as scope:
|
|
|
206
206
|
|
|
207
207
|
### Dependencies that are objects
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
Resolved values are cached in their scope by the callable that resolves them, so a dependency that is an object — a class holding configuration, a parametrized resolver — has to be hashable to get there. A plain dataclass is not, and a frozen one still refuses as soon as it holds a list or a dict.
|
|
210
210
|
|
|
211
211
|
`MakeDataclass` is a base class that makes its subclasses dataclasses with a hash that always answers: by fields when they can be hashed, by identity when they cannot.
|
|
212
212
|
|
|
@@ -6,6 +6,7 @@ from typing import Annotated, Any, Concatenate, get_origin, overload
|
|
|
6
6
|
from fastapi.dependencies.utils import get_typed_signature
|
|
7
7
|
from fastapi.params import Depends
|
|
8
8
|
|
|
9
|
+
from ._calls import annotated
|
|
9
10
|
from ._deps_tp import unwrap_tp
|
|
10
11
|
from .sign import update_func_sign
|
|
11
12
|
from .types import AsyncFunc, DepOf, Func
|
|
@@ -39,12 +40,12 @@ def _is_annotation(tp: Any, /) -> bool:
|
|
|
39
40
|
|
|
40
41
|
def _dep_annotation(dep: DepOf[Any] | Callable[..., Any], /) -> Any:
|
|
41
42
|
if isinstance(dep, Depends):
|
|
42
|
-
return
|
|
43
|
+
return annotated(Any, dep)
|
|
43
44
|
|
|
44
45
|
if _is_annotation(dep) or not callable(dep):
|
|
45
46
|
return dep
|
|
46
47
|
|
|
47
|
-
return
|
|
48
|
+
return annotated(Any, Depends(dep))
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
def signature_with_deps(
|
|
@@ -5,6 +5,7 @@ from typing import Any
|
|
|
5
5
|
from fastapi.dependencies.models import Dependant
|
|
6
6
|
from fastapi.types import DependencyCacheKey
|
|
7
7
|
|
|
8
|
+
from ._calls import unwrap_proxy
|
|
8
9
|
from ._dataclass import MakeDataclass
|
|
9
10
|
from .types import DependencyCache
|
|
10
11
|
|
|
@@ -16,13 +17,14 @@ def overridden_calls(dependant: Dependant, overrides: Collection[Any], /) -> fro
|
|
|
16
17
|
overridden: set[Any] = set()
|
|
17
18
|
|
|
18
19
|
def _visit(dep: Dependant, /) -> bool:
|
|
19
|
-
|
|
20
|
+
call = unwrap_proxy(dep.call)
|
|
21
|
+
is_overridden = call is not None and call in overrides
|
|
20
22
|
|
|
21
23
|
for sub_dep in dep.dependencies:
|
|
22
24
|
is_overridden = _visit(sub_dep) or is_overridden
|
|
23
25
|
|
|
24
|
-
if is_overridden and
|
|
25
|
-
overridden.add(
|
|
26
|
+
if is_overridden and call is not None:
|
|
27
|
+
overridden.add(call)
|
|
26
28
|
|
|
27
29
|
return is_overridden
|
|
28
30
|
|
|
@@ -30,6 +32,14 @@ def overridden_calls(dependant: Dependant, overrides: Collection[Any], /) -> fro
|
|
|
30
32
|
return frozenset(overridden)
|
|
31
33
|
|
|
32
34
|
|
|
35
|
+
def _cache_key(key: DependencyCacheKey, /) -> DependencyCacheKey:
|
|
36
|
+
# kept under the call a proxy stands for: the proxy is released once its resolve is
|
|
37
|
+
# done, the entry has to stay reachable for the rest of the scope
|
|
38
|
+
call, *rest = key
|
|
39
|
+
|
|
40
|
+
return (unwrap_proxy(call), *rest) # type: ignore[ty:invalid-return-type]
|
|
41
|
+
|
|
42
|
+
|
|
33
43
|
class ScopeCache(MakeDataclass, MutableMapping[DependencyCacheKey, Any]):
|
|
34
44
|
cache: DependencyCache
|
|
35
45
|
fallbacks: Sequence[tuple[Mapping[DependencyCacheKey, Any], frozenset[Any]]] = field(default_factory=tuple)
|
|
@@ -42,6 +52,8 @@ class ScopeCache(MakeDataclass, MutableMapping[DependencyCacheKey, Any]):
|
|
|
42
52
|
yield cache
|
|
43
53
|
|
|
44
54
|
def __getitem__(self, key: DependencyCacheKey) -> Any:
|
|
55
|
+
key = _cache_key(key)
|
|
56
|
+
|
|
45
57
|
try:
|
|
46
58
|
return self.cache[key]
|
|
47
59
|
except KeyError:
|
|
@@ -56,10 +68,10 @@ class ScopeCache(MakeDataclass, MutableMapping[DependencyCacheKey, Any]):
|
|
|
56
68
|
raise KeyError(key)
|
|
57
69
|
|
|
58
70
|
def __setitem__(self, key: DependencyCacheKey, value: Any) -> None:
|
|
59
|
-
self.cache[key] = value
|
|
71
|
+
self.cache[_cache_key(key)] = value
|
|
60
72
|
|
|
61
73
|
def __delitem__(self, key: DependencyCacheKey) -> None:
|
|
62
|
-
del self.cache[key]
|
|
74
|
+
del self.cache[_cache_key(key)]
|
|
63
75
|
|
|
64
76
|
def __iter__(self) -> Iterator[DependencyCacheKey]:
|
|
65
77
|
seen = set(self.cache)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import inspect
|
|
3
|
+
import typing
|
|
4
|
+
from collections.abc import Callable, Iterator, Mapping
|
|
5
|
+
from typing import Any, get_args, get_origin
|
|
6
|
+
|
|
7
|
+
from fastapi.dependencies.utils import get_typed_signature
|
|
8
|
+
from fastapi.params import Depends
|
|
9
|
+
|
|
10
|
+
from ._deps_tp import unwrap_tp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def annotated(tp: Any, /, *metadata: Any) -> Any:
|
|
14
|
+
# `Annotated[...]` goes through typing's own cache, which holds on to the last
|
|
15
|
+
# aliases it built - and with them whatever value a runtime marker carries
|
|
16
|
+
return typing._AnnotatedAlias(tp, metadata) # type: ignore[ty:unresolved-attribute] # noqa: SLF001
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_static_call(call: Any, /) -> bool:
|
|
20
|
+
# what source code wrote down lives as long as the process anyway, so caching
|
|
21
|
+
# by it keeps nothing extra alive - an instance or a closure may carry request data
|
|
22
|
+
if inspect.isclass(call):
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
# a builtin bound to an instance, like `cache.get`, carries that instance along
|
|
26
|
+
if inspect.isbuiltin(call):
|
|
27
|
+
return call.__self__ is None or inspect.ismodule(call.__self__)
|
|
28
|
+
|
|
29
|
+
# `__qualname__` is what `functools.wraps` copied over, the code knows where it was defined
|
|
30
|
+
if inspect.isfunction(call):
|
|
31
|
+
return "<locals>" not in call.__code__.co_qualname
|
|
32
|
+
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _depends_metadata(annotation: Any, /) -> tuple[Any, list[Any]] | None:
|
|
37
|
+
annotation = unwrap_tp(annotation)
|
|
38
|
+
|
|
39
|
+
if get_origin(annotation) is not typing.Annotated:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
tp, *metadata = get_args(annotation)
|
|
43
|
+
|
|
44
|
+
return tp, metadata
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_static_annotation(annotation: Any, /) -> bool:
|
|
48
|
+
match _depends_metadata(annotation):
|
|
49
|
+
case (tp, metadata):
|
|
50
|
+
return all(
|
|
51
|
+
is_static_call(tp if meta.dependency is None else meta.dependency)
|
|
52
|
+
for meta in metadata
|
|
53
|
+
if isinstance(meta, Depends)
|
|
54
|
+
)
|
|
55
|
+
case _:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class CallProxy:
|
|
60
|
+
# what FastAPI is handed in place of a call made at runtime: its classification
|
|
61
|
+
# caches keep every call they are shown, and once released this is all they keep
|
|
62
|
+
__slots__ = ("_hash", "_proxies", "_signature", "call")
|
|
63
|
+
|
|
64
|
+
def __init__(self, call: Callable[..., Any], proxies: list["CallProxy"], /) -> None:
|
|
65
|
+
self.call: Callable[..., Any] | None = call
|
|
66
|
+
self._proxies = proxies
|
|
67
|
+
self._signature: inspect.Signature | None = None
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
self._hash = hash(call)
|
|
71
|
+
except TypeError:
|
|
72
|
+
self._hash = id(call)
|
|
73
|
+
|
|
74
|
+
proxies.append(self)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def __wrapped__(self) -> Callable[..., Any]:
|
|
78
|
+
# FastAPI unwraps a call to tell a coroutine from a generator, so it sees the
|
|
79
|
+
# real one - a released proxy has nothing left to show
|
|
80
|
+
if self.call is None:
|
|
81
|
+
raise AttributeError("__wrapped__")
|
|
82
|
+
|
|
83
|
+
return self.call
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def __signature__(self) -> inspect.Signature:
|
|
87
|
+
# FastAPI builds the dependencies of this call from its signature, so whatever
|
|
88
|
+
# was made at runtime among them is handed over behind a proxy as well
|
|
89
|
+
if self._signature is None:
|
|
90
|
+
sign = get_typed_signature(self.__wrapped__)
|
|
91
|
+
self._signature = sign.replace(
|
|
92
|
+
parameters=[self._detach_param(param) for param in sign.parameters.values()],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return self._signature
|
|
96
|
+
|
|
97
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
98
|
+
return self.__wrapped__(*args, **kwargs)
|
|
99
|
+
|
|
100
|
+
def __hash__(self) -> int:
|
|
101
|
+
return self._hash
|
|
102
|
+
|
|
103
|
+
def __eq__(self, other: object) -> bool:
|
|
104
|
+
# equal to the call it stands for, so it finds the same cache entries and overrides
|
|
105
|
+
if self.call is None:
|
|
106
|
+
return other is self
|
|
107
|
+
|
|
108
|
+
return self.call == unwrap_proxy(other)
|
|
109
|
+
|
|
110
|
+
def release(self) -> None:
|
|
111
|
+
self.call = None
|
|
112
|
+
self._signature = None
|
|
113
|
+
self._proxies = []
|
|
114
|
+
|
|
115
|
+
def _detach_depends(self, depends: Depends, /) -> Depends:
|
|
116
|
+
if depends.dependency is None or is_static_call(depends.dependency):
|
|
117
|
+
return depends
|
|
118
|
+
|
|
119
|
+
return dataclasses.replace(depends, dependency=CallProxy(depends.dependency, self._proxies))
|
|
120
|
+
|
|
121
|
+
def _detach_param(self, param: inspect.Parameter, /) -> inspect.Parameter:
|
|
122
|
+
if isinstance(param.default, Depends):
|
|
123
|
+
param = param.replace(default=self._detach_depends(param.default))
|
|
124
|
+
|
|
125
|
+
match _depends_metadata(param.annotation):
|
|
126
|
+
case (tp, metadata):
|
|
127
|
+
detached = [self._detach_depends(meta) if isinstance(meta, Depends) else meta for meta in metadata]
|
|
128
|
+
|
|
129
|
+
if any(new is not old for new, old in zip(detached, metadata, strict=True)):
|
|
130
|
+
param = param.replace(annotation=annotated(tp, *detached))
|
|
131
|
+
|
|
132
|
+
return param
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class DetachedOverrides(Mapping[Any, Any]):
|
|
136
|
+
# an override replaces a call inside FastAPI, past where a proxy could be put on it,
|
|
137
|
+
# so the overrides of a resolve hand out what they hold behind one
|
|
138
|
+
__slots__ = ("_handed", "_overrides", "_proxies")
|
|
139
|
+
|
|
140
|
+
def __init__(self, overrides: Mapping[Any, Any], proxies: list[CallProxy], /) -> None:
|
|
141
|
+
self._overrides = overrides
|
|
142
|
+
self._proxies = proxies
|
|
143
|
+
self._handed: dict[int, CallProxy] = {}
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def dependency_overrides(self) -> Mapping[Any, Any]:
|
|
147
|
+
# FastAPI asks a provider for its overrides, and this is both
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
def __getitem__(self, key: Any) -> Any:
|
|
151
|
+
override = self._overrides[key]
|
|
152
|
+
|
|
153
|
+
if is_static_call(override):
|
|
154
|
+
return override
|
|
155
|
+
|
|
156
|
+
if (proxy := self._handed.get(id(override))) is None:
|
|
157
|
+
proxy = self._handed[id(override)] = CallProxy(override, self._proxies)
|
|
158
|
+
|
|
159
|
+
return proxy
|
|
160
|
+
|
|
161
|
+
def __iter__(self) -> Iterator[Any]:
|
|
162
|
+
return iter(self._overrides)
|
|
163
|
+
|
|
164
|
+
def __len__(self) -> int:
|
|
165
|
+
return len(self._overrides)
|
|
166
|
+
|
|
167
|
+
def __bool__(self) -> bool:
|
|
168
|
+
# FastAPI takes the override path only when there are some, and asks at every step
|
|
169
|
+
return bool(self._overrides)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def release(proxies: list[CallProxy], /) -> None:
|
|
173
|
+
for proxy in proxies:
|
|
174
|
+
proxy.release()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def unwrap_proxy(call: Any, /) -> Any:
|
|
178
|
+
if isinstance(call, CallProxy) and call.call is not None:
|
|
179
|
+
return call.call
|
|
180
|
+
|
|
181
|
+
return call
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
__all__ = [
|
|
185
|
+
"CallProxy",
|
|
186
|
+
"DetachedOverrides",
|
|
187
|
+
"annotated",
|
|
188
|
+
"is_static_annotation",
|
|
189
|
+
"is_static_call",
|
|
190
|
+
"release",
|
|
191
|
+
"unwrap_proxy",
|
|
192
|
+
]
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Any, cast
|
|
2
2
|
|
|
3
3
|
from fastapi import Depends
|
|
4
4
|
|
|
5
|
+
from ._calls import annotated
|
|
5
6
|
from ._dataclass import MakeDataclass
|
|
6
7
|
from .types import DepOf
|
|
7
8
|
|
|
@@ -16,7 +17,7 @@ class _Constant[R](MakeDataclass):
|
|
|
16
17
|
def Given[R](value: R, /) -> DepOf[R]: # noqa: N802
|
|
17
18
|
# a dependency that was already resolved by whoever built it - constants that hold
|
|
18
19
|
# the same value are the same dependency, so they are cached and overridden alike
|
|
19
|
-
return cast("DepOf[R]",
|
|
20
|
+
return cast("DepOf[R]", annotated(Any, Depends(_Constant(value))))
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
__all__ = [
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
from collections.abc import Callable
|
|
3
|
-
from contextlib import AsyncExitStack
|
|
2
|
+
from collections.abc import Callable, Iterator
|
|
3
|
+
from contextlib import AsyncExitStack, contextmanager
|
|
4
4
|
from functools import lru_cache, wraps
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Any, Literal, Protocol, cast, overload, runtime_checkable
|
|
6
6
|
|
|
7
7
|
from fastapi import Depends, params
|
|
8
8
|
from fastapi.dependencies.models import Dependant
|
|
9
9
|
from fastapi.dependencies.utils import get_dependant, get_typed_signature, solve_dependencies
|
|
10
10
|
from fastapi.exceptions import RequestValidationError
|
|
11
11
|
|
|
12
|
+
from ._calls import CallProxy, DetachedOverrides, annotated, is_static_annotation, release
|
|
12
13
|
from ._deps_tp import is_dep, unwrap_tp
|
|
13
14
|
from .scope import InjectScope
|
|
14
15
|
from .sign import prepare_sign, update_func_sign
|
|
@@ -55,25 +56,41 @@ def create_dependant[**P, R](func: Callable[P, Coro[R]], /) -> Dependant:
|
|
|
55
56
|
)
|
|
56
57
|
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
@contextmanager
|
|
60
|
+
def single_dependant[**P, R](
|
|
59
61
|
func: Callable[P, R] | HasDependsHook[P, R],
|
|
60
62
|
/,
|
|
61
63
|
*,
|
|
62
64
|
path: str | None = None,
|
|
63
|
-
) -> Dependant:
|
|
65
|
+
) -> Iterator[Dependant]:
|
|
64
66
|
match func:
|
|
65
67
|
case _ if is_dep(func):
|
|
66
68
|
annotation = unwrap_tp(func)
|
|
67
69
|
case HasDependsHook():
|
|
68
|
-
annotation =
|
|
70
|
+
annotation = annotated(Any, func.__get_depends__())
|
|
69
71
|
case _:
|
|
70
|
-
annotation =
|
|
72
|
+
annotation = annotated(Any, Depends(func))
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
if is_static_annotation(annotation):
|
|
75
|
+
yield _create_annotation_dependant(annotation, path=path)
|
|
76
|
+
return
|
|
73
77
|
|
|
78
|
+
# a dependency made at runtime is analysed again on every resolve, and FastAPI only
|
|
79
|
+
# ever sees its calls behind proxies released here - caching either would keep
|
|
80
|
+
# whatever it carries alive past the request
|
|
81
|
+
proxies: list[CallProxy] = []
|
|
82
|
+
dependant = get_dependant(
|
|
83
|
+
path=path or "",
|
|
84
|
+
call=CallProxy(_value_factory(annotation), proxies),
|
|
85
|
+
)
|
|
74
86
|
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
try:
|
|
88
|
+
yield dependant
|
|
89
|
+
finally:
|
|
90
|
+
release(proxies)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _value_factory(annotation: Any, /) -> Callable[..., Coro[Any]]:
|
|
77
94
|
async def _factory(__value__: Any) -> Any:
|
|
78
95
|
return __value__
|
|
79
96
|
|
|
@@ -88,9 +105,14 @@ def _create_annotation_dependant(annotation: Any, /, *, path: str | None = None)
|
|
|
88
105
|
return_annotation=Any,
|
|
89
106
|
)
|
|
90
107
|
|
|
108
|
+
return _factory
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@lru_cache(maxsize=1024)
|
|
112
|
+
def _create_annotation_dependant(annotation: Any, /, *, path: str | None = None) -> Dependant:
|
|
91
113
|
return get_dependant(
|
|
92
114
|
path=path or "",
|
|
93
|
-
call=
|
|
115
|
+
call=_value_factory(annotation),
|
|
94
116
|
)
|
|
95
117
|
|
|
96
118
|
|
|
@@ -122,16 +144,21 @@ async def resolve_dependencies(
|
|
|
122
144
|
*,
|
|
123
145
|
single: bool = False,
|
|
124
146
|
) -> dict[str, Any]:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
147
|
+
proxies: list[CallProxy] = []
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
async with scope.lock:
|
|
151
|
+
solved = await solve_dependencies(
|
|
152
|
+
request=scope.bound_request,
|
|
153
|
+
dependant=dependant,
|
|
154
|
+
dependency_cache=cast("DependencyCache", scope.cache_for(dependant)),
|
|
155
|
+
dependency_overrides_provider=DetachedOverrides(scope.dependency_overrides, proxies),
|
|
156
|
+
# this parameter is deprecated and not used
|
|
157
|
+
async_exit_stack=cast(AsyncExitStack, None),
|
|
158
|
+
embed_body_fields=False,
|
|
159
|
+
)
|
|
160
|
+
finally:
|
|
161
|
+
release(proxies)
|
|
135
162
|
|
|
136
163
|
if solved.errors:
|
|
137
164
|
raise DependencyResolutionError(solved.errors)
|
|
@@ -151,6 +178,6 @@ __all__ = [
|
|
|
151
178
|
"MissedDependencyError",
|
|
152
179
|
"clear_dependant_cache",
|
|
153
180
|
"create_dependant",
|
|
154
|
-
"create_single_dependant",
|
|
155
181
|
"resolve_dependencies",
|
|
182
|
+
"single_dependant",
|
|
156
183
|
]
|
|
@@ -3,7 +3,7 @@ from typing import Any, overload
|
|
|
3
3
|
|
|
4
4
|
from typing_extensions import TypeForm
|
|
5
5
|
|
|
6
|
-
from .deps import HasDependsHook,
|
|
6
|
+
from .deps import HasDependsHook, resolve_dependencies, single_dependant
|
|
7
7
|
from .scope import inside_inject_scope
|
|
8
8
|
from .types import DepDecl, DepShape
|
|
9
9
|
|
|
@@ -50,16 +50,12 @@ async def resolve(
|
|
|
50
50
|
async with inside_inject_scope(
|
|
51
51
|
new_scope=new_scope,
|
|
52
52
|
) as inject_scope:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
dependant,
|
|
60
|
-
inject_scope,
|
|
61
|
-
single=True,
|
|
62
|
-
)
|
|
53
|
+
with single_dependant(tp, path=inject_scope.path_format) as dependant:
|
|
54
|
+
return await resolve_dependencies(
|
|
55
|
+
dependant,
|
|
56
|
+
inject_scope,
|
|
57
|
+
single=True,
|
|
58
|
+
)
|
|
63
59
|
|
|
64
60
|
|
|
65
61
|
__all__ = [
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Annotated, Any
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from fastapi import Depends
|
|
7
|
+
|
|
8
|
+
from fastapi_injected import push_inject_scope, resolve
|
|
9
|
+
from fastapi_injected._calls import CallProxy, DetachedOverrides, is_static_annotation, is_static_call
|
|
10
|
+
|
|
11
|
+
pytestmark = pytest.mark.asyncio
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def module_level() -> int:
|
|
15
|
+
return 1
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _closure() -> Any:
|
|
19
|
+
def inner() -> int: # pragma: no cover
|
|
20
|
+
return 1
|
|
21
|
+
|
|
22
|
+
return inner
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _wrapped() -> Any:
|
|
26
|
+
@wraps(module_level)
|
|
27
|
+
def wrapper() -> int: # pragma: no cover
|
|
28
|
+
return 1
|
|
29
|
+
|
|
30
|
+
return wrapper
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Unhashable:
|
|
34
|
+
__hash__ = None # type: ignore[assignment]
|
|
35
|
+
|
|
36
|
+
def __call__(self) -> int:
|
|
37
|
+
return 1
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize(
|
|
41
|
+
("call", "expected"),
|
|
42
|
+
[
|
|
43
|
+
pytest.param(module_level, True, id="function"),
|
|
44
|
+
pytest.param(Unhashable, True, id="class"),
|
|
45
|
+
pytest.param(len, True, id="builtin"),
|
|
46
|
+
pytest.param(_closure(), False, id="closure"),
|
|
47
|
+
# `functools.wraps` copies the name of a function written at module level
|
|
48
|
+
pytest.param(_wrapped(), False, id="wrapped-closure"),
|
|
49
|
+
pytest.param({}.get, False, id="bound-builtin"),
|
|
50
|
+
pytest.param(Unhashable(), False, id="instance"),
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
async def test_what_counts_as_written_in_source(call: Any, expected: bool):
|
|
54
|
+
assert is_static_call(call) is expected
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def test_an_annotation_without_metadata_is_not_static():
|
|
58
|
+
assert not is_static_annotation(int)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def test_a_released_proxy_lets_go_of_its_call():
|
|
62
|
+
proxy = CallProxy(module_level, [])
|
|
63
|
+
proxy.release()
|
|
64
|
+
|
|
65
|
+
assert proxy.call is None
|
|
66
|
+
assert not hasattr(proxy, "__wrapped__")
|
|
67
|
+
assert proxy == proxy # noqa: PLR0124
|
|
68
|
+
assert proxy != module_level
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def test_a_proxy_of_an_unhashable_call_is_hashed_by_identity():
|
|
72
|
+
call = Unhashable()
|
|
73
|
+
proxy = CallProxy(call, [])
|
|
74
|
+
|
|
75
|
+
assert hash(proxy) == id(call)
|
|
76
|
+
assert proxy() == 1
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class One:
|
|
80
|
+
def __call__(self) -> int:
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Defaulted:
|
|
85
|
+
# a dependency that is an object, written as a default rather than in the annotation
|
|
86
|
+
def __init__(self, value: int) -> None:
|
|
87
|
+
self.value = value
|
|
88
|
+
|
|
89
|
+
async def __call__(self, one: int = Depends(One())) -> int:
|
|
90
|
+
return self.value + one
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def test_a_dependency_written_as_a_default_is_detached_too():
|
|
94
|
+
async with push_inject_scope():
|
|
95
|
+
assert await resolve(Defaulted(41)) == 42
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def test_a_proxy_shows_fastapi_the_signature_it_stands_for():
|
|
99
|
+
async def call(value: Annotated[int, Depends(module_level)]) -> int: # pragma: no cover
|
|
100
|
+
return value
|
|
101
|
+
|
|
102
|
+
# FastAPI reads the parameters only, its own typed signature drops the return annotation
|
|
103
|
+
assert inspect.signature(CallProxy(call, [])).parameters == inspect.signature(call).parameters
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def test_detached_overrides_hand_out_what_was_made_at_runtime_behind_a_proxy():
|
|
107
|
+
value = One()
|
|
108
|
+
proxies: list[CallProxy] = []
|
|
109
|
+
overrides = DetachedOverrides({module_level: value, One: module_level}, proxies)
|
|
110
|
+
|
|
111
|
+
assert overrides.dependency_overrides is overrides
|
|
112
|
+
assert (list(overrides), len(overrides), bool(overrides)) == ([module_level, One], 2, True)
|
|
113
|
+
assert overrides[One] is module_level # written in source, nothing to detach
|
|
114
|
+
assert overrides[module_level] is overrides[module_level]
|
|
115
|
+
assert overrides[module_level] == value
|
|
116
|
+
assert proxies == [value]
|
|
117
|
+
assert not DetachedOverrides({}, [])
|