anydi 0.24.2__tar.gz → 0.25.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 (28) hide show
  1. {anydi-0.24.2 → anydi-0.25.0}/PKG-INFO +57 -1
  2. {anydi-0.24.2 → anydi-0.25.0}/README.md +56 -0
  3. {anydi-0.24.2 → anydi-0.25.0}/anydi/_container.py +15 -7
  4. {anydi-0.24.2 → anydi-0.25.0}/anydi/_context.py +2 -2
  5. {anydi-0.24.2 → anydi-0.25.0}/anydi/_scanner.py +4 -4
  6. {anydi-0.24.2 → anydi-0.25.0}/anydi/_types.py +6 -4
  7. anydi-0.25.0/anydi/_utils.py +122 -0
  8. anydi-0.25.0/anydi/ext/django/__init__.py +9 -0
  9. anydi-0.25.0/anydi/ext/django/_container.py +18 -0
  10. anydi-0.25.0/anydi/ext/django/_settings.py +39 -0
  11. anydi-0.25.0/anydi/ext/django/_utils.py +111 -0
  12. anydi-0.25.0/anydi/ext/django/apps.py +82 -0
  13. anydi-0.25.0/anydi/ext/django/middleware.py +26 -0
  14. anydi-0.25.0/anydi/ext/django/ninja/__init__.py +16 -0
  15. anydi-0.25.0/anydi/ext/django/ninja/_operation.py +75 -0
  16. anydi-0.25.0/anydi/ext/django/ninja/_signature.py +64 -0
  17. {anydi-0.24.2 → anydi-0.25.0}/anydi/ext/fastapi.py +2 -2
  18. {anydi-0.24.2 → anydi-0.25.0}/pyproject.toml +18 -2
  19. anydi-0.24.2/anydi/_utils.py +0 -113
  20. {anydi-0.24.2 → anydi-0.25.0}/LICENSE +0 -0
  21. {anydi-0.24.2 → anydi-0.25.0}/anydi/__init__.py +0 -0
  22. {anydi-0.24.2 → anydi-0.25.0}/anydi/_logger.py +0 -0
  23. {anydi-0.24.2 → anydi-0.25.0}/anydi/_module.py +0 -0
  24. {anydi-0.24.2 → anydi-0.25.0}/anydi/ext/__init__.py +0 -0
  25. {anydi-0.24.2 → anydi-0.25.0}/anydi/ext/pytest_plugin.py +0 -0
  26. {anydi-0.24.2 → anydi-0.25.0}/anydi/ext/starlette/__init__.py +0 -0
  27. {anydi-0.24.2 → anydi-0.25.0}/anydi/ext/starlette/middleware.py +0 -0
  28. {anydi-0.24.2 → anydi-0.25.0}/anydi/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: anydi
3
- Version: 0.24.2
3
+ Version: 0.25.0
4
4
  Summary: Dependency Injection library
5
5
  Home-page: https://github.com/antonrh/anydi
6
6
  License: MIT
@@ -139,3 +139,59 @@ def say_hello(message: str = Inject()) -> dict[str, str]:
139
139
  anydi.ext.fastapi.install(app, container)
140
140
  ```
141
141
 
142
+
143
+
144
+ ## Django Ninja Example
145
+
146
+ *container.py*
147
+
148
+ ```python
149
+ from anydi import Container
150
+
151
+
152
+ def get_container() -> Container:
153
+ container = Container()
154
+
155
+ @container.provider(scope="singleton")
156
+ def message() -> str:
157
+ return "Hello, World!"
158
+
159
+ return container
160
+ ```
161
+
162
+ *settings.py*
163
+
164
+ ```python
165
+ INSTALLED_APPS = [
166
+ ...
167
+ "anydi.ext.django",
168
+ ]
169
+
170
+ ANYDI = {
171
+ "CONTAINER_FACTORY": "myapp.container.get_container",
172
+ "PATCH_NINJA": True,
173
+ }
174
+ ```
175
+
176
+ *urls.py*
177
+
178
+ ```python
179
+ from django.http import HttpRequest
180
+ from django.urls import path
181
+ from ninja import NinjaAPI
182
+
183
+ from anydi import auto
184
+
185
+ api = NinjaAPI()
186
+
187
+
188
+ @api.get("/hello")
189
+ def say_hello(request: HttpRequest, message: str = auto) -> dict[str, str]:
190
+ return {"message": message}
191
+
192
+
193
+ urlpatterns = [
194
+ path("api/", api.urls),
195
+ ]
196
+ ```
197
+
@@ -98,3 +98,59 @@ def say_hello(message: str = Inject()) -> dict[str, str]:
98
98
  # Install the container into the FastAPI app
99
99
  anydi.ext.fastapi.install(app, container)
100
100
  ```
