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.
Files changed (99) hide show
  1. {anydi-0.42.0 → anydi-0.44.0}/PKG-INFO +1 -1
  2. {anydi-0.42.0 → anydi-0.44.0}/anydi/_container.py +19 -41
  3. {anydi-0.42.0 → anydi-0.44.0}/anydi/_decorators.py +49 -7
  4. {anydi-0.42.0 → anydi-0.44.0}/anydi/_module.py +4 -3
  5. {anydi-0.42.0 → anydi-0.44.0}/anydi/_scan.py +25 -25
  6. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/_utils.py +1 -11
  7. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/_settings.py +0 -2
  8. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/_utils.py +1 -41
  9. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/apps.py +1 -3
  10. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/pytest_plugin.py +4 -4
  11. {anydi-0.42.0 → anydi-0.44.0}/anydi/testing.py +1 -4
  12. {anydi-0.42.0 → anydi-0.44.0}/docs/extensions/django.md +1 -2
  13. {anydi-0.42.0 → anydi-0.44.0}/docs/usage.md +4 -16
  14. {anydi-0.42.0 → anydi-0.44.0}/pyproject.toml +3 -2
  15. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/fastapi/app.py +1 -1
  16. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/fastapi/test_ext.py +2 -2
  17. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/faststream/test_ext.py +2 -2
  18. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/faststream/test_subscribers.py +1 -1
  19. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/test_pytest_plugin.py +1 -1
  20. {anydi-0.42.0 → anydi-0.44.0}/tests/test_container.py +40 -79
  21. {anydi-0.42.0 → anydi-0.44.0}/tests/test_decorators.py +21 -11
  22. anydi-0.44.0/tests/test_scan.py +68 -0
  23. {anydi-0.42.0 → anydi-0.44.0}/tests/test_testing.py +8 -11
  24. {anydi-0.42.0 → anydi-0.44.0}/uv.lock +15 -1
  25. anydi-0.42.0/override.py +0 -0
  26. anydi-0.42.0/tests/ext/fastapi/test_auto_register.py +0 -34
  27. anydi-0.42.0/tests/test_scan.py +0 -41
  28. {anydi-0.42.0 → anydi-0.44.0}/.editorconfig +0 -0
  29. {anydi-0.42.0 → anydi-0.44.0}/.github/workflows/ci.yml +0 -0
  30. {anydi-0.42.0 → anydi-0.44.0}/.gitignore +0 -0
  31. {anydi-0.42.0 → anydi-0.44.0}/.readthedocs.yaml +0 -0
  32. {anydi-0.42.0 → anydi-0.44.0}/LICENSE +0 -0
  33. {anydi-0.42.0 → anydi-0.44.0}/Makefile +0 -0
  34. {anydi-0.42.0 → anydi-0.44.0}/README.md +0 -0
  35. {anydi-0.42.0 → anydi-0.44.0}/anydi/__init__.py +0 -0
  36. {anydi-0.42.0 → anydi-0.44.0}/anydi/_async.py +0 -0
  37. {anydi-0.42.0 → anydi-0.44.0}/anydi/_context.py +0 -0
  38. {anydi-0.42.0 → anydi-0.44.0}/anydi/_provider.py +0 -0
  39. {anydi-0.42.0 → anydi-0.44.0}/anydi/_scope.py +0 -0
  40. {anydi-0.42.0 → anydi-0.44.0}/anydi/_typing.py +0 -0
  41. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/__init__.py +0 -0
  42. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/__init__.py +0 -0
  43. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/_container.py +0 -0
  44. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/middleware.py +0 -0
  45. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/ninja/__init__.py +0 -0
  46. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/ninja/_operation.py +0 -0
  47. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/django/ninja/_signature.py +0 -0
  48. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/fastapi.py +0 -0
  49. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/faststream.py +0 -0
  50. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/pydantic_settings.py +0 -0
  51. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/starlette/__init__.py +0 -0
  52. {anydi-0.42.0 → anydi-0.44.0}/anydi/ext/starlette/middleware.py +0 -0
  53. {anydi-0.42.0 → anydi-0.44.0}/anydi/py.typed +0 -0
  54. {anydi-0.42.0 → anydi-0.44.0}/docs/examples/basic.md +0 -0
  55. {anydi-0.42.0 → anydi-0.44.0}/docs/extensions/fastapi.md +0 -0
  56. {anydi-0.42.0 → anydi-0.44.0}/docs/extensions/faststream.md +0 -0
  57. {anydi-0.42.0 → anydi-0.44.0}/docs/extensions/pydantic_settings.md +0 -0
  58. {anydi-0.42.0 → anydi-0.44.0}/docs/index.md +0 -0
  59. {anydi-0.42.0 → anydi-0.44.0}/mkdocs.yml +0 -0
  60. {anydi-0.42.0 → anydi-0.44.0}/tests/__init__.py +0 -0
  61. {anydi-0.42.0 → anydi-0.44.0}/tests/conftest.py +0 -0
  62. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/__init__.py +0 -0
  63. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/__init__.py +0 -0
  64. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/api/__init__.py +0 -0
  65. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/api/router.py +0 -0
  66. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/api/test_router.py +0 -0
  67. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/api/urls.py +0 -0
  68. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/conftest.py +0 -0
  69. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/container.py +0 -0
  70. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/scan/__init__.py +0 -0
  71. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/services.py +0 -0
  72. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/settings.py +0 -0
  73. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/test_views.py +0 -0
  74. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/urls.py +0 -0
  75. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/django/views.py +0 -0
  76. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/fastapi/__init__.py +0 -0
  77. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/fastapi/conftest.py +0 -0
  78. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/fastapi/test_routes.py +0 -0
  79. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/faststream/__init__.py +0 -0
  80. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/fixtures.py +0 -0
  81. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/starlette/__init__.py +0 -0
  82. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/starlette/app.py +0 -0
  83. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/starlette/conftest.py +0 -0
  84. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/starlette/test_routes.py +0 -0
  85. {anydi-0.42.0 → anydi-0.44.0}/tests/ext/test_pydantic.py +0 -0
  86. {anydi-0.42.0 → anydi-0.44.0}/tests/fixtures.py +0 -0
  87. {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/__init__.py +0 -0
  88. {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/__init__.py +0 -0
  89. {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/a1/__init__.py +0 -0
  90. {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/a1/handlers.py +0 -0
  91. {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/a2/__init__.py +0 -0
  92. {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/a2/a21/__init__.py +0 -0
  93. {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/a2/a21/handlers.py +0 -0
  94. {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/a3/__init__.py +0 -0
  95. {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/a/a3/handlers.py +0 -0
  96. {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/b/__init__.py +0 -0
  97. {anydi-0.42.0 → anydi-0.44.0}/tests/scan_app/b/handlers.py +0 -0
  98. {anydi-0.42.0 → anydi-0.44.0}/tests/test_module.py +0 -0
  99. {anydi-0.42.0 → anydi-0.44.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anydi
3
- Version: 0.42.0
3
+ Version: 0.44.0
4
4
  Summary: Dependency Injection library
5
5
  Project-URL: Repository, https://github.com/antonrh/anydi
6
6
  Author-email: Anton Ruhlov <antonruhlov@gmail.com>
@@ -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 Annotated, Any, Callable, TypeVar, cast, overload
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 self.strict or interface is inspect.Parameter.empty:
436
+ if interface is inspect.Parameter.empty:
443
437
  raise
444
- if get_origin(interface) is Annotated and (args := get_args(interface)):
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 hasattr(call, "__scope__"):
451
- scope = getattr(call, "__scope__")
440
+ if is_provided(interface):
441
+ scope = interface.__provided__["scope"]
452
442
  else:
453
443
  scope = parent_scope
454
- return self._register_provider(call, scope, interface, **defaults)
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
- self, parameter: inspect.Parameter, /, **defaults: Any
462
+ parameter: inspect.Parameter, /, **defaults: Any
472
463
  ) -> bool:
473
464
  has_default_in_kwargs = parameter.name in defaults if defaults else False
474
- has_non_strict_default = not self.strict and (
475
- parameter.default is not inspect.Parameter.empty
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
- try:
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.is_registered(parameter.annotation):
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 Callable, Concatenate, ParamSpec, TypedDict, TypeVar, overload
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__ = True
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(wrapped=True, tags=tags) # type: ignore
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, getattr(value, "__provider__"))
18
+ (name, value.__provider__)
18
19
  for name, value in attrs.items()
19
- if hasattr(value, "__provider__")
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 InjectableMetadata
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
- metadata: InjectableMetadata = getattr(
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
- if not container.strict and not container.is_registered(parameter.annotation):
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[Any, f"{prefix}{setting_name}"],
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 is not registered
92
- if container.strict and not container.is_registered(interface):
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 is not registered
132
- if container.strict and not container.is_registered(interface):
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.is_registered(interface) and self.strict:
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(strict=True)
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
- ## Strict Mode
148
+ ## Auto-Registration
149
149
 
150
150
 
151
- `AnyDI` operates in non-strict mode by default, meaning it doesn't require explicit registration for every type. It can dynamically resolve or auto-register dependencies, simplifying setups where manual registration for each type is impractical.
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(strict=False)
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.42.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.42.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*)\\.
@@ -10,7 +10,7 @@ from anydi.testing import TestContainer
10
10
 
11
11
  from tests.ext.fixtures import Mail, MailService, User, UserService
12
12
 
13
- container = TestContainer(strict=True)
13
+ container = TestContainer()
14
14
 
15
15
 
16
16
  @container.provider(scope="singleton")
@@ -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(strict=True)
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(strict=True)
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(strict=True)
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(strict=True)
37
+ container = Container()
38
38
 
39
39
  broker = RedisBroker()
40
40
 
@@ -18,7 +18,7 @@ class UserMessage(BaseModel):
18
18
 
19
19
  @pytest.fixture(scope="session")
20
20
  def container() -> Container:
21
- container = Container(strict=True)
21
+ container = Container()
22
22
 
23
23
  @container.provider(scope="singleton")
24
24
  def provide_user_service() -> UserService:
@@ -14,7 +14,7 @@ class UnknownService:
14
14
 
15
15
  @pytest.fixture(scope="module")
16
16
  def container() -> Container:
17
- container = Container(strict=True)
17
+ container = Container()
18
18
  container.register(Service, lambda: Service(), scope="singleton")
19
19
  return container
20
20