anydi 0.42.0__tar.gz → 0.44.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.
- {anydi-0.42.0 → anydi-0.44.0}/PKG-INFO +1 -1
- {anydi-0.42.0 → anydi-0.44.0}/anydi/_container.py +19 -41
- {anydi-0.42.0 → anydi-0.44.0}/anydi/_decorators.py +49 -7
- {anydi-0.42.0 → anydi-0.44.0}/anydi/_module.py +4 -3
- {anydi-0.42.0 → anydi-0.44.0}/anydi/_scan.py +25 -25
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/_utils.py +1 -11
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/_settings.py +0 -2
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/_utils.py +1 -41
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/apps.py +1 -3
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/pytest_plugin.py +4 -4
- {anydi-0.42.0 → anydi-0.44.0}/anydi/testing.py +1 -4
- {anydi-0.42.0 → anydi-0.44.0}/docs/extensions/django.md +1 -2
- {anydi-0.42.0 → anydi-0.44.0}/docs/usage.md +4 -16
- {anydi-0.42.0 → anydi-0.44.0}/pyproject.toml +3 -2
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/fastapi/app.py +1 -1
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/fastapi/test_ext.py +2 -2
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/faststream/test_ext.py +2 -2
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/faststream/test_subscribers.py +1 -1
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/test_pytest_plugin.py +1 -1
- {anydi-0.42.0 → anydi-0.44.0}/tests/test_container.py +40 -79
- {anydi-0.42.0 → anydi-0.44.0}/tests/test_decorators.py +21 -11
- anydi-0.44.0/tests/test_scan.py +68 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/test_testing.py +8 -11
- {anydi-0.42.0 → anydi-0.44.0}/uv.lock +15 -1
- anydi-0.42.0/override.py +0 -0
- anydi-0.42.0/tests/ext/fastapi/test_auto_register.py +0 -34
- anydi-0.42.0/tests/test_scan.py +0 -41
- {anydi-0.42.0 → anydi-0.44.0}/.editorconfig +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/.github/workflows/ci.yml +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/.gitignore +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/.readthedocs.yaml +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/LICENSE +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/Makefile +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/README.md +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/_async.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/_context.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/_provider.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/_scope.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/_typing.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/_container.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/middleware.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/ninja/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/ninja/_operation.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/ninja/_signature.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/fastapi.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/faststream.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/pydantic_settings.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/starlette/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/starlette/middleware.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/anydi/py.typed +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/docs/examples/basic.md +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/docs/extensions/fastapi.md +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/docs/extensions/faststream.md +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/docs/extensions/pydantic_settings.md +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/docs/index.md +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/mkdocs.yml +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/conftest.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/api/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/api/router.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/api/test_router.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/api/urls.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/conftest.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/container.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/scan/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/services.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/settings.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/test_views.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/urls.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/views.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/fastapi/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/fastapi/conftest.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/fastapi/test_routes.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/faststream/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/fixtures.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/starlette/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/starlette/app.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/starlette/conftest.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/starlette/test_routes.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/ext/test_pydantic.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/fixtures.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/a1/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/a1/handlers.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/a2/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/a2/a21/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/a2/a21/handlers.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/a3/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/a3/handlers.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/b/__init__.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/b/handlers.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/test_module.py +0 -0
- {anydi-0.42.0 → anydi-0.44.0}/tests/test_utils.py +0 -0
|
@@ -11,18 +11,15 @@ import uuid
|
|
|
11
11
|
from collections import defaultdict
|
|
12
12
|
from collections.abc import AsyncIterator, Iterable, Iterator
|
|
13
13
|
from contextvars import ContextVar
|
|
14
|
-
from typing import
|
|
14
|
+
from typing import Any, Callable, TypeVar, cast, overload
|
|
15
15
|
|
|
16
16
|
from typing_extensions import ParamSpec, Self, get_args, get_origin
|
|
17
17
|
|
|
18
18
|
from ._async import run_sync
|
|
19
19
|
from ._context import InstanceContext
|
|
20
|
+
from ._decorators import is_provided
|
|
20
21
|
from ._module import ModuleDef, ModuleRegistrar
|
|
21
|
-
from ._provider import
|
|
22
|
-
Provider,
|
|
23
|
-
ProviderDef,
|
|
24
|
-
ProviderKind,
|
|
25
|
-
)
|
|
22
|
+
from ._provider import Provider, ProviderDef, ProviderKind
|
|
26
23
|
from ._scan import PackageOrIterable, Scanner
|
|
27
24
|
from ._scope import ALLOWED_SCOPES, Scope
|
|
28
25
|
from ._typing import (
|
|
@@ -52,12 +49,10 @@ class Container:
|
|
|
52
49
|
*,
|
|
53
50
|
providers: Iterable[ProviderDef] | None = None,
|
|
54
51
|
modules: Iterable[ModuleDef] | None = None,
|
|
55
|
-
strict: bool = False,
|
|
56
52
|
default_scope: Scope = "transient",
|
|
57
53
|
logger: logging.Logger | None = None,
|
|
58
54
|
) -> None:
|
|
59
55
|
self._providers: dict[Any, Provider] = {}
|
|
60
|
-
self._strict = strict
|
|
61
56
|
self._default_scope: Scope = default_scope
|
|
62
57
|
self._logger = logger or logging.getLogger(__name__)
|
|
63
58
|
self._resources: dict[str, list[Any]] = defaultdict(list)
|
|
@@ -90,11 +85,6 @@ class Container:
|
|
|
90
85
|
# Properties
|
|
91
86
|
############################
|
|
92
87
|
|
|
93
|
-
@property
|
|
94
|
-
def strict(self) -> bool:
|
|
95
|
-
"""Check if strict mode is enabled."""
|
|
96
|
-
return self._strict
|
|
97
|
-
|
|
98
88
|
@property
|
|
99
89
|
def default_scope(self) -> Scope:
|
|
100
90
|
"""Get the default scope."""
|
|
@@ -230,6 +220,10 @@ class Container:
|
|
|
230
220
|
"""Check if a provider is registered for the specified interface."""
|
|
231
221
|
return interface in self._providers
|
|
232
222
|
|
|
223
|
+
def has_provider_for(self, interface: Any) -> bool:
|
|
224
|
+
"""Check if a provider exists for the specified interface."""
|
|
225
|
+
return self.is_registered(interface) or is_provided(interface)
|
|
226
|
+
|
|
233
227
|
def unregister(self, interface: Any) -> None:
|
|
234
228
|
"""Unregister a provider by interface."""
|
|
235
229
|
if not self.is_registered(interface):
|
|
@@ -439,19 +433,15 @@ class Container:
|
|
|
439
433
|
try:
|
|
440
434
|
return self._get_provider(interface)
|
|
441
435
|
except LookupError:
|
|
442
|
-
if
|
|
436
|
+
if interface is inspect.Parameter.empty:
|
|
443
437
|
raise
|
|
444
|
-
if
|
|
445
|
-
call = args[0]
|
|
446
|
-
else:
|
|
447
|
-
call = interface
|
|
448
|
-
if inspect.isclass(call) and not is_builtin_type(call):
|
|
438
|
+
if inspect.isclass(interface) and not is_builtin_type(interface):
|
|
449
439
|
# Try to get defined scope
|
|
450
|
-
if
|
|
451
|
-
scope =
|
|
440
|
+
if is_provided(interface):
|
|
441
|
+
scope = interface.__provided__["scope"]
|
|
452
442
|
else:
|
|
453
443
|
scope = parent_scope
|
|
454
|
-
return self._register_provider(
|
|
444
|
+
return self._register_provider(interface, scope, interface, **defaults)
|
|
455
445
|
raise
|
|
456
446
|
|
|
457
447
|
def _set_provider(self, provider: Provider) -> None:
|
|
@@ -467,14 +457,13 @@ class Container:
|
|
|
467
457
|
if provider.is_resource:
|
|
468
458
|
self._resources[provider.scope].remove(provider.interface)
|
|
469
459
|
|
|
460
|
+
@staticmethod
|
|
470
461
|
def _parameter_has_default(
|
|
471
|
-
|
|
462
|
+
parameter: inspect.Parameter, /, **defaults: Any
|
|
472
463
|
) -> bool:
|
|
473
464
|
has_default_in_kwargs = parameter.name in defaults if defaults else False
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
)
|
|
477
|
-
return has_default_in_kwargs or has_non_strict_default
|
|
465
|
+
has_default = parameter.default is not inspect.Parameter.empty
|
|
466
|
+
return has_default_in_kwargs or has_default
|
|
478
467
|
|
|
479
468
|
############################
|
|
480
469
|
# Instance Methods
|
|
@@ -793,8 +782,8 @@ class Container:
|
|
|
793
782
|
return cast(Callable[P, T], self._inject_cache[call])
|
|
794
783
|
|
|
795
784
|
injected_params = self._get_injected_params(call)
|
|
796
|
-
|
|
797
785
|
if not injected_params:
|
|
786
|
+
self._inject_cache[call] = call
|
|
798
787
|
return call
|
|
799
788
|
|
|
800
789
|
if inspect.iscoroutinefunction(call):
|
|
@@ -825,18 +814,7 @@ class Container:
|
|
|
825
814
|
for parameter in get_typed_parameters(call):
|
|
826
815
|
if not is_marker(parameter.default):
|
|
827
816
|
continue
|
|
828
|
-
|
|
829
|
-
self._validate_injected_parameter(call, parameter)
|
|
830
|
-
except LookupError as exc:
|
|
831
|
-
if not self.strict:
|
|
832
|
-
self.logger.debug(
|
|
833
|
-
f"Cannot validate the `{type_repr(call)}` parameter "
|
|
834
|
-
f"`{parameter.name}` with an annotation of "
|
|
835
|
-
f"`{type_repr(parameter.annotation)} due to being "
|
|
836
|
-
"in non-strict mode. It will be validated at the first call."
|
|
837
|
-
)
|
|
838
|
-
else:
|
|
839
|
-
raise exc
|
|
817
|
+
self._validate_injected_parameter(call, parameter)
|
|
840
818
|
injected_params[parameter.name] = parameter.annotation
|
|
841
819
|
return injected_params
|
|
842
820
|
|
|
@@ -849,7 +827,7 @@ class Container:
|
|
|
849
827
|
f"Missing `{type_repr(call)}` parameter `{parameter.name}` annotation."
|
|
850
828
|
)
|
|
851
829
|
|
|
852
|
-
if not self.
|
|
830
|
+
if not self.has_provider_for(parameter.annotation):
|
|
853
831
|
raise LookupError(
|
|
854
832
|
f"`{type_repr(call)}` has an unknown dependency parameter "
|
|
855
833
|
f"`{parameter.name}` with an annotation of "
|
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
from collections.abc import Iterable
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import (
|
|
3
|
+
TYPE_CHECKING,
|
|
4
|
+
Any,
|
|
5
|
+
Callable,
|
|
6
|
+
Concatenate,
|
|
7
|
+
ParamSpec,
|
|
8
|
+
Protocol,
|
|
9
|
+
TypedDict,
|
|
10
|
+
TypeGuard,
|
|
11
|
+
TypeVar,
|
|
12
|
+
overload,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ._module import Module
|
|
17
|
+
|
|
3
18
|
|
|
4
|
-
from ._module import Module
|
|
5
19
|
from ._scope import Scope
|
|
6
20
|
|
|
7
21
|
T = TypeVar("T")
|
|
8
22
|
P = ParamSpec("P")
|
|
9
23
|
|
|
10
24
|
ClassT = TypeVar("ClassT", bound=type)
|
|
11
|
-
ModuleT = TypeVar("ModuleT", bound=Module)
|
|
25
|
+
ModuleT = TypeVar("ModuleT", bound="Module")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ProvidedMetadata(TypedDict):
|
|
29
|
+
"""Metadata for classes marked as provided by AnyDI."""
|
|
30
|
+
|
|
31
|
+
scope: Scope
|
|
12
32
|
|
|
13
33
|
|
|
14
34
|
def provided(*, scope: Scope) -> Callable[[ClassT], ClassT]:
|
|
15
35
|
"""Decorator for marking a class as provided by AnyDI with a specific scope."""
|
|
16
36
|
|
|
17
37
|
def decorator(cls: ClassT) -> ClassT:
|
|
18
|
-
cls.__provided__ =
|
|
19
|
-
cls.__scope__ = scope
|
|
38
|
+
cls.__provided__ = ProvidedMetadata(scope=scope)
|
|
20
39
|
return cls
|
|
21
40
|
|
|
22
41
|
return decorator
|
|
@@ -28,6 +47,14 @@ request = provided(scope="request")
|
|
|
28
47
|
singleton = provided(scope="singleton")
|
|
29
48
|
|
|
30
49
|
|
|
50
|
+
class Provided(Protocol):
|
|
51
|
+
__provided__: ProvidedMetadata
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_provided(cls: Any) -> TypeGuard[type[Provided]]:
|
|
55
|
+
return hasattr(cls, "__provided__")
|
|
56
|
+
|
|
57
|
+
|
|
31
58
|
class ProviderMetadata(TypedDict):
|
|
32
59
|
scope: Scope
|
|
33
60
|
override: bool
|
|
@@ -49,8 +76,15 @@ def provider(
|
|
|
49
76
|
return decorator
|
|
50
77
|
|
|
51
78
|
|
|
79
|
+
class Provider(Protocol):
|
|
80
|
+
__provider__: ProviderMetadata
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def is_provider(obj: Callable[..., Any]) -> TypeGuard[Provider]:
|
|
84
|
+
return hasattr(obj, "__provider__")
|
|
85
|
+
|
|
86
|
+
|
|
52
87
|
class InjectableMetadata(TypedDict):
|
|
53
|
-
wrapped: bool
|
|
54
88
|
tags: Iterable[str] | None
|
|
55
89
|
|
|
56
90
|
|
|
@@ -71,10 +105,18 @@ def injectable(
|
|
|
71
105
|
"""Decorator for marking a function or method as requiring dependency injection."""
|
|
72
106
|
|
|
73
107
|
def decorator(inner: Callable[P, T]) -> Callable[P, T]:
|
|
74
|
-
inner.__injectable__ = InjectableMetadata(
|
|
108
|
+
inner.__injectable__ = InjectableMetadata(tags=tags) # type: ignore
|
|
75
109
|
return inner
|
|
76
110
|
|
|
77
111
|
if func is None:
|
|
78
112
|
return decorator
|
|
79
113
|
|
|
80
114
|
return decorator(func)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Injectable(Protocol):
|
|
118
|
+
__injectable__: InjectableMetadata
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def is_injectable(obj: Callable[..., Any]) -> TypeGuard[Injectable]:
|
|
122
|
+
return hasattr(obj, "__injectable__")
|
|
@@ -4,9 +4,10 @@ import importlib
|
|
|
4
4
|
import inspect
|
|
5
5
|
from typing import TYPE_CHECKING, Any, Callable
|
|
6
6
|
|
|
7
|
+
from ._decorators import ProviderMetadata, is_provider
|
|
8
|
+
|
|
7
9
|
if TYPE_CHECKING:
|
|
8
10
|
from ._container import Container
|
|
9
|
-
from ._decorators import ProviderMetadata
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class ModuleMeta(type):
|
|
@@ -14,9 +15,9 @@ class ModuleMeta(type):
|
|
|
14
15
|
|
|
15
16
|
def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> Any:
|
|
16
17
|
attrs["providers"] = [
|
|
17
|
-
(name,
|
|
18
|
+
(name, value.__provider__)
|
|
18
19
|
for name, value in attrs.items()
|
|
19
|
-
if
|
|
20
|
+
if is_provider(value)
|
|
20
21
|
]
|
|
21
22
|
return super().__new__(cls, name, bases, attrs)
|
|
22
23
|
|
|
@@ -6,9 +6,9 @@ import pkgutil
|
|
|
6
6
|
from collections.abc import Iterable
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from types import ModuleType
|
|
9
|
-
from typing import TYPE_CHECKING, Any, Union
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Callable, Union
|
|
10
10
|
|
|
11
|
-
from ._decorators import
|
|
11
|
+
from ._decorators import is_injectable
|
|
12
12
|
from ._typing import get_typed_parameters, is_marker
|
|
13
13
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
@@ -73,9 +73,8 @@ class Scanner:
|
|
|
73
73
|
|
|
74
74
|
return dependencies
|
|
75
75
|
|
|
76
|
-
@staticmethod
|
|
77
76
|
def _scan_module(
|
|
78
|
-
module: ModuleType, *, tags: Iterable[str]
|
|
77
|
+
self, module: ModuleType, *, tags: Iterable[str]
|
|
79
78
|
) -> list[ScannedDependency]:
|
|
80
79
|
"""Scan a module for decorated members."""
|
|
81
80
|
dependencies: list[ScannedDependency] = []
|
|
@@ -84,27 +83,28 @@ class Scanner:
|
|
|
84
83
|
if getattr(member, "__module__", None) != module.__name__:
|
|
85
84
|
continue
|
|
86
85
|
|
|
87
|
-
|
|
88
|
-
member,
|
|
89
|
-
"__injectable__",
|
|
90
|
-
InjectableMetadata(wrapped=False, tags=[]),
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
should_include = False
|
|
94
|
-
if metadata["wrapped"]:
|
|
95
|
-
should_include = True
|
|
96
|
-
elif tags and metadata["tags"]:
|
|
97
|
-
should_include = bool(set(metadata["tags"]) & set(tags))
|
|
98
|
-
elif tags and not metadata["tags"]:
|
|
99
|
-
continue # tags are provided but member has none
|
|
100
|
-
|
|
101
|
-
if not should_include:
|
|
102
|
-
for param in get_typed_parameters(member):
|
|
103
|
-
if is_marker(param.default):
|
|
104
|
-
should_include = True
|
|
105
|
-
break
|
|
106
|
-
|
|
107
|
-
if should_include:
|
|
86
|
+
if self._should_include_member(member, tags=tags):
|
|
108
87
|
dependencies.append(ScannedDependency(member=member, module=module))
|
|
109
88
|
|
|
110
89
|
return dependencies
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _should_include_member(
|
|
93
|
+
member: Callable[..., Any], *, tags: Iterable[str]
|
|
94
|
+
) -> bool:
|
|
95
|
+
"""Determine if a member should be included based on tags or marker defaults."""
|
|
96
|
+
|
|
97
|
+
if is_injectable(member):
|
|
98
|
+
member_tags = set(member.__injectable__["tags"] or [])
|
|
99
|
+
if tags:
|
|
100
|
+
return bool(set(tags) & member_tags)
|
|
101
|
+
return True # No tags passed → include all injectables
|
|
102
|
+
|
|
103
|
+
# If no tags are passed and not explicitly injectable,
|
|
104
|
+
# check for parameter markers
|
|
105
|
+
if not tags:
|
|
106
|
+
for param in get_typed_parameters(member):
|
|
107
|
+
if is_marker(param.default):
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
return False
|
|
@@ -9,7 +9,6 @@ from typing import Annotated, Any, Callable
|
|
|
9
9
|
from typing_extensions import get_args, get_origin
|
|
10
10
|
|
|
11
11
|
from anydi._container import Container
|
|
12
|
-
from anydi._typing import type_repr
|
|
13
12
|
|
|
14
13
|
logger = logging.getLogger(__name__)
|
|
15
14
|
|
|
@@ -69,15 +68,6 @@ def patch_call_parameter(
|
|
|
69
68
|
if not isinstance(parameter.default, HasInterface):
|
|
70
69
|
return None
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
logger.debug(
|
|
74
|
-
f"Callable `{type_repr(call)}` injected parameter "
|
|
75
|
-
f"`{parameter.name}` with an annotation of "
|
|
76
|
-
f"`{type_repr(parameter.annotation)}` "
|
|
77
|
-
"is not registered. It will be registered at runtime with the "
|
|
78
|
-
"first call because it is running in non-strict mode."
|
|
79
|
-
)
|
|
80
|
-
else:
|
|
81
|
-
container._validate_injected_parameter(call, parameter) # noqa
|
|
71
|
+
container._validate_injected_parameter(call, parameter) # noqa
|
|
82
72
|
|
|
83
73
|
parameter.default.interface = parameter.annotation
|
|
@@ -8,7 +8,6 @@ from typing_extensions import TypedDict
|
|
|
8
8
|
|
|
9
9
|
class Settings(TypedDict):
|
|
10
10
|
CONTAINER_FACTORY: str | None
|
|
11
|
-
STRICT_MODE: bool
|
|
12
11
|
REGISTER_SETTINGS: bool
|
|
13
12
|
REGISTER_COMPONENTS: bool
|
|
14
13
|
INJECT_URLCONF: str | Sequence[str] | None
|
|
@@ -19,7 +18,6 @@ class Settings(TypedDict):
|
|
|
19
18
|
|
|
20
19
|
DEFAULTS = Settings(
|
|
21
20
|
CONTAINER_FACTORY=None,
|
|
22
|
-
STRICT_MODE=False,
|
|
23
21
|
REGISTER_SETTINGS=False,
|
|
24
22
|
REGISTER_COMPONENTS=False,
|
|
25
23
|
MODULES=[],
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Iterator
|
|
4
|
-
from functools import wraps
|
|
5
4
|
from typing import Annotated, Any
|
|
6
5
|
|
|
7
6
|
from django.conf import settings
|
|
@@ -9,7 +8,6 @@ from django.core.cache import BaseCache, caches
|
|
|
9
8
|
from django.db import connections
|
|
10
9
|
from django.db.backends.base.base import BaseDatabaseWrapper
|
|
11
10
|
from django.urls import URLPattern, URLResolver, get_resolver
|
|
12
|
-
from typing_extensions import get_origin
|
|
13
11
|
|
|
14
12
|
from anydi import Container
|
|
15
13
|
|
|
@@ -29,14 +27,11 @@ def register_settings(
|
|
|
29
27
|
continue
|
|
30
28
|
|
|
31
29
|
container.register(
|
|
32
|
-
Annotated[
|
|
30
|
+
Annotated[type(setting_value), f"{prefix}{setting_name}"],
|
|
33
31
|
_get_setting_value(setting_value),
|
|
34
32
|
scope="singleton",
|
|
35
33
|
)
|
|
36
34
|
|
|
37
|
-
# Patch AnyDI to resolve Any types for annotated settings
|
|
38
|
-
_patch_any_typed_annotated(container, prefix=prefix)
|
|
39
|
-
|
|
40
35
|
|
|
41
36
|
def register_components(container: Container) -> None:
|
|
42
37
|
"""Register Django components into the container."""
|
|
@@ -91,38 +86,3 @@ def iter_urlpatterns(
|
|
|
91
86
|
|
|
92
87
|
def _get_setting_value(value: Any) -> Any:
|
|
93
88
|
return lambda: value
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _any_typed_interface(interface: Any, prefix: str) -> Any:
|
|
97
|
-
origin = get_origin(interface)
|
|
98
|
-
if origin is not Annotated:
|
|
99
|
-
return interface # pragma: no cover
|
|
100
|
-
named = interface.__metadata__[-1]
|
|
101
|
-
|
|
102
|
-
if isinstance(named, str) and named.startswith(prefix):
|
|
103
|
-
_, setting_name = named.rsplit(prefix, maxsplit=1)
|
|
104
|
-
return Annotated[Any, f"{prefix}{setting_name}"]
|
|
105
|
-
return interface
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def _patch_any_typed_annotated(container: Container, *, prefix: str) -> None:
|
|
109
|
-
def _patch_resolve(resolve: Any) -> Any:
|
|
110
|
-
@wraps(resolve)
|
|
111
|
-
def wrapper(interface: Any) -> Any:
|
|
112
|
-
return resolve(_any_typed_interface(interface, prefix))
|
|
113
|
-
|
|
114
|
-
return wrapper
|
|
115
|
-
|
|
116
|
-
def _patch_aresolve(resolve: Any) -> Any:
|
|
117
|
-
@wraps(resolve)
|
|
118
|
-
async def wrapper(interface: Any) -> Any:
|
|
119
|
-
return await resolve(_any_typed_interface(interface, prefix))
|
|
120
|
-
|
|
121
|
-
return wrapper
|
|
122
|
-
|
|
123
|
-
container.resolve = _patch_resolve( # type: ignore
|
|
124
|
-
container.resolve
|
|
125
|
-
)
|
|
126
|
-
container.aresolve = _patch_aresolve( # type: ignore
|
|
127
|
-
container.aresolve
|
|
128
|
-
)
|
|
@@ -34,9 +34,7 @@ class ContainerConfig(AppConfig):
|
|
|
34
34
|
) from exc
|
|
35
35
|
self.container = container_factory()
|
|
36
36
|
else:
|
|
37
|
-
self.container = anydi.Container(
|
|
38
|
-
strict=self.settings["STRICT_MODE"],
|
|
39
|
-
)
|
|
37
|
+
self.container = anydi.Container()
|
|
40
38
|
|
|
41
39
|
def ready(self) -> None: # noqa: C901
|
|
42
40
|
# Register Django settings
|
|
@@ -88,8 +88,8 @@ def _anydi_inject(
|
|
|
88
88
|
container = cast(Container, request.getfixturevalue("anydi_setup_container"))
|
|
89
89
|
|
|
90
90
|
for argname, interface in _anydi_injected_parameter_iterator():
|
|
91
|
-
# Skip if the interface
|
|
92
|
-
if
|
|
91
|
+
# Skip if the interface has no provider
|
|
92
|
+
if not container.has_provider_for(interface):
|
|
93
93
|
continue
|
|
94
94
|
|
|
95
95
|
try:
|
|
@@ -128,8 +128,8 @@ def _anydi_ainject(
|
|
|
128
128
|
container = cast(Container, request.getfixturevalue("anydi_setup_container"))
|
|
129
129
|
|
|
130
130
|
for argname, interface in _anydi_injected_parameter_iterator():
|
|
131
|
-
# Skip if the interface
|
|
132
|
-
if
|
|
131
|
+
# Skip if the interface has no provider
|
|
132
|
+
if not container.has_provider_for(interface):
|
|
133
133
|
continue
|
|
134
134
|
|
|
135
135
|
try:
|
|
@@ -23,14 +23,12 @@ class TestContainer(Container):
|
|
|
23
23
|
*,
|
|
24
24
|
providers: Sequence[ProviderDef] | None = None,
|
|
25
25
|
modules: Iterable[ModuleDef] | None = None,
|
|
26
|
-
strict: bool = False,
|
|
27
26
|
default_scope: Scope = "transient",
|
|
28
27
|
logger: logging.Logger | None = None,
|
|
29
28
|
) -> None:
|
|
30
29
|
super().__init__(
|
|
31
30
|
providers=providers,
|
|
32
31
|
modules=modules,
|
|
33
|
-
strict=strict,
|
|
34
32
|
default_scope=default_scope,
|
|
35
33
|
logger=logger,
|
|
36
34
|
)
|
|
@@ -47,7 +45,6 @@ class TestContainer(Container):
|
|
|
47
45
|
)
|
|
48
46
|
for provider in container.providers.values()
|
|
49
47
|
],
|
|
50
|
-
strict=container.strict,
|
|
51
48
|
default_scope=container.default_scope,
|
|
52
49
|
logger=container.logger,
|
|
53
50
|
)
|
|
@@ -57,7 +54,7 @@ class TestContainer(Container):
|
|
|
57
54
|
"""
|
|
58
55
|
Override the provider for the specified interface with a specific instance.
|
|
59
56
|
"""
|
|
60
|
-
if not self.
|
|
57
|
+
if not self.has_provider_for(interface):
|
|
61
58
|
raise LookupError(
|
|
62
59
|
f"The provider interface `{type_repr(interface)}` not registered."
|
|
63
60
|
)
|
|
@@ -59,7 +59,6 @@ The `HelloService` will be automatically injected into the hello view through th
|
|
|
59
59
|
`ANYDI` supports the following settings:
|
|
60
60
|
|
|
61
61
|
* `CONTAINER_FACTORY: str | None` - Specifies the factory function used to create the container. If not provided, the default container factory will be utilized.
|
|
62
|
-
* `STRICT_MODE: bool` - Determines the container's behavior when a dependency cannot be resolved. If set to `True`, the container will raise an exception. If `False`, it will attempt to automatically create the dependency.
|
|
63
62
|
* `REGISTER_SETTINGS: bool` - If `True`, the container will register the Django settings within it.
|
|
64
63
|
* `REGISTER_COMPONENTS: bool` - If `True`, the container will register Django components such as the database and cache.
|
|
65
64
|
* `INJECT_URLCONF: str | Sequence[str] | None` - Specifies the URL configuration where dependencies should be injected.
|
|
@@ -148,7 +147,7 @@ from anydi import Container
|
|
|
148
147
|
|
|
149
148
|
|
|
150
149
|
def get_container() -> Container:
|
|
151
|
-
container = Container(
|
|
150
|
+
container = Container()
|
|
152
151
|
# Add custom container configuration here
|
|
153
152
|
return container
|
|
154
153
|
```
|
|
@@ -145,10 +145,11 @@ assert not container.is_resolved(int)
|
|
|
145
145
|
|
|
146
146
|
This pattern can be used while writing unit tests to ensure that each test case has a clean dependency graph.
|
|
147
147
|
|
|
148
|
-
##
|
|
148
|
+
## Auto-Registration
|
|
149
149
|
|
|
150
150
|
|
|
151
|
-
`AnyDI`
|
|
151
|
+
`AnyDI` doesn't require explicit registration for every type. It can dynamically resolve and auto-register dependencies,
|
|
152
|
+
simplifying setups where manual registration for each type is impractical.
|
|
152
153
|
|
|
153
154
|
Consider a scenario with class dependencies:
|
|
154
155
|
|
|
@@ -194,19 +195,6 @@ assert container.is_resolved(Repository)
|
|
|
194
195
|
assert container.is_resolved(Database)
|
|
195
196
|
```
|
|
196
197
|
|
|
197
|
-
### Enabling Strict Mode
|
|
198
|
-
|
|
199
|
-
For strict checking, enable strict mode by setting `strict=True` when creating the `Container`. In strict mode, all types must be explicitly registered or have a definable provider before instantiation.
|
|
200
|
-
|
|
201
|
-
```python
|
|
202
|
-
container = Container(strict=True)
|
|
203
|
-
|
|
204
|
-
# Raises LookupError if `Service` or dependencies aren't registered.
|
|
205
|
-
_ = container.resolve(Service)
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
Here's an improved version of the documentation with some enhancements for clarity, completeness, and formatting:
|
|
209
|
-
|
|
210
198
|
### Automatic Resource Management
|
|
211
199
|
|
|
212
200
|
When your class dependencies implement the context manager protocol by defining the `__enter__/__aenter__` and `__exit__/__aexit__` methods, these resources are automatically managed by the container for `singleton` and `request` scoped providers.
|
|
@@ -229,7 +217,7 @@ class Connection:
|
|
|
229
217
|
self.disconnected = True
|
|
230
218
|
|
|
231
219
|
|
|
232
|
-
container = Container(
|
|
220
|
+
container = Container()
|
|
233
221
|
connection = container.resolve(Connection)
|
|
234
222
|
|
|
235
223
|
assert container.is_resolved(Connection)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "anydi"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.44.0"
|
|
4
4
|
description = "Dependency Injection library"
|
|
5
5
|
authors = [{ name = "Anton Ruhlov", email = "antonruhlov@gmail.com" }]
|
|
6
6
|
requires-python = "~=3.9"
|
|
@@ -65,6 +65,7 @@ dev = [
|
|
|
65
65
|
"pydantic-settings>=2.4.0,<3",
|
|
66
66
|
"bump-my-version>=1.1.4",
|
|
67
67
|
"pyright>=1.1.401",
|
|
68
|
+
"pytest-mock>=3.14.1",
|
|
68
69
|
]
|
|
69
70
|
docs = [
|
|
70
71
|
"mkdocs>=1.4.2,<2",
|
|
@@ -136,7 +137,7 @@ omit = [
|
|
|
136
137
|
]
|
|
137
138
|
|
|
138
139
|
[tool.bumpversion]
|
|
139
|
-
current_version = "0.
|
|
140
|
+
current_version = "0.44.0"
|
|
140
141
|
parse = """(?x)
|
|
141
142
|
(?P<major>0|[1-9]\\d*)\\.
|
|
142
143
|
(?P<minor>0|[1-9]\\d*)\\.
|
|
@@ -15,7 +15,7 @@ def test_inject_param_missing_interface() -> None:
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_install_without_annotation() -> None:
|
|
18
|
-
container = Container(
|
|
18
|
+
container = Container()
|
|
19
19
|
|
|
20
20
|
@container.provider(scope="singleton")
|
|
21
21
|
def message() -> str:
|
|
@@ -34,7 +34,7 @@ def test_install_without_annotation() -> None:
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def test_install_unknown_annotation() -> None:
|
|
37
|
-
container = Container(
|
|
37
|
+
container = Container()
|
|
38
38
|
|
|
39
39
|
app = FastAPI()
|
|
40
40
|
|
|
@@ -15,7 +15,7 @@ def test_inject_param_missing_interface() -> None:
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_install_without_annotation() -> None:
|
|
18
|
-
container = Container(
|
|
18
|
+
container = Container()
|
|
19
19
|
|
|
20
20
|
@container.provider(scope="singleton")
|
|
21
21
|
def message() -> str:
|
|
@@ -34,7 +34,7 @@ def test_install_without_annotation() -> None:
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def test_install_unknown_annotation() -> None:
|
|
37
|
-
container = Container(
|
|
37
|
+
container = Container()
|
|
38
38
|
|
|
39
39
|
broker = RedisBroker()
|
|
40
40
|
|