wireup 2.2.2__tar.gz → 2.3.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 (31) hide show
  1. {wireup-2.2.2 → wireup-2.3.0}/PKG-INFO +4 -4
  2. {wireup-2.2.2 → wireup-2.3.0}/pyproject.toml +1 -1
  3. {wireup-2.2.2 → wireup-2.3.0}/wireup/__init__.py +1 -2
  4. {wireup-2.2.2 → wireup-2.3.0}/wireup/_annotations.py +3 -9
  5. {wireup-2.2.2 → wireup-2.3.0}/wireup/errors.py +1 -1
  6. {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/django/__init__.py +2 -1
  7. {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/django/apps.py +16 -1
  8. wireup-2.3.0/wireup/integration/django/decorators.py +19 -0
  9. {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/container/__init__.py +1 -8
  10. {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/container/async_container.py +11 -2
  11. {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/container/base_container.py +28 -16
  12. {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/factory_compiler.py +3 -1
  13. {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/service_registry.py +55 -14
  14. wireup-2.3.0/wireup/ioc/type_analysis.py +57 -0
  15. {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/util.py +6 -36
  16. {wireup-2.2.2 → wireup-2.3.0}/readme.md +0 -0
  17. {wireup-2.2.2 → wireup-2.3.0}/wireup/_decorators.py +0 -0
  18. {wireup-2.2.2 → wireup-2.3.0}/wireup/_discovery.py +0 -0
  19. {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/__init__.py +0 -0
  20. {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/aiohttp.py +0 -0
  21. {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/click.py +0 -0
  22. {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/fastapi.py +0 -0
  23. {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/flask.py +0 -0
  24. {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/starlette.py +0 -0
  25. {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/__init__.py +0 -0
  26. {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/_exit_stack.py +0 -0
  27. {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/container/sync_container.py +0 -0
  28. {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/override_manager.py +0 -0
  29. {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/parameter.py +0 -0
  30. {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/types.py +0 -0
  31. {wireup-2.2.2 → wireup-2.3.0}/wireup/py.typed +0 -0
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: wireup
3
- Version: 2.2.2
3
+ Version: 2.3.0
4
4
  Summary: Python Dependency Injection Library
5
+ Home-page: https://github.com/maldoinc/wireup
5
6
  License: MIT
6
7
  Keywords: flask,django,injector,dependency injection,dependency injection container,dependency injector
7
8
  Author: Aldo Mateli
@@ -25,8 +26,8 @@ Classifier: Programming Language :: Python :: 3.10
25
26
  Classifier: Programming Language :: Python :: 3.11
26
27
  Classifier: Programming Language :: Python :: 3.12
27
28
  Classifier: Programming Language :: Python :: 3.13
28
- Classifier: Programming Language :: Python :: 3.14
29
29
  Classifier: Programming Language :: Python :: 3 :: Only
30
+ Classifier: Programming Language :: Python :: 3.14
30
31
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
31
32
  Classifier: Typing :: Typed
32
33
  Provides-Extra: eval-type
@@ -34,7 +35,6 @@ Requires-Dist: eval-type-backport (>=0.2.0,<0.3.0) ; (python_version < "3.11") a
34
35
  Requires-Dist: typing_extensions (>=4.7,<5.0)
35
36
  Project-URL: Changelog, https://github.com/maldoinc/wireup/releases
36
37
  Project-URL: Documentation, https://maldoinc.github.io/wireup/
37
- Project-URL: Homepage, https://github.com/maldoinc/wireup
38
38
  Project-URL: Repository, https://github.com/maldoinc/wireup
39
39
  Description-Content-Type: text/markdown
40
40
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "wireup"
3
- version = "2.2.2"
3
+ version = "2.3.0"
4
4
  description = "Python Dependency Injection Library"
5
5
  authors = ["Aldo Mateli <aldo.mateli@gmail.com>"]
6
6
  license = "MIT"
@@ -1,4 +1,4 @@
1
- from wireup._annotations import Inject, Injected, abstract, injectable, service
1
+ from wireup._annotations import Inject, Injected, abstract, service
2
2
  from wireup._decorators import inject_from_container
3
3
  from wireup.ioc.container import (
4
4
  create_async_container,
@@ -21,6 +21,5 @@ __all__ = [
21
21
  "create_async_container",
22
22
  "create_sync_container",
23
23
  "inject_from_container",
24
- "injectable",
25
24
  "service",
26
25
  ]
@@ -23,7 +23,6 @@ if TYPE_CHECKING:
23
23
 
24
24
  def Inject( # noqa: N802
25
25
  *,
26
- config: str | None = None,
27
26
  param: str | None = None,
28
27
  expr: str | None = None,
29
28
  qualifier: Qualifier | None = None,
@@ -42,8 +41,6 @@ def Inject( # noqa: N802
42
41
  """
43
42
  res: InjectableType
44
43
 
45
- if config:
46
- res = ParameterWrapper(config)
47
44
  if param:
48
45
  res = ParameterWrapper(param)
49
46
  elif expr:
@@ -92,7 +89,7 @@ class AbstractDeclaration:
92
89
 
93
90
 
94
91
  @overload
95
- def injectable(
92
+ def service(
96
93
  obj: None = None,
97
94
  *,
98
95
  qualifier: Qualifier | None = None,
@@ -102,7 +99,7 @@ def injectable(
102
99
 
103
100
 
104
101
  @overload
105
- def injectable(
102
+ def service(
106
103
  obj: T,
107
104
  *,
108
105
  qualifier: Qualifier | None = None,
@@ -111,7 +108,7 @@ def injectable(
111
108
  pass
112
109
 
113
110
 
114
- def injectable(
111
+ def service(
115
112
  obj: T | None = None,
116
113
  *,
117
114
  qualifier: Qualifier | None = None,
@@ -129,9 +126,6 @@ def injectable(
129
126
  return _service_decorator if obj is None else _service_decorator(obj)
130
127
 
131
128
 
132
- service = injectable
133
-
134
-
135
129
  def abstract(cls: type[T]) -> type[T]:
136
130
  """Mark the decorated class as an abstract service."""
137
131
  cls.__wireup_registration__ = AbstractDeclaration(cls) # type: ignore[attr-defined]
@@ -71,7 +71,7 @@ class UnknownQualifiedServiceRequestedError(WireupError):
71
71
  class UnknownServiceRequestedError(WireupError):
72
72
  """Raised when requesting an unknown type."""
73
73
 
74
- def __init__(self, klass: type[Any], qualifier: Qualifier | None = None) -> None:
74
+ def __init__(self, klass: type[Any] | None, qualifier: Qualifier | None = None) -> None:
75
75
  qualifier_str = f" with qualifier '{qualifier}'" if qualifier else ""
76
76
  msg = f"Cannot create unknown service {klass}{qualifier_str}. Make sure it is registered with the container."
77
77
  super().__init__(msg)
@@ -1,3 +1,4 @@
1
1
  from wireup.integration.django.apps import WireupSettings, get_app_container, get_request_container, wireup_middleware
2
+ from wireup.integration.django.decorators import inject
2
3
 
3
- __all__ = ["WireupSettings", "get_app_container", "get_request_container", "wireup_middleware"]
4
+ __all__ = ["WireupSettings", "get_app_container", "get_request_container", "inject", "wireup_middleware"]
@@ -114,7 +114,8 @@ class WireupConfig(AppConfig):
114
114
  )
115
115
  self.inject_scoped = inject_from_container(self.container, get_request_container)
116
116
 
117
- self._inject(django.urls.get_resolver())
117
+ if integration_settings.auto_inject_views:
118
+ self._inject(django.urls.get_resolver())
118
119
 
119
120
  def _inject(self, resolver: URLResolver) -> None:
120
121
  for p in resolver.url_patterns:
@@ -123,6 +124,10 @@ class WireupConfig(AppConfig):
123
124
  continue
124
125
 
125
126
  if isinstance(p, URLPattern) and p.callback: # type: ignore[reportUnnecessaryComparison]
127
+ # Skip auto-injection if the view is already marked by @inject decorator
128
+ if getattr(p.callback, "__wireup_marked__", False):
129
+ continue
130
+
126
131
  if hasattr(p.callback, "view_class") and hasattr(p.callback, "view_initkwargs"):
127
132
  p.callback = self._inject_class_based_view(p.callback)
128
133
  else:
@@ -160,3 +165,13 @@ class WireupSettings:
160
165
 
161
166
  service_modules: List[Union[str, ModuleType]]
162
167
  """List of modules containing wireup service registrations."""
168
+
169
+ auto_inject_views: bool = True
170
+ """Whether to automatically inject dependencies into Django views.
171
+
172
+ When True (default), Wireup will automatically inject dependencies into all Django views.
173
+ When False, you must use the @inject decorator explicitly on views that need injection.
174
+
175
+ Set this to False if you want to use @inject explicitly across all views (useful when mixing
176
+ core Django views with third-party views like Django REST framework).
177
+ """
@@ -0,0 +1,19 @@
1
+ from typing import Any, Callable
2
+
3
+ from wireup._decorators import inject_from_container_unchecked
4
+ from wireup.errors import WireupError
5
+ from wireup.integration.django.apps import get_request_container
6
+ from wireup.ioc.util import hide_annotated_names
7
+
8
+
9
+ def inject(func: Callable[..., Any]) -> Callable[..., Any]:
10
+ if getattr(func, "__wireup_marked__", False):
11
+ msg = f"@inject decorator applied multiple times to {func.__module__}.{func.__name__}. Apply it only once."
12
+ raise WireupError(msg)
13
+
14
+ # Modify __signature__ and __annotations__ to hide injectable params (useful for packages like django-ninja)
15
+ # This also stores the original injectable params in __wireup_names__ for later use.
16
+ hide_annotated_names(func)
17
+
18
+ func.__wireup_marked__ = True # type: ignore[attr-defined]
19
+ return inject_from_container_unchecked(get_request_container)(func)
@@ -92,9 +92,7 @@ def _merge_definitions(
92
92
  def create_sync_container(
93
93
  service_modules: list[ModuleType] | None = None,
94
94
  services: list[Any] | None = None,
95
- injectables: list[Any] | None = None,
96
95
  parameters: dict[str, Any] | None = None,
97
- config: dict[str, Any] | None = None,
98
96
  ) -> SyncContainer:
99
97
  """Create a Wireup container.
100
98
 
@@ -106,12 +104,7 @@ def create_sync_container(
106
104
  request parameters via the `Inject(param="name")` syntax.
107
105
  :raises WireupError: Raised if the dependencies cannot be fully resolved.
108
106
  """
109
- return _create_container(
110
- SyncContainer,
111
- service_modules=service_modules,
112
- services=[*(services or []), *(injectables or [])],
113
- parameters=parameters,
114
- )
107
+ return _create_container(SyncContainer, service_modules=service_modules, services=services, parameters=parameters)
115
108
 
116
109
 
117
110
  def create_async_container(
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, TypeVar
3
+ from typing import TYPE_CHECKING, TypeVar, overload
4
4
 
5
5
  from typing_extensions import Self
6
6
 
@@ -18,7 +18,16 @@ T = TypeVar("T")
18
18
 
19
19
 
20
20
  class BareAsyncContainer(BaseContainer):
21
- async def get(self, klass: type[T], qualifier: Qualifier | None = None) -> T:
21
+ @overload
22
+ async def get(self, klass: type[T], qualifier: Qualifier | None = None) -> T: ...
23
+ @overload
24
+ async def get(self, klass: type[T] | None, qualifier: Qualifier | None = None) -> T | None: ...
25
+
26
+ async def get(
27
+ self,
28
+ klass: type[T] | None,
29
+ qualifier: Qualifier | None = None,
30
+ ) -> T | None:
22
31
  """Get an instance of the requested type.
23
32
 
24
33
  :param qualifier: Qualifier for the class if it was registered with one.
@@ -1,27 +1,30 @@
1
+ from __future__ import annotations
2
+
1
3
  from typing import (
4
+ TYPE_CHECKING,
2
5
  Any,
3
6
  AsyncGenerator,
4
- Dict,
5
7
  Generator,
6
8
  List,
7
- Optional,
8
- Type,
9
9
  TypeVar,
10
10
  Union,
11
+ overload,
11
12
  )
12
13
 
13
14
  from wireup.errors import (
14
15
  UnknownServiceRequestedError,
15
16
  WireupError,
16
17
  )
17
- from wireup.ioc.factory_compiler import FactoryCompiler
18
- from wireup.ioc.override_manager import OverrideManager
19
- from wireup.ioc.parameter import ParameterBag
20
- from wireup.ioc.service_registry import ServiceRegistry
21
- from wireup.ioc.types import (
22
- ContainerObjectIdentifier,
23
- Qualifier,
24
- )
18
+
19
+ if TYPE_CHECKING:
20
+ from wireup.ioc.factory_compiler import FactoryCompiler
21
+ from wireup.ioc.override_manager import OverrideManager
22
+ from wireup.ioc.parameter import ParameterBag
23
+ from wireup.ioc.service_registry import ServiceRegistry
24
+ from wireup.ioc.types import (
25
+ ContainerObjectIdentifier,
26
+ Qualifier,
27
+ )
25
28
 
26
29
  T = TypeVar("T")
27
30
  ContainerExitStack = List[Union[Generator[Any, Any, Any], AsyncGenerator[Any, Any]]]
@@ -46,10 +49,10 @@ class BaseContainer:
46
49
  override_manager: OverrideManager,
47
50
  factory_compiler: FactoryCompiler,
48
51
  scoped_compiler: FactoryCompiler,
49
- global_scope_objects: Dict[ContainerObjectIdentifier, Any],
50
- global_scope_exit_stack: List[Union[Generator[Any, Any, Any], AsyncGenerator[Any, Any]]],
51
- current_scope_objects: Optional[Dict[ContainerObjectIdentifier, Any]] = None,
52
- current_scope_exit_stack: Optional[List[Union[Generator[Any, Any, Any], AsyncGenerator[Any, Any]]]] = None,
52
+ global_scope_objects: dict[ContainerObjectIdentifier, Any],
53
+ global_scope_exit_stack: list[Generator[Any, Any, Any] | AsyncGenerator[Any, Any]],
54
+ current_scope_objects: dict[ContainerObjectIdentifier, Any] | None = None,
55
+ current_scope_exit_stack: list[Generator[Any, Any, Any] | AsyncGenerator[Any, Any]] | None = None,
53
56
  ) -> None:
54
57
  self._registry = registry
55
58
  self._override_mgr = override_manager
@@ -71,7 +74,16 @@ class BaseContainer:
71
74
  """Override registered container services with new values."""
72
75
  return self._override_mgr
73
76
 
74
- def _synchronous_get(self, klass: Type[T], qualifier: Optional[Qualifier] = None) -> T:
77
+ @overload
78
+ def _synchronous_get(self, klass: type[T], qualifier: Qualifier | None = None) -> T: ...
79
+ @overload
80
+ def _synchronous_get(self, klass: type[T] | None, qualifier: Qualifier | None = None) -> T | None: ...
81
+
82
+ def _synchronous_get(
83
+ self,
84
+ klass: type[T] | None,
85
+ qualifier: Qualifier | None = None,
86
+ ) -> T | None:
75
87
  """Get an instance of the requested type.
76
88
 
77
89
  :param qualifier: Qualifier for the class if it was registered with one.
@@ -24,6 +24,7 @@ _CONTAINER_SCOPE_ERROR_MSG = (
24
24
  "If you are within a scope, use the scoped container instance to create dependencies."
25
25
  )
26
26
  _WIREUP_GENERATED_FACTORY_NAME = "_wireup_factory"
27
+ _SENTINEL = object()
27
28
 
28
29
 
29
30
  class FactoryCompiler:
@@ -82,7 +83,7 @@ class FactoryCompiler:
82
83
  else:
83
84
  code += " storage = container._current_scope_objects\n"
84
85
 
85
- code += " if res := storage.get(OBJ_ID):\n"
86
+ code += " if (res := storage.get(OBJ_ID, _SENTINEL)) is not _SENTINEL:\n"
86
87
  code += " return res\n"
87
88
 
88
89
  kwargs = ""
@@ -148,6 +149,7 @@ class FactoryCompiler:
148
149
  "TemplatedString": TemplatedString,
149
150
  "WireupError": WireupError,
150
151
  "_CONTAINER_SCOPE_ERROR_MSG": _CONTAINER_SCOPE_ERROR_MSG,
152
+ "_SENTINEL": _SENTINEL,
151
153
  "parameters": self._registry.parameters,
152
154
  }
153
155
 
@@ -18,6 +18,7 @@ from wireup.errors import (
18
18
  WireupError,
19
19
  )
20
20
  from wireup.ioc.parameter import ParameterBag
21
+ from wireup.ioc.type_analysis import analyze_type
21
22
  from wireup.ioc.types import (
22
23
  AnnotatedParameter,
23
24
  AnyCallable,
@@ -26,7 +27,7 @@ from wireup.ioc.types import (
26
27
  ParameterWrapper,
27
28
  ServiceLifetime,
28
29
  )
29
- from wireup.ioc.util import ensure_is_type, get_globals, param_get_annotation, stringify_type, unwrap_optional_type
30
+ from wireup.ioc.util import ensure_is_type, get_globals, param_get_annotation, stringify_type
30
31
 
31
32
  if TYPE_CHECKING:
32
33
  from wireup._annotations import AbstractDeclaration, ServiceDeclaration
@@ -56,6 +57,8 @@ class ServiceFactory:
56
57
  factory: Callable[..., Any]
57
58
  factory_type: FactoryType
58
59
  is_async: bool
60
+ is_optional_type: bool
61
+ raw_type: type
59
62
 
60
63
 
61
64
  ServiceCreationDetails = Tuple[Callable[..., Any], ContainerObjectIdentifier, FactoryType, ServiceLifetime]
@@ -90,7 +93,7 @@ def _function_get_unwrapped_return_type(fn: Callable[..., T]) -> type[T] | None:
90
93
  return None
91
94
  ret = args[0] # Extract the yield type from the generator
92
95
 
93
- return unwrap_optional_type(ret) # type: ignore[no-any-return]
96
+ return ret # type: ignore[no-any-return]
94
97
 
95
98
  return None
96
99
 
@@ -125,7 +128,16 @@ class ServiceRegistry:
125
128
  self._register_abstract(abstract.obj)
126
129
 
127
130
  for impl in impls or []:
128
- self._register(obj=impl.obj, lifetime=impl.lifetime, qualifier=impl.qualifier)
131
+ obj = impl.obj
132
+ if not callable(obj):
133
+ raise InvalidRegistrationTypeError(obj)
134
+
135
+ klass = _function_get_unwrapped_return_type(obj)
136
+
137
+ if klass is None:
138
+ raise FactoryReturnTypeIsEmptyError(obj)
139
+
140
+ self._register(klass=klass, factory_fn=obj, lifetime=impl.lifetime, qualifier=impl.qualifier)
129
141
 
130
142
  self.assert_dependencies_valid()
131
143
  self._precompute_ctors()
@@ -181,17 +193,13 @@ class ServiceRegistry:
181
193
 
182
194
  def _register(
183
195
  self,
184
- obj: Callable[..., Any],
196
+ klass: type[Any],
197
+ factory_fn: Callable[..., Any],
185
198
  lifetime: ServiceLifetime = "singleton",
186
199
  qualifier: Qualifier | None = None,
187
200
  ) -> None:
188
- if not callable(obj):
189
- raise InvalidRegistrationTypeError(obj)
190
-
191
- klass = _function_get_unwrapped_return_type(obj)
192
-
193
- if klass is None:
194
- raise FactoryReturnTypeIsEmptyError(obj)
201
+ type_analysis = analyze_type(klass)
202
+ klass = type_analysis.normalized_type
195
203
 
196
204
  if self.is_type_with_qualifier_known(klass, qualifier):
197
205
  raise DuplicateServiceRegistrationError(klass, qualifier=qualifier)
@@ -208,14 +216,47 @@ class ServiceRegistry:
208
216
  if hasattr(klass, "__bases__"):
209
217
  discover_interfaces(klass.__bases__)
210
218
 
211
- self._target_init_context(obj)
219
+ self._target_init_context(factory_fn)
212
220
  self.lifetime[klass, qualifier] = lifetime
213
- factory_type = _get_factory_type(obj)
221
+ factory_type = _get_factory_type(factory_fn)
214
222
  self.factories[klass, qualifier] = ServiceFactory(
215
- factory=obj, factory_type=factory_type, is_async=factory_type in ASYNC_FACTORY_TYPES
223
+ factory=factory_fn,
224
+ factory_type=factory_type,
225
+ is_async=factory_type in ASYNC_FACTORY_TYPES,
226
+ is_optional_type=type_analysis.is_optional,
227
+ raw_type=type_analysis.raw_type,
216
228
  )
217
229
  self.impls[klass].add(qualifier)
218
230
 
231
+ if type_analysis.is_optional:
232
+ # Backwards compatibility: In earlier versions when a factory returned T | None
233
+ # you could do container.get(T). Alias that type to the normalized T | None factory.
234
+ # Create a fake factory that warns and returns the original instance.
235
+ # https://github.com/maldoinc/wireup/commit/00590dc741035a4c7042c5b6fc434ed08e27f5c0
236
+ def compat_fn(raw_type_instance: Any) -> Any:
237
+ type_name = type_analysis.raw_type.__name__
238
+ deprecated_msg = (
239
+ f"Deprecated: {stringify_type(type_analysis.raw_type)} was registered as optional "
240
+ f"and retrieving it via container.get({type_name}) is deprecated. "
241
+ f"Please use container.get({type_name} | None) or container.get(Optional[{type_name}]) instead."
242
+ )
243
+
244
+ warnings.warn(deprecated_msg, DeprecationWarning, stacklevel=4)
245
+
246
+ return raw_type_instance
247
+
248
+ compat_fn.__signature__ = inspect.Signature( # type: ignore[attr-defined]
249
+ parameters=[
250
+ inspect.Parameter(
251
+ "raw_type_instance",
252
+ kind=inspect.Parameter.POSITIONAL_OR_KEYWORD,
253
+ annotation=klass,
254
+ )
255
+ ],
256
+ )
257
+
258
+ self._register(type_analysis.raw_type, factory_fn=compat_fn, lifetime=lifetime, qualifier=qualifier)
259
+
219
260
  def _register_abstract(self, klass: type) -> None:
220
261
  self.interfaces[klass] = {}
221
262
 
@@ -0,0 +1,57 @@
1
+ import sys
2
+ from dataclasses import dataclass
3
+ from typing import Any, List, Optional, Tuple, Union
4
+
5
+ from typing_extensions import Annotated, get_args, get_origin
6
+
7
+ if sys.version_info >= (3, 10):
8
+ from types import UnionType
9
+ else:
10
+ UnionType = object()
11
+
12
+
13
+ @dataclass(eq=True)
14
+ class TypeAnalysis:
15
+ normalized_type: type
16
+ """The type normalized to Optional[T] (if optional) or T."""
17
+
18
+ raw_type: type
19
+ """The core inner type T, stripped of all Optional/Annotated wrappers."""
20
+
21
+ is_optional: bool
22
+ """True if None was found anywhere in the wrapping layers."""
23
+
24
+ annotations: Tuple[Any, ...]
25
+ """All metadata collected from every Annotated layer found."""
26
+
27
+
28
+ def analyze_type(type_hint: Any) -> TypeAnalysis:
29
+ current_type = type_hint
30
+ annotations: List[Any] = []
31
+ is_optional = False
32
+
33
+ while True:
34
+ origin = get_origin(current_type)
35
+ args = get_args(current_type)
36
+
37
+ if origin is Annotated:
38
+ current_type = args[0]
39
+ annotations.extend(args[1:])
40
+ continue
41
+
42
+ # Handle Union[T, None] / Optional[T] / T | None (if on 3.10+)
43
+ if (origin is Union or origin is UnionType) and type(None) in args:
44
+ is_optional = True
45
+ union_without_none = tuple(arg for arg in args if arg is not type(None))
46
+
47
+ current_type = union_without_none[0] if len(union_without_none) == 1 else Union[union_without_none] # type:ignore[reportUnknownVariableType, unused-ignore]
48
+ continue
49
+
50
+ break
51
+
52
+ return TypeAnalysis(
53
+ normalized_type=Optional[current_type] if is_optional else current_type, # type:ignore[arg-type]
54
+ raw_type=current_type, # type:ignore[arg-type, unused-ignore]
55
+ is_optional=is_optional,
56
+ annotations=tuple(annotations),
57
+ )
@@ -4,16 +4,15 @@ import functools
4
4
  import importlib
5
5
  import inspect
6
6
  import sys
7
- import types
8
7
  import typing
9
8
  from inspect import Parameter
10
9
  from typing import Any, Sequence, TypeVar, cast
11
10
 
12
11
  from wireup.errors import WireupError
12
+ from wireup.ioc.type_analysis import analyze_type
13
13
  from wireup.ioc.types import AnnotatedParameter, AnyCallable, InjectableType
14
14
 
15
15
  T = TypeVar("T")
16
- _OPTIONAL_UNION_ARG_COUNT = 2
17
16
  _eval_type = cast("Callable[..., Any]", typing._eval_type) # type: ignore[attr-defined]
18
17
 
19
18
  if typing.TYPE_CHECKING:
@@ -65,24 +64,12 @@ def param_get_annotation(
65
64
  if not resolved_type:
66
65
  return None
67
66
 
68
- annotation = None
69
- inner_type = resolved_type
67
+ type_analysis = analyze_type(resolved_type)
70
68
 
71
- # Handle Annotated[Optional[T], ...] pattern
72
- if hasattr(resolved_type, "__metadata__") and hasattr(resolved_type, "__args__"):
73
- annotation = _get_wireup_annotation(resolved_type.__metadata__)
74
- inner_type = resolved_type.__args__[0]
75
- unwrapped_type = unwrap_optional_type(inner_type)
76
- inner_type = unwrapped_type
77
- else:
78
- # Handle Optional[T] or Optional[Annotated[T, ...]] pattern
79
- unwrapped_type = unwrap_optional_type(resolved_type)
80
- inner_type = unwrapped_type
81
- if hasattr(inner_type, "__metadata__") and hasattr(inner_type, "__args__"):
82
- annotation = _get_wireup_annotation(inner_type.__metadata__)
83
- inner_type = inner_type.__args__[0]
84
-
85
- return AnnotatedParameter(klass=inner_type, annotation=annotation)
69
+ return AnnotatedParameter(
70
+ klass=type_analysis.normalized_type,
71
+ annotation=_get_wireup_annotation(type_analysis.annotations),
72
+ )
86
73
 
87
74
 
88
75
  def _type_get_globals(typ: type) -> dict[str, Any]:
@@ -169,23 +156,6 @@ def ensure_is_type(value: type[T] | str, globalns_supplier: Callable[[], dict[st
169
156
  raise WireupError(msg) from e
170
157
 
171
158
 
172
- def unwrap_optional_type(type_: Any) -> Any:
173
- """If the given type is Optional[T], returns T. Otherwise returns type_."""
174
- valid_origins = [typing.Union]
175
-
176
- # types.UnionType requires py310+
177
- if union_type := getattr(types, "UnionType", None):
178
- valid_origins.append(union_type)
179
-
180
- origin = typing.get_origin(type_) or type_
181
- if origin in valid_origins:
182
- args = typing.get_args(type_)
183
- if len(args) == _OPTIONAL_UNION_ARG_COUNT and type(None) in args:
184
- return next(arg for arg in args if arg is not type(None))
185
-
186
- return type_
187
-
188
-
189
159
  def stringify_type(target: type | AnyCallable) -> str:
190
160
  return f"{type(target).__name__.capitalize()} {target.__module__}.{target.__name__}"
191
161
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes