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.
Files changed (34) hide show
  1. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/PKG-INFO +3 -2
  2. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/_deps_tp.py +9 -6
  3. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/_fastapi_lifecycle.py +22 -8
  4. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/deps.py +29 -6
  5. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/overrides.py +26 -10
  6. fastapi_injected-0.2.1/fastapi_injected/resolve.py +56 -0
  7. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/scope.py +9 -0
  8. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/types.py +4 -1
  9. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/pyproject.toml +2 -1
  10. fastapi_injected-0.2.1/tests/test_fastapi.py +121 -0
  11. fastapi_injected-0.2.1/tests/test_overrides.py +107 -0
  12. fastapi_injected-0.2.1/tests/test_resolve.py +67 -0
  13. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/uv.lock +201 -160
  14. fastapi_injected-0.2.0/fastapi_injected/resolve.py +0 -26
  15. fastapi_injected-0.2.0/tests/test_fastapi.py +0 -35
  16. fastapi_injected-0.2.0/tests/test_resolve.py +0 -32
  17. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/.github/dependabot.yml +0 -0
  18. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/.github/workflows/automerge.yml +0 -0
  19. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/.github/workflows/lint.yml +0 -0
  20. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/.github/workflows/publish.yml +0 -0
  21. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/.github/workflows/test.yml +0 -0
  22. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/.gitignore +0 -0
  23. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/.pre-commit-config.yaml +0 -0
  24. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/LICENSE +0 -0
  25. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/README.md +0 -0
  26. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/__init__.py +0 -0
  27. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/inject.py +0 -0
  28. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/fastapi_injected/sign.py +0 -0
  29. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/tests/__init__.py +0 -0
  30. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/tests/deps.py +0 -0
  31. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/tests/ext/__init__.py +0 -0
  32. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/tests/ext/test_pydantic_ai.py +0 -0
  33. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/tests/test_inject.py +0 -0
  34. {fastapi_injected-0.2.0 → fastapi_injected-0.2.1}/tests/test_typing.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: fastapi-injected
3
- Version: 0.2.0
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, TypeAliasType, TypeVar, get_args, get_origin
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 _unwrap_tp(tp: Any) -> Any:
7
- if isinstance(tp, TypeAliasType):
8
- return tp.__value__
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 = _unwrap_tp(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 = _unwrap_tp(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 AsyncIterable
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) -> AsyncIterable[None]:
53
- async with push_inject_scope(
54
- dependency_cache=_get_dependency_cache(request),
55
- request=request,
56
- ):
57
- yield
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](func: Callable[P, R], /) -> Dependant:
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=Annotated[Any, Depends(func)],
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=_dependency_override_provider.get(),
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 _get_current_overrides() -> Iterator[Mapping[Any, Any]]:
19
- obj = get_inject_dependency_override_provider()
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 _OverridesProvider:
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
- _OverridesProvider(deps),
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.0"
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