101
+
102
+
103
+
104
+ ## Django Ninja Example
105
+
106
+ *container.py*
107
+
108
+ ```python
109
+ from anydi import Container
110
+
111
+
112
+ def get_container() -> Container:
113
+ container = Container()
114
+
115
+ @container.provider(scope="singleton")
116
+ def message() -> str:
117
+ return "Hello, World!"
118
+
119
+ return container
120
+ ```
121
+
122
+ *settings.py*
123
+
124
+ ```python
125
+ INSTALLED_APPS = [
126
+ ...
127
+ "anydi.ext.django",
128
+ ]
129
+
130
+ ANYDI = {
131
+ "CONTAINER_FACTORY": "myapp.container.get_container",
132
+ "PATCH_NINJA": True,
133
+ }
134
+ ```
135
+
136
+ *urls.py*
137
+
138
+ ```python
139
+ from django.http import HttpRequest
140
+ from django.urls import path
141
+ from ninja import NinjaAPI
142
+
143
+ from anydi import auto
144
+
145
+ api = NinjaAPI()
146
+
147
+
148
+ @api.get("/hello")
149
+ def say_hello(request: HttpRequest, message: str = auto) -> dict[str, str]:
150
+ return {"message": message}
151
+
152
+
153
+ urlpatterns = [
154
+ path("api/", api.urls),
155
+ ]
156
+ ```
@@ -43,7 +43,13 @@ from ._logger import logger
43
43
  from ._module import Module, ModuleRegistry
44
44
  from ._scanner import Scanner
45
45
  from ._types import AnyInterface, Interface, Provider, Scope, is_marker
46
- from ._utils import get_full_qualname, get_signature, is_builtin_type
46
+ from ._utils import (
47
+ get_full_qualname,
48
+ get_typed_parameters,
49
+ get_typed_return_annotation,
50
+ has_resource_origin,
51
+ is_builtin_type,
52
+ )
47
53
 
48
54
  T = TypeVar("T", bound=Any)
49
55
  P = ParamSpec("P")
@@ -327,7 +333,7 @@ class Container:
327
333
  """
328
334
  related_providers = []
329
335
 
330
- for parameter in provider.parameters.values():
336
+ for parameter in provider.parameters:
331
337
  if parameter.annotation is inspect._empty: # noqa
332
338
  raise TypeError(
333
339
  f"Missing provider `{provider}` "
@@ -366,7 +372,7 @@ class Container:
366
372
  The auto scope, or None if the auto scope cannot be detected.
367
373
  """
368
374
  has_transient, has_request, has_singleton = False, False, False
369
- for parameter in get_signature(obj).parameters.values():
375
+ for parameter in get_typed_parameters(obj):
370
376
  sub_provider = self._get_or_register_provider(parameter.annotation)
371
377
  if not has_transient and sub_provider.scope == "transient":
372
378
  has_transient = True
@@ -711,14 +717,16 @@ class Container:
711
717
  Raises:
712
718
  TypeError: If the provider return annotation is missing or invalid.
713
719
  """
714
- annotation = get_signature(obj).return_annotation
720
+ annotation = get_typed_return_annotation(obj)
715
721
 
716
- if annotation is inspect._empty: # noqa
722
+ if annotation is None:
717
723
  raise TypeError(
718
724
  f"Missing `{get_full_qualname(obj)}` provider return annotation."
719
725
  )
720
726
 
721
- if get_origin(annotation) in (get_origin(Iterator), get_origin(AsyncIterator)):
727
+ origin = get_origin(annotation)
728
+
729
+ if has_resource_origin(origin):
722
730
  args = get_args(annotation)
723
731
  if args:
724
732
  return args[0]
@@ -741,7 +749,7 @@ class Container:
741
749
  of the injected parameters.
742
750
  """
743
751
  injected_params = {}
744
- for parameter in get_signature(obj).parameters.values():
752
+ for parameter in get_typed_parameters(obj):
745
753
  if not is_marker(parameter.default):
746
754
  continue
747
755
  try:
@@ -97,7 +97,7 @@ class ScopedContext(abc.ABC):
97
97
  The arguments for the provider.
98
98
  """
99
99
  args, kwargs = [], {}
100
- for parameter in provider.parameters.values():
100
+ for parameter in provider.parameters:
101
101
  instance = self.container.resolve(parameter.annotation)
102
102
  if parameter.kind == parameter.POSITIONAL_ONLY:
103
103
  args.append(instance)
@@ -117,7 +117,7 @@ class ScopedContext(abc.ABC):
117
117
  The arguments for the provider.
118
118
  """
119
119
  args, kwargs = [], {}
120
- for parameter in provider.parameters.values():
120
+ for parameter in provider.parameters:
121
121
  instance = await self.container.aresolve(parameter.annotation)
122
122
  if parameter.kind == parameter.POSITIONAL_ONLY:
