fastapi-injected 0.2.0__tar.gz → 0.2.1__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.
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/PKG-INFO +3 -2
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/_deps_tp.py +9 -6
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/_fastapi_lifecycle.py +22 -8
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/deps.py +29 -6
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/overrides.py +26 -10
- fastapi_injected-0.2.1/fastapi_injected/resolve.py +56 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/scope.py +9 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/types.py +4 -1
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/pyproject.toml +2 -1
- fastapi_injected-0.2.1/tests/test_fastapi.py +121 -0
- fastapi_injected-0.2.1/tests/test_overrides.py +107 -0
- fastapi_injected-0.2.1/tests/test_resolve.py +67 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/uv.lock +201 -160
- fastapi_injected-0.2.0/fastapi_injected/resolve.py +0 -26
- fastapi_injected-0.2.0/tests/test_fastapi.py +0 -35
- fastapi_injected-0.2.0/tests/test_resolve.py +0 -32
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/.github/dependabot.yml +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/.github/workflows/automerge.yml +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/.github/workflows/lint.yml +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/.github/workflows/publish.yml +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/.github/workflows/test.yml +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/.gitignore +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/.pre-commit-config.yaml +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/LICENSE +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/README.md +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/__init__.py +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/inject.py +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/sign.py +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/tests/__init__.py +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/tests/deps.py +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/tests/ext/__init__.py +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/tests/ext/test_pydantic_ai.py +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/tests/test_inject.py +0 -0
- {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/tests/test_typing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: fastapi-injected
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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>
|
|
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
16
16
|
Requires-Python: >=3.12
|
|
17
17
|
Requires-Dist: fastapi>=0.139.2
|
|
18
18
|
Requires-Dist: typing-extensions>=4.16.0
|
|
19
|
+
Requires-Dist: typing-inspection>=0.4.4
|
|
19
20
|
Description-Content-Type: text/markdown
|
|
20
21
|
|
|
21
22
|
# fastapi-injected
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
from typing import Annotated, Any,
|
|
1
|
+
from typing import Annotated, Any, TypeVar, get_args, get_origin
|
|
2
2
|
|
|
3
3
|
from fastapi.params import Depends
|
|
4
|
+
from typing_inspection.typing_objects import is_typealiastype
|
|
4
5
|
|
|
5
6
|
|
|
6
|
-
def
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
def unwrap_tp(tp: Any) -> Any:
|
|
8
|
+
# PEP 695 aliases can be nested, e.g. `type A = B` where `type B = Annotated[...]`
|
|
9
|
+
while is_typealiastype(tp):
|
|
10
|
+
tp = tp.__value__
|
|
9
11
|
|
|
10
12
|
return tp
|
|
11
13
|
|
|
@@ -18,13 +20,13 @@ def _get_annotated_metadata(tp: Any) -> tuple[Any, ...]:
|
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
def is_dep(tp: Any) -> bool:
|
|
21
|
-
tp =
|
|
23
|
+
tp = unwrap_tp(tp)
|
|
22
24
|
|
|
23
25
|
return any(isinstance(tp, Depends) for tp in _get_annotated_metadata(tp))
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
def _enforce_dep(tp: Any) -> Any:
|
|
27
|
-
tp =
|
|
29
|
+
tp = unwrap_tp(tp)
|
|
28
30
|
|
|
29
31
|
if not is_dep(tp):
|
|
30
32
|
msg = f"Expected a Dep, got {tp!r}"
|
|
@@ -57,4 +59,5 @@ __all__ = [
|
|
|
57
59
|
"is_dep",
|
|
58
60
|
"unwrap_dep_dependency",
|
|
59
61
|
"unwrap_dep_tp",
|
|
62
|
+
"unwrap_tp",
|
|
60
63
|
]
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
from collections.abc import
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
2
|
from functools import wraps
|
|
3
3
|
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
|
-
from fastapi import Request, WebSocket, routing
|
|
5
|
+
from fastapi import Depends, FastAPI, Request, WebSocket, routing
|
|
6
6
|
from fastapi.dependencies import utils
|
|
7
7
|
|
|
8
|
+
from .overrides import push_overrides
|
|
8
9
|
from .scope import push_inject_scope
|
|
9
10
|
from .types import DependencyCache
|
|
10
11
|
|
|
@@ -49,14 +50,27 @@ def _get_dependency_cache(request: Request) -> DependencyCache:
|
|
|
49
50
|
raise MissingDependencyCacheError("Dependency cache not found") from None
|
|
50
51
|
|
|
51
52
|
|
|
52
|
-
async def init_inject_scope(request: Request) ->
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
async def init_inject_scope(request: Request) -> AsyncGenerator[None]:
|
|
54
|
+
with push_overrides(provider=request.scope["route"].dependency_overrides_provider):
|
|
55
|
+
async with push_inject_scope(
|
|
56
|
+
dependency_cache=_get_dependency_cache(request),
|
|
57
|
+
request=request,
|
|
58
|
+
):
|
|
59
|
+
yield
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def add_injected_scope(
|
|
63
|
+
app: FastAPI,
|
|
64
|
+
/,
|
|
65
|
+
) -> None:
|
|
66
|
+
if any(dep.dependency is init_inject_scope for dep in app.router.dependencies):
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
dependency = Depends(init_inject_scope)
|
|
70
|
+
app.router.dependencies.insert(0, dependency)
|
|
58
71
|
|
|
59
72
|
|
|
60
73
|
__all__ = [
|
|
74
|
+
"add_injected_scope",
|
|
61
75
|
"init_inject_scope",
|
|
62
76
|
]
|
|
@@ -4,17 +4,27 @@ from contextlib import AbstractContextManager, AsyncExitStack, contextmanager
|
|
|
4
4
|
from contextvars import ContextVar, Token
|
|
5
5
|
from copy import copy
|
|
6
6
|
from functools import lru_cache, wraps
|
|
7
|
-
from typing import Annotated, Any, Literal, cast, overload
|
|
7
|
+
from typing import Annotated, Any, Literal, Protocol, cast, overload, runtime_checkable
|
|
8
8
|
|
|
9
|
-
from fastapi import Depends
|
|
9
|
+
from fastapi import Depends, params
|
|
10
10
|
from fastapi.dependencies.models import Dependant
|
|
11
11
|
from fastapi.dependencies.utils import get_dependant, get_typed_signature, solve_dependencies
|
|
12
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
|
|
15
16
|
from .types import Coro, HasSignature
|
|
16
17
|
|
|
17
18
|
|
|
19
|
+
@runtime_checkable
|
|
20
|
+
class HasDependsHook[**P, R](Protocol):
|
|
21
|
+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Any:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def __get_depends__(self) -> params.Depends:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
18
28
|
@lru_cache(maxsize=1024)
|
|
19
29
|
def create_dependant[**P, R](func: Callable[P, Coro[R]], /) -> Dependant:
|
|
20
30
|
@wraps(func)
|
|
@@ -33,23 +43,36 @@ def create_dependant[**P, R](func: Callable[P, Coro[R]], /) -> Dependant:
|
|
|
33
43
|
|
|
34
44
|
|
|
35
45
|
@lru_cache(maxsize=1024)
|
|
36
|
-
def create_single_dependant[**P, R](
|
|
46
|
+
def create_single_dependant[**P, R](
|
|
47
|
+
func: Callable[P, R] | HasDependsHook[P, R],
|
|
48
|
+
/,
|
|
49
|
+
*,
|
|
50
|
+
path: str | None = None,
|
|
51
|
+
) -> Dependant:
|
|
37
52
|
async def _factory(__value__: R) -> R:
|
|
38
53
|
return __value__
|
|
39
54
|
|
|
55
|
+
match func:
|
|
56
|
+
case _ if is_dep(func):
|
|
57
|
+
annotation = unwrap_tp(func)
|
|
58
|
+
case HasDependsHook():
|
|
59
|
+
annotation = Annotated[Any, func.__get_depends__()]
|
|
60
|
+
case _:
|
|
61
|
+
annotation = Annotated[Any, Depends(func)]
|
|
62
|
+
|
|
40
63
|
cast("HasSignature", _factory).__signature__ = inspect.Signature(
|
|
41
64
|
parameters=[
|
|
42
65
|
inspect.Parameter(
|
|
43
66
|
"__value__",
|
|
44
67
|
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
45
|
-
annotation=
|
|
68
|
+
annotation=annotation,
|
|
46
69
|
),
|
|
47
70
|
],
|
|
48
71
|
return_annotation=Any,
|
|
49
72
|
)
|
|
50
73
|
|
|
51
74
|
return get_dependant(
|
|
52
|
-
path="",
|
|
75
|
+
path=path or "",
|
|
53
76
|
call=_factory,
|
|
54
77
|
)
|
|
55
78
|
|
|
@@ -82,7 +105,7 @@ async def resolve_dependencies(
|
|
|
82
105
|
request=scope.request,
|
|
83
106
|
dependant=dependant,
|
|
84
107
|
dependency_cache=copy(scope.dependency_cache),
|
|
85
|
-
dependency_overrides_provider=
|
|
108
|
+
dependency_overrides_provider=get_inject_dependency_override_provider(),
|
|
86
109
|
# this parameter is deprecated and not used
|
|
87
110
|
async_exit_stack=cast(AsyncExitStack, None),
|
|
88
111
|
embed_body_fields=False,
|
|
@@ -15,8 +15,9 @@ class NonFreshScopeError(Exception):
|
|
|
15
15
|
super().__init__("push_overrides requires a fresh scope")
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def
|
|
19
|
-
obj
|
|
18
|
+
def _get_overrides(obj: Any | None, /) -> Iterator[Mapping[Any, Any]]:
|
|
19
|
+
if obj is None:
|
|
20
|
+
return
|
|
20
21
|
|
|
21
22
|
if isinstance(obj, HasDependencyOverrides):
|
|
22
23
|
yield obj.dependency_overrides
|
|
@@ -33,7 +34,7 @@ def _get_override_key(tp: Any) -> Any:
|
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
@dataclass
|
|
36
|
-
class
|
|
37
|
+
class OverridesProvider:
|
|
37
38
|
dependency_overrides: Mapping[Any, Any]
|
|
38
39
|
|
|
39
40
|
|
|
@@ -77,30 +78,45 @@ def _enforce_fresh_scope() -> None:
|
|
|
77
78
|
raise NonFreshScopeError
|
|
78
79
|
|
|
79
80
|
|
|
81
|
+
def create_fallback_override_provider(
|
|
82
|
+
*,
|
|
83
|
+
overrides: Overrides | None = None,
|
|
84
|
+
provider: HasDependencyOverrides | None = None,
|
|
85
|
+
) -> OverridesProvider:
|
|
86
|
+
return OverridesProvider(
|
|
87
|
+
ChainMap(
|
|
88
|
+
{_get_override_key(k): _create_resolver(v) for k, v in (overrides or {}).items()},
|
|
89
|
+
*_get_overrides(provider), # type: ignore[ty:invalid-argument-type]
|
|
90
|
+
*_get_overrides(get_inject_dependency_override_provider()), # type: ignore[ty:invalid-argument-type]
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
80
95
|
@contextmanager
|
|
81
96
|
def push_overrides(
|
|
82
|
-
overrides: Overrides,
|
|
97
|
+
overrides: Overrides | None = None,
|
|
83
98
|
/,
|
|
84
99
|
*,
|
|
100
|
+
provider: HasDependencyOverrides | None = None,
|
|
85
101
|
require_fresh_scope: bool = True,
|
|
86
102
|
) -> Iterator[None]:
|
|
87
103
|
|
|
88
104
|
if require_fresh_scope:
|
|
89
105
|
_enforce_fresh_scope()
|
|
90
106
|
|
|
91
|
-
deps = ChainMap(
|
|
92
|
-
{_get_override_key(k): _create_resolver(v) for k, v in overrides.items()},
|
|
93
|
-
*_get_current_overrides(), # type: ignore[ty:invalid-argument-type]
|
|
94
|
-
)
|
|
95
|
-
|
|
96
107
|
with set_inject_dependency_override_provider(
|
|
97
|
-
|
|
108
|
+
create_fallback_override_provider(
|
|
109
|
+
overrides=overrides,
|
|
110
|
+
provider=provider,
|
|
111
|
+
),
|
|
98
112
|
):
|
|
99
113
|
yield
|
|
100
114
|
|
|
101
115
|
|
|
102
116
|
__all__ = [
|
|
103
117
|
"FactoryOverride",
|
|
118
|
+
"OverridesProvider",
|
|
104
119
|
"ValueOverride",
|
|
120
|
+
"create_fallback_override_provider",
|
|
105
121
|
"push_overrides",
|
|
106
122
|
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any, overload
|
|
3
|
+
|
|
4
|
+
from typing_extensions import TypeForm
|
|
5
|
+
|
|
6
|
+
from fastapi_injected.types import DepReturn
|
|
7
|
+
|
|
8
|
+
from .deps import HasDependsHook, create_single_dependant, resolve_dependencies
|
|
9
|
+
from .scope import inside_inject_scope
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@overload
|
|
13
|
+
async def resolve[**P, R](
|
|
14
|
+
tp: Callable[P, DepReturn[R]] | HasDependsHook[P, DepReturn[R]],
|
|
15
|
+
/,
|
|
16
|
+
*,
|
|
17
|
+
new_scope: bool = False,
|
|
18
|
+
) -> R:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@overload
|
|
23
|
+
async def resolve[R](
|
|
24
|
+
tp: TypeForm[R],
|
|
25
|
+
/,
|
|
26
|
+
*,
|
|
27
|
+
new_scope: bool = False,
|
|
28
|
+
) -> R:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def resolve(
|
|
33
|
+
tp: Any,
|
|
34
|
+
/,
|
|
35
|
+
*,
|
|
36
|
+
new_scope: bool = False,
|
|
37
|
+
) -> Any:
|
|
38
|
+
|
|
39
|
+
async with inside_inject_scope(
|
|
40
|
+
new_scope=new_scope,
|
|
41
|
+
) as inject_scope:
|
|
42
|
+
dependant = create_single_dependant(
|
|
43
|
+
tp,
|
|
44
|
+
path=inject_scope.path_format,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return await resolve_dependencies(
|
|
48
|
+
dependant,
|
|
49
|
+
inject_scope,
|
|
50
|
+
single=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"resolve",
|
|
56
|
+
]
|
|
@@ -7,6 +7,7 @@ from contextvars import ContextVar
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
|
|
9
9
|
from fastapi import Request
|
|
10
|
+
from fastapi.routing import APIRoute, APIWebSocketRoute
|
|
10
11
|
from starlette.types import Message, Scope
|
|
11
12
|
|
|
12
13
|
from .types import DependencyCache
|
|
@@ -50,6 +51,14 @@ class InjectScope:
|
|
|
50
51
|
dependency_cache: DependencyCache
|
|
51
52
|
request: Request
|
|
52
53
|
|
|
54
|
+
@property
|
|
55
|
+
def path_format(self) -> str | None:
|
|
56
|
+
match self.request.scope:
|
|
57
|
+
case {"route": APIRoute() | APIWebSocketRoute() as route}:
|
|
58
|
+
return route.path_format
|
|
59
|
+
case _:
|
|
60
|
+
return None
|
|
61
|
+
|
|
53
62
|
|
|
54
63
|
_inject_scope: ContextVar[InjectScope | None] = ContextVar(
|
|
55
64
|
"_inject_scope",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
from collections.abc import Callable, Coroutine, Mapping
|
|
2
|
+
from collections.abc import AsyncIterator, Callable, Coroutine, Iterator, Mapping
|
|
3
3
|
from typing import TYPE_CHECKING, Annotated, Any, Protocol, TypeVar, runtime_checkable
|
|
4
4
|
|
|
5
5
|
from fastapi import Depends
|
|
@@ -54,12 +54,15 @@ class HasDependencyOverrides(Protocol):
|
|
|
54
54
|
dependency_overrides: Mapping[Any, Any]
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
type DepReturn[R] = Coro[R] | Iterator[R] | AsyncIterator[R] | R
|
|
58
|
+
|
|
57
59
|
__all__ = [
|
|
58
60
|
"AsyncFunc",
|
|
59
61
|
"Coro",
|
|
60
62
|
"Decorator",
|
|
61
63
|
"Dep",
|
|
62
64
|
"DepFactory",
|
|
65
|
+
"DepReturn",
|
|
63
66
|
"DependencyCache",
|
|
64
67
|
"Func",
|
|
65
68
|
"HasDependencyOverrides",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fastapi-injected"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "Yet another library to reuse fastapi dependency injection"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Yurii Karabas", email = "1998uriyyo@gmail.com" },
|
|
@@ -20,6 +20,7 @@ classifiers = [
|
|
|
20
20
|
dependencies = [
|
|
21
21
|
"fastapi>=0.139.2",
|
|
22
22
|
"typing-extensions>=4.16.0",
|
|
23
|
+
"typing-inspection>=0.4.4",
|
|
23
24
|
]
|
|
24
25
|
|
|
25
26
|
[dependency-groups]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends, FastAPI, status
|
|
4
|
+
from fastapi.testclient import TestClient
|
|
5
|
+
|
|
6
|
+
from fastapi_injected import Dep, Injected, init_inject_scope, inject, resolve
|
|
7
|
+
from fastapi_injected._fastapi_lifecycle import add_injected_scope
|
|
8
|
+
|
|
9
|
+
from .deps import Child, Container
|
|
10
|
+
|
|
11
|
+
app = FastAPI(
|
|
12
|
+
dependencies=[
|
|
13
|
+
Depends(init_inject_scope),
|
|
14
|
+
],
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
client = TestClient(app)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@inject
|
|
21
|
+
async def _func(
|
|
22
|
+
*,
|
|
23
|
+
container: Dep[Container] = Injected,
|
|
24
|
+
) -> Container:
|
|
25
|
+
return container
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.get("/")
|
|
29
|
+
async def route(container: Dep[Container]) -> str:
|
|
30
|
+
assert await resolve(Container) is container
|
|
31
|
+
assert await _func() is container
|
|
32
|
+
|
|
33
|
+
return ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_cache_is_working() -> None:
|
|
37
|
+
result = client.get("/")
|
|
38
|
+
assert result.status_code == status.HTTP_200_OK
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def _item_id_dep(item_id: int) -> int:
|
|
42
|
+
return item_id
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
type ItemID = Annotated[int, Depends(_item_id_dep)]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.get("/items/{item_id}")
|
|
49
|
+
async def item_route(item_id: ItemID) -> int:
|
|
50
|
+
assert await resolve(ItemID) == item_id
|
|
51
|
+
|
|
52
|
+
return item_id
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_resolve_uses_route_path() -> None:
|
|
56
|
+
result = client.get("/items/42")
|
|
57
|
+
|
|
58
|
+
assert result.status_code == status.HTTP_200_OK
|
|
59
|
+
assert result.json() == 42
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_add_injected_scope() -> None:
|
|
63
|
+
other_app = FastAPI()
|
|
64
|
+
assert not other_app.router.dependencies
|
|
65
|
+
|
|
66
|
+
add_injected_scope(other_app)
|
|
67
|
+
assert [dep.dependency for dep in other_app.router.dependencies] == [init_inject_scope]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_add_injected_scope_is_idempotent() -> None:
|
|
71
|
+
other_app = FastAPI(dependencies=[Depends(_item_id_dep)])
|
|
72
|
+
|
|
73
|
+
add_injected_scope(other_app)
|
|
74
|
+
add_injected_scope(other_app)
|
|
75
|
+
|
|
76
|
+
assert [dep.dependency for dep in other_app.router.dependencies] == [
|
|
77
|
+
init_inject_scope,
|
|
78
|
+
_item_id_dep,
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
overridden_app = FastAPI()
|
|
83
|
+
add_injected_scope(overridden_app)
|
|
84
|
+
|
|
85
|
+
overridden_client = TestClient(overridden_app)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@inject
|
|
89
|
+
async def _injected_child(
|
|
90
|
+
*,
|
|
91
|
+
child: Dep[Child] = Injected,
|
|
92
|
+
) -> Child:
|
|
93
|
+
return child
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@overridden_app.get("/")
|
|
97
|
+
async def overridden_route() -> str:
|
|
98
|
+
container = await resolve(Container)
|
|
99
|
+
|
|
100
|
+
assert container.child is _overridden_child
|
|
101
|
+
assert await _injected_child() is _overridden_child
|
|
102
|
+
|
|
103
|
+
return ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
_overridden_child = Child()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def _override_child() -> Child:
|
|
110
|
+
return _overridden_child
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_app_dependency_overrides_are_used() -> None:
|
|
114
|
+
overridden_app.dependency_overrides[Child] = _override_child
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
result = overridden_client.get("/")
|
|
118
|
+
finally:
|
|
119
|
+
overridden_app.dependency_overrides.clear()
|
|
120
|
+
|
|
121
|
+
assert result.status_code == status.HTTP_200_OK
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from fastapi_injected import Dep, ValueOverride, push_inject_scope, push_overrides, resolve
|
|
8
|
+
from fastapi_injected.overrides import (
|
|
9
|
+
NonFreshScopeError,
|
|
10
|
+
OverridesProvider,
|
|
11
|
+
create_fallback_override_provider,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .deps import Child, Container, ContextState, ctx_dep
|
|
15
|
+
|
|
16
|
+
pytestmark = pytest.mark.asyncio
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Provider:
|
|
21
|
+
dependency_overrides: Mapping[Any, Any] = field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _child_factory(child: Child) -> Any:
|
|
25
|
+
async def _factory() -> Child:
|
|
26
|
+
return child
|
|
27
|
+
|
|
28
|
+
return _factory
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def test_push_overrides():
|
|
32
|
+
child = Child()
|
|
33
|
+
|
|
34
|
+
with push_overrides({Child: ValueOverride(child)}):
|
|
35
|
+
container = await resolve(Container)
|
|
36
|
+
|
|
37
|
+
assert container.child is child
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def test_push_overrides_provider():
|
|
41
|
+
child = Child()
|
|
42
|
+
provider = Provider({Child: _child_factory(child)})
|
|
43
|
+
|
|
44
|
+
with push_overrides(provider=provider):
|
|
45
|
+
container = await resolve(Container)
|
|
46
|
+
|
|
47
|
+
assert container.child is child
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def test_push_overrides_without_anything():
|
|
51
|
+
with push_overrides():
|
|
52
|
+
container = await resolve(Container)
|
|
53
|
+
|
|
54
|
+
assert isinstance(container.child, Child)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def test_push_overrides_take_precedence_over_provider():
|
|
58
|
+
from_overrides, from_provider = Child(), Child()
|
|
59
|
+
provider = Provider({Child: _child_factory(from_provider)})
|
|
60
|
+
|
|
61
|
+
with push_overrides({Child: ValueOverride(from_overrides)}, provider=provider):
|
|
62
|
+
container = await resolve(Container)
|
|
63
|
+
|
|
64
|
+
assert container.child is from_overrides
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def test_push_overrides_fallback_to_outer_provider():
|
|
68
|
+
child, ctx = Child(), ContextState()
|
|
69
|
+
|
|
70
|
+
with push_overrides({Child: ValueOverride(child)}):
|
|
71
|
+
with push_overrides({ctx_dep: ValueOverride(ctx)}, require_fresh_scope=False):
|
|
72
|
+
container = await resolve(Container)
|
|
73
|
+
|
|
74
|
+
# `ctx` comes from the inner overrides, `child` falls back to the outer ones
|
|
75
|
+
assert container.ctx is ctx
|
|
76
|
+
assert container.child is child
|
|
77
|
+
|
|
78
|
+
container = await resolve(Container)
|
|
79
|
+
|
|
80
|
+
assert container.ctx is not ctx
|
|
81
|
+
assert container.child is child
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def test_push_overrides_provider_fallback_to_outer_provider():
|
|
85
|
+
outer = Child()
|
|
86
|
+
provider = Provider()
|
|
87
|
+
|
|
88
|
+
with push_overrides({Child: ValueOverride(outer)}), push_overrides(provider=provider, require_fresh_scope=False):
|
|
89
|
+
container = await resolve(Container)
|
|
90
|
+
|
|
91
|
+
assert container.child is outer
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def test_push_overrides_requires_fresh_scope():
|
|
95
|
+
async with push_inject_scope():
|
|
96
|
+
await resolve(Container)
|
|
97
|
+
|
|
98
|
+
with pytest.raises(NonFreshScopeError), push_overrides({Child: ValueOverride(Child())}):
|
|
99
|
+
pass # pragma: no cover
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def test_create_fallback_override_provider():
|
|
103
|
+
child = Child()
|
|
104
|
+
provider = create_fallback_override_provider(overrides={Dep[Child]: ValueOverride(child)})
|
|
105
|
+
|
|
106
|
+
assert isinstance(provider, OverridesProvider)
|
|
107
|
+
assert list(provider.dependency_overrides) == [Child]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from fastapi import Depends, params
|
|
5
|
+
|
|
6
|
+
from fastapi_injected import Dep, push_inject_scope, resolve
|
|
7
|
+
|
|
8
|
+
pytestmark = pytest.mark.asyncio
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Foo:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Bar:
|
|
18
|
+
foo: Dep[Foo]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def test_resolve():
|
|
22
|
+
bar = await resolve(Bar)
|
|
23
|
+
|
|
24
|
+
assert isinstance(bar, Bar)
|
|
25
|
+
assert isinstance(bar.foo, Foo)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def test_resolve_reuse_cache():
|
|
29
|
+
async with push_inject_scope():
|
|
30
|
+
b1 = await resolve(Bar)
|
|
31
|
+
b2 = await resolve(Bar)
|
|
32
|
+
|
|
33
|
+
assert b1 is b2
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class DependsHook:
|
|
38
|
+
use_cache: bool = True
|
|
39
|
+
|
|
40
|
+
def __call__(self) -> Bar: # pragma: no cover
|
|
41
|
+
raise AssertionError("should be resolved via __get_depends__")
|
|
42
|
+
|
|
43
|
+
def __get_depends__(self) -> params.Depends:
|
|
44
|
+
return Depends(Bar, use_cache=self.use_cache)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def test_resolve_depends_hook():
|
|
48
|
+
bar = await resolve(DependsHook())
|
|
49
|
+
|
|
50
|
+
assert isinstance(bar, Bar)
|
|
51
|
+
assert isinstance(bar.foo, Foo)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def test_resolve_depends_hook_reuse_cache():
|
|
55
|
+
async with push_inject_scope():
|
|
56
|
+
b1 = await resolve(DependsHook())
|
|
57
|
+
b2 = await resolve(DependsHook())
|
|
58
|
+
|
|
59
|
+
assert b1 is b2
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def test_resolve_depends_hook_no_cache():
|
|
63
|
+
async with push_inject_scope():
|
|
64
|
+
b1 = await resolve(DependsHook(use_cache=False))
|
|
65
|
+
b2 = await resolve(DependsHook(use_cache=False))
|
|
66
|
+
|
|
67
|
+
assert b1 is not b2
|