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.
- {wireup-2.2.2 → wireup-2.3.0}/PKG-INFO +4 -4
- {wireup-2.2.2 → wireup-2.3.0}/pyproject.toml +1 -1
- {wireup-2.2.2 → wireup-2.3.0}/wireup/__init__.py +1 -2
- {wireup-2.2.2 → wireup-2.3.0}/wireup/_annotations.py +3 -9
- {wireup-2.2.2 → wireup-2.3.0}/wireup/errors.py +1 -1
- {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/django/__init__.py +2 -1
- {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/django/apps.py +16 -1
- wireup-2.3.0/wireup/integration/django/decorators.py +19 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/container/__init__.py +1 -8
- {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/container/async_container.py +11 -2
- {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/container/base_container.py +28 -16
- {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/factory_compiler.py +3 -1
- {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/service_registry.py +55 -14
- wireup-2.3.0/wireup/ioc/type_analysis.py +57 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/util.py +6 -36
- {wireup-2.2.2 → wireup-2.3.0}/readme.md +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/_decorators.py +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/_discovery.py +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/__init__.py +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/aiohttp.py +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/click.py +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/fastapi.py +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/flask.py +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/integration/starlette.py +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/__init__.py +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/_exit_stack.py +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/container/sync_container.py +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/override_manager.py +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/parameter.py +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/ioc/types.py +0 -0
- {wireup-2.2.2 → wireup-2.3.0}/wireup/py.typed +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: wireup
|
|
3
|
-
Version: 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,4 +1,4 @@
|
|
|
1
|
-
from wireup._annotations import Inject, Injected, abstract,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
from wireup.ioc.
|
|
20
|
-
from wireup.ioc.
|
|
21
|
-
from wireup.ioc.
|
|
22
|
-
|
|
23
|
-
|
|
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:
|
|
50
|
-
global_scope_exit_stack:
|
|
51
|
-
current_scope_objects:
|
|
52
|
-
current_scope_exit_stack:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
196
|
+
klass: type[Any],
|
|
197
|
+
factory_fn: Callable[..., Any],
|
|
185
198
|
lifetime: ServiceLifetime = "singleton",
|
|
186
199
|
qualifier: Qualifier | None = None,
|
|
187
200
|
) -> None:
|
|
188
|
-
|
|
189
|
-
|
|
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(
|
|
219
|
+
self._target_init_context(factory_fn)
|
|
212
220
|
self.lifetime[klass, qualifier] = lifetime
|
|
213
|
-
factory_type = _get_factory_type(
|
|
221
|
+
factory_type = _get_factory_type(factory_fn)
|
|
214
222
|
self.factories[klass, qualifier] = ServiceFactory(
|
|
215
|
-
factory=
|
|
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
|
-
|
|
69
|
-
inner_type = resolved_type
|
|
67
|
+
type_analysis = analyze_type(resolved_type)
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
annotation
|
|
74
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|