123
123
  args.append(instance)
@@ -20,7 +20,7 @@ from typing import (
20
20
  from typing_extensions import NamedTuple, ParamSpec
21
21
 
22
22
  from ._types import is_marker
23
- from ._utils import get_signature
23
+ from ._utils import get_typed_parameters
24
24
 
25
25
  if TYPE_CHECKING:
26
26
  from ._container import Container
@@ -157,10 +157,10 @@ class Scanner:
157
157
 
158
158
  # Get by Marker
159
159
  if inspect.isclass(member):
160
- signature = get_signature(member.__init__)
160
+ parameters = get_typed_parameters(member.__init__)
161
161
  else:
162
- signature = get_signature(member)
163
- for parameter in signature.parameters.values():
162
+ parameters = get_typed_parameters(member)
163
+ for parameter in parameters:
164
164
  if is_marker(parameter.default):
165
165
  dependencies.append(
166
166
  self._create_dependency(member=member, module=module)
@@ -1,11 +1,13 @@
1
+ from __future__ import annotations
2
+
1
3
  import inspect
2
4
  from dataclasses import dataclass
3
5
  from functools import cached_property
4
- from typing import Any, Callable, Mapping, Type, TypeVar, Union
6
+ from typing import Any, Callable, Type, TypeVar, Union
5
7
 
6
8
  from typing_extensions import Annotated, Literal, Self, TypeAlias
7
9
 
8
- from ._utils import get_full_qualname, get_signature
10
+ from ._utils import get_full_qualname, get_typed_parameters
9
11
 
10
12
  Scope = Literal["transient", "singleton", "request"]
11
13
 
@@ -58,13 +60,13 @@ class Provider:
58
60
  return get_full_qualname(self.obj)
59
61
 
60
62
  @cached_property
61
- def parameters(self) -> Mapping[str, inspect.Parameter]:
63
+ def parameters(self) -> list[inspect.Parameter]:
62
64
  """Returns the parameters of the provider as a mapping.
63
65
 
64
66
  Returns:
65
67
  The parameters of the provider.
66
68
  """
67
- return get_signature(self.obj).parameters
69
+ return get_typed_parameters(self.obj)
68
70
 
69
71
  @cached_property
70
72
  def is_class(self) -> bool:
@@ -0,0 +1,122 @@
1
+ """Shared AnyDI utils module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ import functools
7
+ import inspect
8
+ import sys
9
+ from typing import Any, AsyncIterator, Callable, ForwardRef, Iterator, TypeVar, cast
10
+
11
+ from typing_extensions import Annotated, ParamSpec, get_origin
12
+
13
+ try:
14
+ import anyio # noqa
15
+ except ImportError:
16
+ anyio = None # type: ignore[assignment]
17
+
18
+
19
+ if sys.version_info < (3, 9): # pragma: nocover
20
+
21
+ def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
22
+ return type_._evaluate(globalns, localns) # noqa
23
+
24
+ else:
25
+
26
+ def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
27
+ return cast(Any, type_)._evaluate(globalns, localns, set()) # noqa
28
+
29
+
30
+ T = TypeVar("T")
31
+ P = ParamSpec("P")
32
+
33
+
34
+ def get_full_qualname(obj: Any) -> str:
35
+ """Get the fully qualified name of an object."""
36
+ origin = get_origin(obj)
37
+ if origin is Annotated:
38
+ metadata = ", ".join(
39
+ [
40
+ f'"{arg}"' if isinstance(arg, str) else str(arg)
41
+ for arg in obj.__metadata__
42
+ ]
43
+ )
44
+ return f"Annotated[{get_full_qualname(obj.__args__[0])}, {metadata}]]"
45
+
46
+ qualname = getattr(obj, "__qualname__", None)
47
+ module_name = getattr(obj, "__module__", None)
48
+ if qualname is None:
49
+ qualname = type(obj).__qualname__
50
+
51
+ if module_name is None:
52
+ module_name = type(obj).__module__
53
+
54
+ if module_name == builtins.__name__:
55
+ return qualname
56
+ return f"{module_name}.{qualname}"
57
+
58
+
59
+ def is_builtin_type(tp: type[Any]) -> bool:
60
+ """Check if the given type is a built-in type."""
61
+ return tp.__module__ == builtins.__name__
62
+
63
+
64
+ def make_forwardref(annotation: str, globalns: dict[str, Any]) -> Any:
65
+ """Create a forward reference from a string annotation."""
66
+ forward_ref = ForwardRef(annotation)
67
+ return evaluate_forwardref(forward_ref, globalns, globalns)
68
+
69
+
70
+ def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
71
+ """Get the typed annotation of a parameter."""
72
+ if isinstance(annotation, str):
73
+ annotation = ForwardRef(annotation)
74
+ annotation = evaluate_forwardref(annotation, globalns, globalns)
75
+ return annotation
76
+
77
+
78
+ def get_typed_return_annotation(obj: Callable[..., Any]) -> Any:
79
+ """Get the typed return annotation of a callable object."""
80
+ signature = inspect.signature(obj)
81
+ annotation = signature.return_annotation
82
+ if annotation is inspect.Signature.empty:
83
+ return None
84
+ globalns = getattr(obj, "__globals__", {})
85
+ return get_typed_annotation(annotation, globalns)
86
+
87
+
88
+ def get_typed_parameters(obj: Callable[..., Any]) -> list[inspect.Parameter]:
89
+ """Get the typed parameters of a callable object."""
90
+ globalns = getattr(obj, "__globals__", {})
91
+ return [
92
+ parameter.replace(
93
+ annotation=get_typed_annotation(parameter.annotation, globalns)
94
+ )
95
+ for name, parameter in inspect.signature(obj).parameters.items()
96
+ ]
97
+
98
+
99
+ _resource_origins = (
100
+ get_origin(Iterator),
101
+ get_origin(AsyncIterator),
102
+ )
103
+
104
+
105
+ def has_resource_origin(origin: Any) -> bool:
106
+ """Check if the given origin is a resource origin."""
107
+ return origin in _resource_origins
108
+
109
+
110
+ async def run_async(
111
+ func: Callable[P, T],
112
+ /,
113
+ *args: P.args,
114
+ **kwargs: P.kwargs,
115
+ ) -> T:
116
+ """Runs the given function asynchronously using the `anyio` library."""
117
+ if not anyio:
118
+ raise ImportError(
119
+ "`anyio` library is not currently installed. Please make sure to install "
120
+ "it first, or consider using `anydi[full]` instead."
121
+ )
122
+ return await anyio.to_thread.run_sync(functools.partial(func, *args, **kwargs))
@@ -0,0 +1,9 @@
1
+ from ._container import container
2
+ from ._utils import inject_urlpatterns, register_components, register_settings
3
+
4
+ __all__ = [
5
+ "container",
6
+ "register_components",
7
+ "register_settings",
8
+ "inject_urlpatterns",
9
+ ]
@@ -0,0 +1,18 @@
1
+ from typing import cast
2
+
3
+ from django.apps.registry import apps
4
+ from django.utils.functional import SimpleLazyObject
5
+
6
+ import anydi
7
+
8
+ from .apps import ContainerConfig
9
+
10
+ __all__ = ["container"]
11
+
12
+
13
+ def _get_container() -> anydi.Container:
14
+ app_config = cast(ContainerConfig, apps.get_app_config(ContainerConfig.label))
15
+ return app_config.container
16
+
17
+
18
+ container = cast(anydi.Container, SimpleLazyObject(_get_container))
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Sequence
4
+
5
+ from django.conf import settings
6
+ from typing_extensions import TypedDict
7
+
8
+
9
+ class Settings(TypedDict):
10
+ CONTAINER_FACTORY: str | None
11
+ STRICT_MODE: bool
12
+ REGISTER_SETTINGS: bool
13
+ REGISTER_COMPONENTS: bool
14
+ INJECT_URLCONF: str | None
15
+ MODULES: Sequence[str]
16
+ SCAN_PACKAGES: Sequence[str]
17
+ PATCH_NINJA: bool
18
+
19
+
20
+ DEFAULTS = Settings(
21
+ CONTAINER_FACTORY=None,
22
+ STRICT_MODE=False,
23
+ REGISTER_SETTINGS=False,
24
+ REGISTER_COMPONENTS=False,
25
+ MODULES=[],
26
+ PATCH_NINJA=False,
27
+ INJECT_URLCONF=None,
28
+ SCAN_PACKAGES=[],
29
+ )
30
+
31
+
32
+ def get_settings() -> Settings:
33
+ """Get the AnyDI settings from the Django settings."""
34
+ return Settings(
35
+ **{
36
+ **DEFAULTS,
37
+ **getattr(settings, "ANYDI", {}),
38
+ }
39
+ )
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from functools import wraps
5
+ from typing import Any
6
+
7
+ from django.conf import settings
8
+ from django.core.cache import BaseCache, caches
9
+ from django.db import connections
10
+ from django.db.backends.base.base import BaseDatabaseWrapper
11
+ from django.urls import URLPattern, URLResolver, get_resolver
12
+ from typing_extensions import Annotated, get_origin
13
+
14
+ import anydi
15
+
16
+
17
+ def register_settings(
18
+ container: anydi.Container, prefix: str = "django.conf.setting."
19
+ ) -> None:
20
+ """Register Django settings into the container."""
21
+
22
+ def _get_setting_value(value: Any) -> Any:
23
+ return lambda: value
24
+
25
+ for setting_name in dir(settings):
26
+ setting_value = getattr(settings, setting_name)
27
+ if not setting_name.isupper():
28
+ continue
29
+
30
+ container.register(
31
+ Annotated[Any, f"{prefix}{setting_name}"],
32
+ _get_setting_value(setting_value),
33
+ scope="singleton",
34
+ )
35
+
36
+ def _resolve(resolve: Any) -> Any:
37
+ @wraps(resolve)
38
+ def wrapper(interface: Any) -> Any:
39
+ return resolve(_aware_settings(interface, prefix))
40
+
41
+ return wrapper
42
+
43
+ def _aresolve(resolve: Any) -> Any:
44
+ @wraps(resolve)
45
+ async def wrapper(interface: Any) -> Any:
46
+ return await resolve(_aware_settings(interface, prefix))
47
+
48
+ return wrapper
49
+
50
+ # Patch resolvers
51
+ container.resolve = _resolve(container.resolve) # type: ignore[method-assign] # noqa
52
+ container.aresolve = _aresolve(container.aresolve) # type: ignore[method-assign] # noqa
53
+
54
+
55
+ def _aware_settings(interface: Any, prefix: str) -> Any:
56
+ origin = get_origin(interface)
57
+ if origin is not Annotated:
58
+ return interface # pragma: no cover
59
+ named = interface.__metadata__[-1]
60
+
61
+ if isinstance(named, str) and named.startswith(prefix):
62
+ _, setting_name = named.rsplit(prefix, maxsplit=1)
63
+ return Annotated[Any, f"{prefix}{setting_name}"]
64
+ return interface
65
+
66
+
67
+ def register_components(container: anydi.Container) -> None:
68
+ """Register Django components into the container."""
69
+
70
+ # Register caches
71
+ def _get_cache(cache_name: str) -> Any:
72
+ return lambda: caches[cache_name]
73
+
74
+ for cache_name in caches:
75
+ container.register(
76
+ Annotated[BaseCache, cache_name],
77
+ _get_cache(cache_name),
78
+ scope="singleton",
79
+ )
80
+
81
+ # Register database connections
82
+ def _get_connection(alias: str) -> Any:
83
+ return lambda: connections[alias]
84
+
85
+ for alias in connections:
86
+ container.register(
87
+ Annotated[BaseDatabaseWrapper, alias],
88
+ _get_connection(alias),
89
+ scope="singleton",
90
+ )
91
+
92
+
93
+ def inject_urlpatterns(container: anydi.Container, *, urlconf: str) -> None:
94
+ """Auto-inject the container into views."""
95
+ resolver = get_resolver(urlconf)
96
+ for pattern in iter_urlpatterns(resolver.url_patterns):
97
+ # Skip django-ninja views
98
+ if pattern.lookup_str.startswith("ninja."):
99
+ continue # pragma: no cover
100
+ pattern.callback = container.inject(pattern.callback)
101
+
102
+
103
+ def iter_urlpatterns(
104
+ urlpatterns: list[URLPattern | URLResolver],
105
+ ) -> Iterator[URLPattern]:
106
+ """Iterate over all views in urlpatterns."""
107
+ for url_pattern in urlpatterns:
108
+ if isinstance(url_pattern, URLResolver):
109
+ yield from iter_urlpatterns(url_pattern.url_patterns)
110
+ else:
111
+ yield url_pattern
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import types
5
+ from typing import Callable, cast
6
+
7
+ from django.apps import AppConfig
8
+ from django.conf import settings
9
+ from django.core.exceptions import ImproperlyConfigured
10
+ from django.utils.module_loading import import_string
11
+
12
+ import anydi
13
+
14
+ from ._settings import get_settings
15
+ from ._utils import inject_urlpatterns, register_components, register_settings
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ContainerConfig(AppConfig): # type: ignore[misc]
21
+ name = "anydi.ext.django"
22
+ label = "anydi_django"
23
+
24
+ def __init__(self, app_name: str, app_module: types.ModuleType | None) -> None:
25
+ super().__init__(app_name, app_module)
26
+ self.settings = get_settings()
27
+ # Create a container
28
+ container_factory_path = self.settings["CONTAINER_FACTORY"]
29
+ if container_factory_path:
30
+ try:
31
+ container_factory = cast(
32
+ Callable[[], anydi.Container], import_string(container_factory_path)
33
+ )
34
+ except ImportError as exc:
35
+ raise ImproperlyConfigured(
36
+ f"Cannot import container factory '{container_factory_path}'."
37
+ ) from exc
38
+ self.container = container_factory()
39
+ else:
40
+ self.container = anydi.Container(
41
+ strict=self.settings["STRICT_MODE"],
42
+ )
43
+
44
+ def ready(self) -> None: # noqa: C901
45
+ # Register Django settings
46
+ if self.settings["REGISTER_SETTINGS"]:
47
+ register_settings(
48
+ self.container,
49
+ prefix=getattr(
50
+ settings,
51
+ "ANYDI_SETTINGS_PREFIX",
52
+ "django.conf.settings.",
53
+ ),
54
+ )
55
+
56
+ # Register Django components
57
+ if self.settings["REGISTER_COMPONENTS"]:
58
+ register_components(self.container)
59
+
60
+ # Register modules
61
+ for module_path in self.settings["MODULES"]:
62
+ try:
63
+ module_cls = import_string(module_path)
64
+ except ImportError as exc:
65
+ raise ImproperlyConfigured(
66
+ f"Cannot import module '{module_path}'."
67
+ ) from exc
68
+ self.container.register_module(module_cls)
69
+
70
+ # Patching the django-ninja framework if it installed
71
+ if self.settings["PATCH_NINJA"]:
72
+ from .ninja import patch_ninja
73
+
74
+ patch_ninja()
75
+
76
+ # Auto-injecting the container into views
77
+ if urlconf := self.settings["INJECT_URLCONF"]:
78
+ inject_urlpatterns(self.container, urlconf=urlconf)
79
+
80
+ # Scan packages
81
+ for scan_package in self.settings["SCAN_PACKAGES"]:
82
+ self.container.scan(scan_package)
@@ -0,0 +1,26 @@
1
+ from typing import Callable
2
+
3
+ from asgiref.sync import iscoroutinefunction
4
+ from django.http import HttpRequest, HttpResponse
5
+ from django.utils.decorators import sync_and_async_middleware
6
+
7
+ from ._container import container
8
+
9
+
10
+ @sync_and_async_middleware # type: ignore[misc]
11
+ def request_scoped_middleware(
12
+ get_response: Callable[[HttpRequest], HttpResponse],
13
+ ) -> Callable[[HttpRequest], HttpResponse]:
14
+ if iscoroutinefunction(get_response):
15
+
16
+ async def async_middleware(request: HttpRequest) -> HttpResponse:
17
+ async with container.arequest_context():
18
+ return await get_response(request)
19
+
20
+ return async_middleware
21
+
22
+ def middleware(request: HttpRequest) -> HttpResponse:
23
+ with container.request_context():
24
+ return get_response(request)
25
+
26
+ return middleware
@@ -0,0 +1,16 @@
1
+ try:
2
+ from ninja import operation
3
+ except ImportError as exc: # pragma: no cover
4
+ raise ImportError(
5
+ "'django-ninja' is not installed. "
6
+ "Please install it using 'pip install django-ninja'."
7
+ ) from exc
8
+
9
+ from ._operation import AsyncOperation, Operation
10
+ from ._signature import ViewSignature
11
+
12
+
13
+ def patch_ninja() -> None:
14
+ operation.ViewSignature = ViewSignature # type: ignore[attr-defined]
15
+ operation.Operation = Operation # type: ignore[misc]
16
+ operation.AsyncOperation = AsyncOperation # type: ignore[misc]
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from django.http import HttpRequest, HttpResponseBase
6
+ from ninja.operation import (
7
+ AsyncOperation as BaseAsyncOperation, # noqa
8
+ Operation as BaseOperation,
9
+ )
10
+
11
+ from anydi.ext.django import container
12
+
13
+ from ._signature import ViewSignature
14
+
15
+
16
+ def _update_exc_args(exc: Exception) -> None:
17
+ if isinstance(exc, TypeError) and "required positional argument" in str(exc):
18
+ msg = "Did you fail to use functools.wraps() in a decorator?"
19
+ msg = f"{exc.args[0]}: {msg}" if exc.args else msg
20
+ exc.args = (msg,) + exc.args[1:]
21
+
22
+
23
+ class Operation(BaseOperation):
24
+ signature: ViewSignature
25
+
26
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
27
+ super().__init__(*args, **kwargs)
28
+ self.dependencies = self.signature.dependencies
29
+
30
+ def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase:
31
+ error = self._run_checks(request)
32
+ if error:
33
+ return error
34
+ try:
35
+ temporal_response = self.api.create_temporal_response(request)
36
+ values = self._get_values(request, kw, temporal_response)
37
+ values.update(self._get_dependencies())
38
+ result = self.view_func(request, **values)
39
+ return self._result_to_response(request, result, temporal_response)
40
+ except Exception as e:
41
+ _update_exc_args(e)
42
+ return self.api.on_exception(request, e)
43
+
44
+ def _get_dependencies(self) -> dict[str, Any]:
45
+ return {
46
+ name: container.resolve(interface) for name, interface in self.dependencies
47
+ }
48
+
49
+
50
+ class AsyncOperation(BaseAsyncOperation):
51
+ signature: ViewSignature
52
+
53
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
54
+ super().__init__(*args, **kwargs)
55
+ self.dependencies = self.signature.dependencies
56
+
57
+ async def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase: # type: ignore
58
+ error = await self._run_checks(request)
59
+ if error:
60
+ return error
61
+ try:
62
+ temporal_response = self.api.create_temporal_response(request)
63
+ values = self._get_values(request, kw, temporal_response)
64
+ values.update(await self._get_dependencies())
65
+ result = await self.view_func(request, **values)
66
+ return self._result_to_response(request, result, temporal_response)
67
+ except Exception as e:
68
+ _update_exc_args(e)
69
+ return self.api.on_exception(request, e)
70
+
71
+ async def _get_dependencies(self) -> dict[str, Any]:
72
+ return {
73
+ name: await container.aresolve(interface)
74
+ for name, interface in self.dependencies
75
+ }
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from collections.abc import Callable
5
+ from typing import Any
6
+
7
+ from django.http import HttpResponse
8
+ from ninja.signature.details import (
9
+ FuncParam, # noqa
10
+ ViewSignature as BaseViewSignature,
11
+ )
12
+ from ninja.signature.utils import get_path_param_names, get_typed_signature
13
+
14
+ from anydi._types import Marker # noqa
15
+
16
+
17
+ class ViewSignature(BaseViewSignature):
18
+ def __init__(self, path: str, view_func: Callable[..., Any]) -> None:
19
+ self.view_func = view_func
20
+ self.signature = get_typed_signature(self.view_func)
21
+ self.path = path
22
+ self.path_params_names = get_path_param_names(path)
23
+ self.docstring = inspect.cleandoc(view_func.__doc__ or "")
24
+ self.has_kwargs = False
25
+ self.dependencies = []
26
+
27
+ self.params = []
28
+ for name, arg in self.signature.parameters.items():
29
+ if name == "request":
30
+ # TODO: maybe better assert that 1st param is request or check by type?
31
+ # maybe even have attribute like `has_request`
32
+ # so that users can ignore passing request if not needed
33
+ continue
34
+
35
+ if arg.kind == arg.VAR_KEYWORD:
36
+ # Skipping **kwargs
37
+ self.has_kwargs = True
38
+ continue
39
+
40
+ if arg.kind == arg.VAR_POSITIONAL:
41
+ # Skipping *args
42
+ continue
43
+
44
+ if arg.annotation is HttpResponse:
45
+ self.response_arg = name
46
+ continue
47
+
48
+ # Skip default values that are anydi dependency markers
49
+ if isinstance(arg.default, Marker):
50
+ self.dependencies.append((name, arg.annotation))
51
+ continue
52
+
53
+ func_param = self._get_param_type(name, arg)
54
+ self.params.append(func_param)
55
+
56
+ if hasattr(view_func, "_ninja_contribute_args"):
57
+ for p_name, p_type, p_source in view_func._ninja_contribute_args: # noqa
58
+ self.params.append(
59
+ FuncParam(p_name, p_source.alias or p_name, p_source, p_type, False)
60
+ )
61
+
62
+ self.models = self._create_models()
63
+
64
+ self._validate_view_path_params()
@@ -13,7 +13,7 @@ from starlette.requests import Request
13
13
  from typing_extensions import Annotated, get_args, get_origin
14
14
 
15
15
  from anydi import Container
16
- from anydi._utils import get_full_qualname, get_signature
16
+ from anydi._utils import get_full_qualname, get_typed_parameters
17
17
 
18
18
  from .starlette.middleware import RequestScopedMiddleware
19
19
 
@@ -47,7 +47,7 @@ def install(app: FastAPI, container: Container) -> None:
47
47
  call, *params = dependant.cache_key
48
48
  if not call:
49
49
  continue # pragma: no cover
50
- for parameter in get_signature(call).parameters.values():
50
+ for parameter in get_typed_parameters(call):
51
51
  _patch_route_parameter(call, parameter, container)
52
52
 
53
53
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "anydi"
3
- version = "0.24.2"
3
+ version = "0.25.0"
4
4
  description = "Dependency Injection library"
5
5
  authors = ["Anton Ruhlov <antonruhlov@gmail.com>"]
6
6
  license = "MIT"
@@ -50,8 +50,11 @@ mypy = "^1.10.0"
50
50
  ruff = "^0.4.3"
51
51
  pytest = "^8.1.0"
52
52
  pytest-cov = "^5.0.0"
53
- fastapi = "^0.95.1"
53
+ fastapi = "^0.100.0"
54
54
  httpx = "^0.26.0"
55
+ django = "^4.2"
56
+ django-ninja = "^1.1.0"
57
+ pytest-django = "^4.8.0"
55
58
 
56
59
  [tool.poetry.plugins.pytest11]
57
60
  anydi = "anydi.ext.pytest_plugin"
@@ -74,6 +77,12 @@ convention = "google"
74
77
  python_version = "3.10"
75
78
  strict = true
76
79
 
80
+ [[tool.mypy.overrides]]
81
+ module = [
82
+ "django.*",
83
+ ]
84
+ ignore_missing_imports = true
85
+
77
86
  [tool.pytest.ini_options]
78
87
  addopts = [
79
88
  "--strict-config",
@@ -81,6 +90,7 @@ addopts = [
81
90
  ]
82
91
  xfail_strict = true
83
92
  junit_family = "xunit2"
93
+ DJANGO_SETTINGS_MODULE = "tests.ext.django.settings"
84
94
 
85
95
  [tool.coverage.report]
86
96
  exclude_also = [
@@ -97,6 +107,12 @@ exclude_also = [
97
107
  "if not anyio:",
98
108
  ]
99
109
 
110
+ [tool.coverage.run]
111
+ omit = [
112
+ "anydi/ext/django/ninja/_operation.py",
113
+ "anydi/ext/django/ninja/_signature.py",
114
+ ]
115
+
100
116
  [build-system]
101
117
  requires = ["poetry-core>=1.0.0"]
102
118
  build-backend = "poetry.core.masonry.api"
@@ -1,113 +0,0 @@
1
- """Shared AnyDI utils module."""
2
-
3
- from __future__ import annotations
4
-
5
- import builtins
6
- import functools
7
- import inspect
8
- import sys
9
- from typing import Any, Callable, TypeVar
10
-
11
- from typing_extensions import Annotated, ParamSpec, get_origin
12
-
13
- try:
14
- import anyio # noqa
15
- except ImportError:
16
- anyio = None # type: ignore[assignment]
17
-
18
-
19
- T = TypeVar("T")
20
- P = ParamSpec("P")
21
-
22
-
23
- def get_full_qualname(obj: Any) -> str:
24
- """Get the fully qualified name of an object.
25
-
26
- This function returns the fully qualified name of the given object,
27
- which includes both the module name and the object's qualname.
28
-
29
- Args:
30
- obj: The object for which to retrieve the fully qualified name.
31
-
32
- Returns:
33
- The fully qualified name of the object.
34
- """
35
- origin = get_origin(obj)
36
- if origin is Annotated:
37
- metadata = ", ".join(
38
- [
39
- f'"{arg}"' if isinstance(arg, str) else str(arg)
40
- for arg in obj.__metadata__
41
- ]
42
- )
43
- return f"Annotated[{get_full_qualname(obj.__args__[0])}, {metadata}]]"
44
-
45
- qualname = getattr(obj, "__qualname__", None)
46
- module_name = getattr(obj, "__module__", None)
47
- if qualname is None:
48
- qualname = type(obj).__qualname__
49
-
50
- if module_name is None:
51
- module_name = type(obj).__module__
52
-
53
- if module_name == builtins.__name__:
54
- return qualname
55
- return f"{module_name}.{qualname}"
56
-
57
-
58
- def is_builtin_type(tp: type[Any]) -> bool:
59
- """
60
- Check if the given type is a built-in type.
61
- Args:
62
- tp (type): The type to check.
63
- Returns:
64
- bool: True if the type is a built-in type, False otherwise.
65
- """
66
- return tp.__module__ == builtins.__name__
67
-
68
-
69
- @functools.lru_cache(maxsize=None)
70
- def get_signature(obj: Callable[..., Any]) -> inspect.Signature:
71
- """Get the signature of a callable object.
72
-
73
- This function uses the `inspect.signature` function to retrieve the signature
74
- of the given callable object. It applies an LRU cache decorator to improve
75
- performance by caching the signatures of previously inspected objects.
76
-
77
- Args:
78
- obj: The callable object to inspect.
79
-
80
- Returns:
81
- The signature of the callable object.
82
- """
83
- signature_kwargs: dict[str, Any] = {}
84
- if sys.version_info >= (3, 10):
85
- signature_kwargs["eval_str"] = True
86
- return inspect.signature(obj, **signature_kwargs)
87
-
88
-
89
- async def run_async(
90
- func: Callable[P, T],
91
- /,
92
- *args: P.args,
93
- **kwargs: P.kwargs,
94
- ) -> T:
95
- """Runs the given function asynchronously using the `anyio` library.
96
-
97
- Args:
98
- func: The function to run asynchronously.
99
- args: The positional arguments to pass to the function.
100
- kwargs: The keyword arguments to pass to the function.
101
-
102
- Returns:
103
- The result of the function.
104
-
105
- Raises:
106
- ImportError: If the `anyio` library is not installed.
107
- """
108
- if not anyio:
109
- raise ImportError(
110
- "`anyio` library is not currently installed. Please make sure to install "
111
- "it first, or consider using `anydi[full]` instead."
112
- )
113
- return await anyio.to_thread.run_sync(functools.partial(func, *args, **kwargs))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes