anydi 0.26.8a0__tar.gz → 0.27.0__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 (30) hide show
  1. {anydi-0.26.8a0 → anydi-0.27.0}/PKG-INFO +3 -3
  2. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/_context.py +1 -1
  3. anydi-0.27.0/anydi/ext/_utils.py +89 -0
  4. anydi-0.27.0/anydi/ext/fastapi.py +92 -0
  5. anydi-0.27.0/anydi/ext/faststream.py +65 -0
  6. {anydi-0.26.8a0 → anydi-0.27.0}/pyproject.toml +8 -6
  7. anydi-0.26.8a0/anydi/ext/fastapi.py +0 -165
  8. {anydi-0.26.8a0 → anydi-0.27.0}/LICENSE +0 -0
  9. {anydi-0.26.8a0 → anydi-0.27.0}/README.md +0 -0
  10. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/__init__.py +0 -0
  11. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/_container.py +0 -0
  12. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/_logger.py +0 -0
  13. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/_module.py +0 -0
  14. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/_scanner.py +0 -0
  15. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/_types.py +0 -0
  16. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/_utils.py +0 -0
  17. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/ext/__init__.py +0 -0
  18. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/ext/django/__init__.py +0 -0
  19. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/ext/django/_container.py +0 -0
  20. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/ext/django/_settings.py +0 -0
  21. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/ext/django/_utils.py +0 -0
  22. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/ext/django/apps.py +0 -0
  23. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/ext/django/middleware.py +0 -0
  24. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/ext/django/ninja/__init__.py +0 -0
  25. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/ext/django/ninja/_operation.py +0 -0
  26. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/ext/django/ninja/_signature.py +0 -0
  27. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/ext/pytest_plugin.py +0 -0
  28. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/ext/starlette/__init__.py +0 -0
  29. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/ext/starlette/middleware.py +0 -0
  30. {anydi-0.26.8a0 → anydi-0.27.0}/anydi/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: anydi
3
- Version: 0.26.8a0
3
+ Version: 0.27.0
4
4
  Summary: Dependency Injection library
5
5
  Home-page: https://github.com/antonrh/anydi
6
6
  License: MIT
@@ -32,8 +32,8 @@ Provides-Extra: async
32
32
  Provides-Extra: docs
33
33
  Requires-Dist: anyio (>=3.6.2,<4.0.0) ; extra == "async"
34
34
  Requires-Dist: mkdocs (>=1.4.2,<2.0.0) ; extra == "docs"
35
- Requires-Dist: mkdocs-material (>=9.1.13,<10.0.0) ; extra == "docs"
36
- Requires-Dist: typing-extensions (>=4.8.0,<5.0.0)
35
+ Requires-Dist: mkdocs-material (>=9.5.21,<10.0.0) ; extra == "docs"
36
+ Requires-Dist: typing-extensions (>=4.12.1,<5.0.0)
37
37
  Project-URL: Repository, https://github.com/antonrh/anydi
38
38
  Description-Content-Type: text/markdown
39
39
 
@@ -251,7 +251,7 @@ class ResourceScopedContext(ScopedContext):
251
251
  exc_val: The exception instance, if any.
252
252
  exc_tb: The traceback, if any.
253
253
  """
254
- return self._stack.__exit__(exc_type, exc_val, exc_tb)
254
+ return self._stack.__exit__(exc_type, exc_val, exc_tb) # type: ignore[return-value]
255
255
 
256
256
  @abc.abstractmethod
257
257
  def start(self) -> None:
@@ -0,0 +1,89 @@
1
+ """AnyDI FastAPI extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import logging
7
+ from typing import Any, Callable
8
+
9
+ from typing_extensions import Annotated, get_args, get_origin
10
+
11
+ from anydi import Container
12
+ from anydi._utils import get_full_qualname
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class HasInterface:
18
+ _interface: Any = None
19
+
20
+ @property
21
+ def interface(self) -> Any:
22
+ if self._interface is None:
23
+ raise TypeError("Interface is not set.")
24
+ return self._interface
25
+
26
+ @interface.setter
27
+ def interface(self, interface: Any) -> None:
28
+ self._interface = interface
29
+
30
+
31
+ def patch_annotated_parameter(parameter: inspect.Parameter) -> inspect.Parameter:
32
+ """Patch an annotated parameter to resolve the default value."""
33
+ if not (
34
+ get_origin(parameter.annotation) is Annotated
35
+ and parameter.default is parameter.empty
36
+ ):
37
+ return parameter
38
+
39
+ tp_origin, *tp_metadata = get_args(parameter.annotation)
40
+ default = tp_metadata[-1]
41
+
42
+ if not isinstance(default, HasInterface):
43
+ return parameter
44
+
45
+ if (num := len(tp_metadata[:-1])) == 0:
46
+ interface = tp_origin
47
+ elif num == 1:
48
+ interface = Annotated[tp_origin, tp_metadata[0]]
49
+ elif num == 2:
50
+ interface = Annotated[tp_origin, tp_metadata[0], tp_metadata[1]]
51
+ elif num == 3:
52
+ interface = Annotated[
53
+ tp_origin,
54
+ tp_metadata[0],
55
+ tp_metadata[1],
56
+ tp_metadata[2],
57
+ ]
58
+ else:
59
+ raise TypeError("Too many annotated arguments.") # pragma: no cover
60
+ return parameter.replace(annotation=interface, default=default)
61
+
62
+
63
+ def patch_call_parameter(
64
+ call: Callable[..., Any], parameter: inspect.Parameter, container: Container
65
+ ) -> None:
66
+ """Patch a parameter to inject dependencies using AnyDI.
67
+
68
+ Args:
69
+ call: The call function.
70
+ parameter: The parameter to patch.
71
+ container: The AnyDI container.
72
+ """
73
+ parameter = patch_annotated_parameter(parameter)
74
+
75
+ if not isinstance(parameter.default, HasInterface):
76
+ return None
77
+
78
+ if not container.strict and not container.is_registered(parameter.annotation):
79
+ logger.debug(
80
+ f"Callable `{get_full_qualname(call)}` injected parameter "
81
+ f"`{parameter.name}` with an annotation of "
82
+ f"`{get_full_qualname(parameter.annotation)}` "
83
+ "is not registered. It will be registered at runtime with the "
84
+ "first call because it is running in non-strict mode."
85
+ )
86
+ else:
87
+ container._validate_injected_parameter(call, parameter) # noqa
88
+
89
+ parameter.default.interface = parameter.annotation
@@ -0,0 +1,92 @@
1
+ """AnyDI FastAPI extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any, Iterator, cast
7
+
8
+ from fastapi import Depends, FastAPI, params
9
+ from fastapi.dependencies.models import Dependant
10
+ from fastapi.routing import APIRoute
11
+ from starlette.requests import Request
12
+
13
+ from anydi import Container
14
+ from anydi._utils import get_typed_parameters
15
+
16
+ from ._utils import HasInterface, patch_call_parameter
17
+ from .starlette.middleware import RequestScopedMiddleware
18
+
19
+ __all__ = ["RequestScopedMiddleware", "install", "get_container", "Inject"]
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def install(app: FastAPI, container: Container) -> None:
25
+ """Install AnyDI into a FastAPI application.
26
+
27
+ Args:
28
+ app: The FastAPI application instance.
29
+ container: The container.
30
+
31
+ This function installs the AnyDI container into a FastAPI application by attaching
32
+ it to the application state. It also patches the route dependencies to inject the
33
+ required dependencies using AnyDI.
34
+ """
35
+ app.state.container = container # noqa
36
+
37
+ patched = []
38
+
39
+ for route in app.routes:
40
+ if not isinstance(route, APIRoute):
41
+ continue
42
+ for dependant in _iter_dependencies(route.dependant):
43
+ if dependant.cache_key in patched:
44
+ continue
45
+ patched.append(dependant.cache_key)
46
+ call, *params = dependant.cache_key
47
+ if not call:
48
+ continue # pragma: no cover
49
+ for parameter in get_typed_parameters(call):
50
+ patch_call_parameter(call, parameter, container)
51
+
52
+
53
+ def get_container(request: Request) -> Container:
54
+ """Get the AnyDI container from a FastAPI request.
55
+
56
+ Args:
57
+ request: The FastAPI request.
58
+
59
+ Returns:
60
+ The AnyDI container associated with the request.
61
+ """
62
+ return cast(Container, request.app.state.container)
63
+
64
+
65
+ class Resolver(HasInterface, params.Depends):
66
+ """Parameter dependency class for injecting dependencies using AnyDI."""
67
+
68
+ def __init__(self) -> None:
69
+ super().__init__(dependency=self._dependency, use_cache=True)
70
+
71
+ async def _dependency(self, container: Container = Depends(get_container)) -> Any:
72
+ return await container.aresolve(self.interface)
73
+
74
+
75
+ def Inject() -> Any: # noqa
76
+ """Decorator for marking a function parameter as requiring injection.
77
+
78
+ The `Inject` decorator is used to mark a function parameter as requiring injection
79
+ of a dependency resolved by AnyDI.
80
+
81
+ Returns:
82
+ The `Resolver` instance representing the parameter dependency.
83
+ """
84
+ return Resolver()
85
+
86
+
87
+ def _iter_dependencies(dependant: Dependant) -> Iterator[Dependant]:
88
+ """Iterate over the dependencies of a dependant."""
89
+ yield dependant
90
+ if dependant.dependencies:
91
+ for sub_dependant in dependant.dependencies:
92
+ yield from _iter_dependencies(sub_dependant)
@@ -0,0 +1,65 @@
1
+ """AnyDI FastStream extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any, cast
7
+
8
+ from fast_depends.dependencies import Depends
9
+ from faststream import ContextRepo
10
+ from faststream.broker.core.usecase import BrokerUsecase
11
+
12
+ from anydi import Container
13
+ from anydi._utils import get_typed_parameters
14
+
15
+ from ._utils import HasInterface, patch_call_parameter
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def install(broker: BrokerUsecase[Any, Any], container: Container) -> None:
21
+ """Install AnyDI into a FastStream broker.
22
+
23
+ Args:
24
+ broker: The broker.
25
+ container: The container.
26
+
27
+ This function installs the AnyDI container into a FastStream broker by attaching
28
+ it to the broker. It also patches the broker handlers to inject the required
29
+ dependencies using AnyDI.
30
+ """
31
+ broker._container = container # type: ignore[attr-defined]
32
+
33
+ for handler in _get_broken_handlers(broker):
34
+ call = handler._original_call # noqa
35
+ for parameter in get_typed_parameters(call):
36
+ patch_call_parameter(call, parameter, container)
37
+
38
+
39
+ def _get_broken_handlers(broker: BrokerUsecase[Any, Any]) -> list[Any]:
40
+ if hasattr(broker, "handlers"):
41
+ return [handler.calls[0][0] for handler in broker.handlers.values()]
42
+ # faststream > 0.5.0
43
+ return [
44
+ subscriber.calls[0].handler
45
+ for subscriber in broker._subscribers.values() # noqa
46
+ ]
47
+
48
+
49
+ def get_container(broker: BrokerUsecase[Any, Any]) -> Container:
50
+ return cast(Container, getattr(broker, "_container")) # noqa
51
+
52
+
53
+ class Resolver(HasInterface, Depends):
54
+ """Parameter dependency class for injecting dependencies using AnyDI."""
55
+
56
+ def __init__(self) -> None:
57
+ super().__init__(dependency=self._dependency, use_cache=True, cast=True)
58
+
59
+ async def _dependency(self, context: ContextRepo) -> Any:
60
+ container = get_container(context.get("broker"))
61
+ return await container.aresolve(self.interface)
62
+
63
+
64
+ def Inject() -> Any: # noqa
65
+ return Resolver()
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "anydi"
3
- version = "0.26.8a0"
3
+ version = "0.27.0"
4
4
  description = "Dependency Injection library"
5
5
  authors = ["Anton Ruhlov <antonruhlov@gmail.com>"]
6
6
  license = "MIT"
@@ -35,25 +35,27 @@ packages = [
35
35
 
36
36
  [tool.poetry.dependencies]
37
37
  python = "^3.8"
38
- typing-extensions = "^4.8.0"
38
+ typing-extensions = "^4.12.1"
39
39
  anyio = { version = "^3.6.2", optional = true }
40
40
  mkdocs = { version = "^1.4.2", optional = true }
41
- mkdocs-material = { version = "^9.1.13", optional = true }
41
+ mkdocs-material = { version = "^9.5.21", optional = true }
42
42
 
43
43
  [tool.poetry.extras]
44
44
  docs = ["mkdocs", "mkdocs-material"]
45
45
  async = ["anyio"]
46
46
 
47
47
  [tool.poetry.group.dev.dependencies]
48
- mypy = "^1.10.0"
49
- ruff = "^0.4.3"
50
- pytest = "^8.1.0"
48
+ mypy = "^1.11.0"
49
+ ruff = "^0.5.4"
50
+ pytest = "^8.3.1"
51
51
  pytest-cov = "^5.0.0"
52
52
  fastapi = "^0.100.0"
53
53
  httpx = "^0.26.0"
54
54
  django = "^4.2"
55
55
  django-ninja = "^1.1.0"
56
56
  pytest-django = "^4.8.0"
57
+ faststream = "^0.5.10"
58
+ redis = "^5.0.4"
57
59
 
58
60
  [tool.poetry.plugins.pytest11]
59
61
  anydi = "anydi.ext.pytest_plugin"
@@ -1,165 +0,0 @@
1
- """AnyDI FastAPI extension."""
2
-
3
- from __future__ import annotations
4
-
5
- import inspect
6
- import logging
7
- from typing import Any, Callable, Iterator, cast
8
-
9
- from fastapi import Depends, FastAPI, params
10
- from fastapi.dependencies.models import Dependant
11
- from fastapi.routing import APIRoute
12
- from starlette.requests import Request
13
- from typing_extensions import Annotated, get_args, get_origin
14
-
15
- from anydi import Container
16
- from anydi._utils import get_full_qualname, get_typed_parameters
17
-
18
- from .starlette.middleware import RequestScopedMiddleware
19
-
20
- __all__ = ["RequestScopedMiddleware", "install", "get_container", "Inject"]
21
-
22
- logger = logging.getLogger(__name__)
23
-
24
-
25
- def install(app: FastAPI, container: Container) -> None:
26
- """Install AnyDI into a FastAPI application.
27
-
28
- Args:
29
- app: The FastAPI application instance.
30
- container: The container.
31
-
32
- This function installs the AnyDI container into a FastAPI application by attaching
33
- it to the application state. It also patches the route dependencies to inject the
34
- required dependencies using AnyDI.
35
- """
36
- app.state.container = container # noqa
37
-
38
- patched = []
39
-
40
- for route in app.routes:
41
- if not isinstance(route, APIRoute):
42
- continue
43
- for dependant in _iter_dependencies(route.dependant):
44
- if dependant.cache_key in patched:
45
- continue
46
- patched.append(dependant.cache_key)
47
- call, *params = dependant.cache_key
48
- if not call:
49
- continue # pragma: no cover
50
- for parameter in get_typed_parameters(call):
51
- _patch_route_parameter(call, parameter, container)
52
-
53
-
54
- def get_container(request: Request) -> Container:
55
- """Get the AnyDI container from a FastAPI request.
56
-
57
- Args:
58
- request: The FastAPI request.
59
-
60
- Returns:
61
- The AnyDI container associated with the request.
62
- """
63
- return cast(Container, request.app.state.container)
64
-
65
-
66
- class Resolver(params.Depends):
67
- """Parameter dependency class for injecting dependencies using AnyDI."""
68
-
69
- def __init__(self) -> None:
70
- super().__init__(dependency=self._dependency, use_cache=True)
71
- self._interface: Any = None
72
-
73
- @property
74
- def interface(self) -> Any:
75
- if self._interface is None:
76
- raise TypeError("Interface is not set.")
77
- return self._interface
78
-
79
- @interface.setter
80
- def interface(self, interface: Any) -> None:
81
- self._interface = interface
82
-
83
- async def _dependency(self, container: Container = Depends(get_container)) -> Any:
84
- return await container.aresolve(self.interface)
85
-
86
-
87
- def Inject() -> Any: # noqa
88
- """Decorator for marking a function parameter as requiring injection.
89
-
90
- The `Inject` decorator is used to mark a function parameter as requiring injection
91
- of a dependency resolved by AnyDI.
92
-
93
- Returns:
94
- The `Resolver` instance representing the parameter dependency.
95
- """
96
- return Resolver()
97
-
98
-
99
- def _iter_dependencies(dependant: Dependant) -> Iterator[Dependant]:
100
- """Iterate over the dependencies of a dependant."""
101
- yield dependant
102
- if dependant.dependencies:
103
- for sub_dependant in dependant.dependencies:
104
- yield from _iter_dependencies(sub_dependant)
105
-
106
-
107
- def _patch_route_parameter(
108
- call: Callable[..., Any], parameter: inspect.Parameter, container: Container
109
- ) -> None:
110
- """Patch a parameter to inject dependencies using AnyDI.
111
-
112
- Args:
113
- call: The call function.
114
- parameter: The parameter to patch.
115
- container: The AnyDI container.
116
- """
117
- parameter = _patch_annotated_parameter(parameter)
118
-
119
- if not isinstance(parameter.default, Resolver):
120
- return None
121
-
122
- if not container.strict and not container.is_registered(parameter.annotation):
123
- logger.debug(
124
- f"Callable `{get_full_qualname(call)}` injected parameter "
125
- f"`{parameter.name}` with an annotation of "
126
- f"`{get_full_qualname(parameter.annotation)}` "
127
- "is not registered. It will be registered at runtime with the "
128
- "first call because it is running in non-strict mode."
129
- )
130
- else:
131
- container._validate_injected_parameter(call, parameter) # noqa
132
-
133
- parameter.default.interface = parameter.annotation
134
-
135
-
136
- def _patch_annotated_parameter(parameter: inspect.Parameter) -> inspect.Parameter:
137
- """Patch an annotated parameter to resolve the default value."""
138
- if not (
139
- get_origin(parameter.annotation) is Annotated
140
- and parameter.default is parameter.empty
141
- ):
142
- return parameter
143
-
144
- tp_origin, *tp_metadata = get_args(parameter.annotation)
145
- default = tp_metadata[-1]
146
-
147
- if not isinstance(default, Resolver):
148
- return parameter
149
-
150
- if (num := len(tp_metadata[:-1])) == 0:
151
- interface = tp_origin
152
- elif num == 1:
153
- interface = Annotated[tp_origin, tp_metadata[0]]
154
- elif num == 2:
155
- interface = Annotated[tp_origin, tp_metadata[0], tp_metadata[1]]
156
- elif num == 3:
157
- interface = Annotated[
158
- tp_origin,
159
- tp_metadata[0],
160
- tp_metadata[1],
161
- tp_metadata[2],
162
- ]
163
- else:
164
- raise TypeError("Too many annotated arguments.") # pragma: no cover
165
- return parameter.replace(annotation=interface, default=default)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes