dependency-injection-pattern 0.0.0__py3-none-any.whl

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.
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: dependency-injection-pattern
3
+ Version: 0.0.0
4
+ Summary: Python implementation of the Dependency Injection pattern.
5
+ Author-email: Anders Madsen <anders.madsen@alphavue.com>
6
+ License: MIT
7
+ Project-URL: repository, https://github.com/apmadsen/dependency-injection-pattern
8
+ Keywords: windows,linux,dependency injection,di,dependency,injection,IoC
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Development Status :: 6 - Mature
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Natural Language :: English
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: typing-utilities==0.0.2
29
+ Provides-Extra: test
30
+ Requires-Dist: pytest>=8.3; extra == "test"
31
+ Requires-Dist: pytest-cov>=6.1; extra == "test"
32
+ Dynamic: license-file
33
+
34
+ [![Test](https://github.com/apmadsen/dependency-injection-pattern/actions/workflows/python-test.yml/badge.svg)](https://github.com/apmadsen/dependency-injection-pattern/actions/workflows/python-test.yml)
35
+ [![Coverage](https://github.com/apmadsen/dependency-injection-pattern/actions/workflows/python-test-coverage.yml/badge.svg)](https://github.com/apmadsen/dependency-injection-pattern/actions/workflows/python-test-coverage.yml)
36
+ [![Stable Version](https://img.shields.io/pypi/v/dependency-injection-pattern?label=stable&sort=semver&color=blue)](https://github.com/apmadsen/dependency-injection-pattern/releases)
37
+ ![Pre-release Version](https://img.shields.io/github/v/release/apmadsen/dependency-injection-pattern?label=pre-release&include_prereleases&sort=semver&color=blue)
38
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/dependency-injection-pattern)
39
+ [![PyPI Downloads](https://static.pepy.tech/badge/dependency-injection-pattern/week)](https://pepy.tech/projects/dependency-injection-pattern)
40
+
41
+ # dependency-injection-pattern
42
+ > A Python implementation of the Dependency Injection pattern.
43
+
44
+ This project extends python with a real dependency injection or Inversion of Control (IoC) implementation, relying on typing and inspection to achieve a seamless injection of dependencies.
45
+
46
+ Though not normally associated with Python applications, dependency injection can greatly reduce time spent during development and testing. Usually, in a longrunning Python application or service, you would put the container registration inside the `__main_.py` file and similar code in test files, where service implementations can easily be substituted.
47
+
48
+ ## How it works
49
+ When a service is requested, the provider looks it up in the factory dictionary, which then gets or creates a fitting instance. Any parameters needed for instantiation is supplied (injected) by the provider.
50
+
51
+ A service implementation may be inferred by a service in which case the service constructor is used, and correspondingly a service may be inferred by the return type of an implementation function.
52
+
53
+ ## Conventions
54
+ - All dependencies are resolved when container is sealed, which implements a fail-fast pattern. Only exceptions are optional dependencies or dependencies with a default value.
55
+
56
+ - Singleton services may not depend on scoped or transient services, as this would negate the "transient" or "scoped" part since a singleton service would keep a reference to it's dependencies indefinitely.
57
+
58
+ ## Example:
59
+
60
+ ```python
61
+ from logging import Logger
62
+ from di import Container
63
+
64
+ def get_logger() -> Logger:
65
+ return Logger("app")
66
+
67
+ class Service1:
68
+ def get_value(self) -> str:
69
+ return "Some value"
70
+
71
+ class Service2:
72
+ def __init__(self, service1: Service1, log: Logger):
73
+ self.service1 = service1
74
+ self.log = log
75
+
76
+ def get_value(self) -> str:
77
+ self.log.debug("Someone requested value...")
78
+ return f"Service1 returned: {self.service1.get_value()}"
79
+
80
+ class Application:
81
+ def __init__(self, service2: Service2, log: Logger):
82
+ self.service2 = service2
83
+ log.info("Application starting")
84
+
85
+ def get_value(self) -> str:
86
+ return f"Service2 returned: {self.service2.get_value()}"
87
+
88
+
89
+ container = Container()
90
+ container.add_singleton(get_logger)
91
+ container.add_transient(Service1)
92
+ container.add_transient(Service2)
93
+ container.add_transient(Application)
94
+
95
+ provider = container.provider() # container is sealed beyond this point
96
+
97
+ app = provider.provide(Application)
98
+
99
+ value = app.get_value() # => "Service2 returned: Service1 returned: Some value"
100
+ ```
101
+
102
+ ## API
103
+
104
+ [Go to API documentation](https://github.com/apmadsen/dependency-injection-pattern/blob/main/docs/api.md)
@@ -0,0 +1,36 @@
1
+ dependency_injection_pattern-0.0.0.dist-info/licenses/LICENSE,sha256=arWCq6_BrCqO8UFdTrMBI356o9Qyqsp_RPvpthW5Yfk,1066
2
+ di/__init__.py,sha256=drok-gOYlq2f6aL-fRBq5EVatHriE9eaKP-OPlWYbSw,288
3
+ di/exceptions.py,sha256=oEZGKkSl-8E3E3mqB46ZovXMGyXC-R1y8Jq-d_HRs1k,588
4
+ di/core/container.py,sha256=n4yYWUsd_PHROVhQkLE4dx4hc1eVHlYv5uBSWj1uq_E,14983
5
+ di/core/default_scope.py,sha256=Bswkav9ImN4ysH9igZfdAg1w1PlkiH0IGecgxULrKJI,3536
6
+ di/core/factory.py,sha256=ntNX0QuwL8CbgVw73hK-aJxbrKkBFpu0AuPSt-fgJEc,751
7
+ di/core/helpers.py,sha256=2jwZRqo8NXn4c7G-OJTy5c5BpHhd9aN6F8ik2O09kGg,1702
8
+ di/core/log.py,sha256=-oiqiq3xW9LRV8xwZIl6ENWQzuhwc62g4iwHMGmtXg4,97
9
+ di/core/named_service.py,sha256=Yq4uB1RsqKm-UpCicaNVLrKiz3MFiZIpLj3sA3kCbo0,155
10
+ di/core/provider.py,sha256=PHWYozeVYcMiA_Rfx1dUTLV_Eyh50mi_aGAkIKBCJzc,2964
11
+ di/core/scope.py,sha256=kdqBL3baKEEouZBFrV4J1YRcI0UFCXx920_skZaXOuo,387
12
+ di/core/service_request.py,sha256=16luchTWfMX_Qp8uLlP9zhXd-suL1CLDrGxh9yu4eCE,608
13
+ di/core/exceptions/add_exception.py,sha256=GiLydWyoydZKcyz9TqSs5KovpI14qUeA_ANMHCVBLsA,286
14
+ di/core/exceptions/container_sealed_error.py,sha256=kgC2Kucq_IUsFZXIgpwnys1rlJzjhEpfs-RUMOvCIlA,197
15
+ di/core/exceptions/factory_provide_exception.py,sha256=CxKAQJMv8IjJQvMrWXKan3PeVQcWBd-BLasl9aR1WXY,1089
16
+ di/core/exceptions/implementation_exception.py,sha256=T9TMLNcBye8TkDCumrHEPETlhYKxJ3r0QyYz1k3hIog,658
17
+ di/core/exceptions/provide_exception.py,sha256=4FeYJHSYTI1c0o8uK753dB6NrND1T13yyT6Al9ucvCw,685
18
+ di/core/exceptions/seal_exception.py,sha256=ySn6Xwh07NsUM625fg9ER7EPYBUVrTcImA8G0zK3YEY,386
19
+ di/core/factories/dependency.py,sha256=WeKorQvGLHs2y117Fc01WvM48nzDF2D0fKPoJaxUJao,1148
20
+ di/core/factories/factory.py,sha256=osh2eMV3xpR2c6c8tdlGwKfl39_D67WDae4jOZaA1Lg,4922
21
+ di/core/factories/scoped_factory.py,sha256=h_zwEYaJ7AGTbgqgeAzeoFwI2RC1UOZIG0JMlo_RzP4,617
22
+ di/core/factories/singleton_factory.py,sha256=GVgclQya0XpzKecVPkbnjGO66RUbj_8IBWLV3fODqdQ,1273
23
+ di/core/factories/transient_factory.py,sha256=gfBFVwlhpC_o3ixNiKSmDAU5aDXykTL6gcwkIRznG7M,529
24
+ di/core/protocols/container.py,sha256=KId3AAjJWq1PJVRZH7d4X_kF-JQJaSJl0opnUtowyTA,594
25
+ di/core/protocols/factory.py,sha256=lF1ldZorxFeRf3zkRwAxtgipbMerGGC4ZxOdqF0VjFE,182
26
+ di/core/protocols/provider.py,sha256=9itUX435vjxyRxsxfZ2Stgri4O4HyUNYiQZLzz51O9M,661
27
+ di/core/reflection/__init__.py,sha256=ncHwkEs3kORETL5pEzWkWnUjusPBmGASPanIoRJYG_o,4493
28
+ di/core/reflection/parameter.py,sha256=ZdD_z4YJrl2oz4ryvH1aW_Gr0CzwsOXoJKRZUoIOLhk,1850
29
+ di/core/reflection/parameter_kind.py,sha256=KwhCxNu6hCCYvEufJeYxM_bbLLEk6Gh6sC_jDy3knuw,291
30
+ di/core/reflection/parameter_mapper.py,sha256=CgKufyvmw7CQr5ORumzOLNYN2fzd_pROKdZsQhbKl0Y,1838
31
+ di/core/reflection/signature.py,sha256=vEW__Jh0e9fq2bNQNTNnv_tDBkYtr5YAEBpCwe5ZQ10,1246
32
+ di/core/reflection/undefined.py,sha256=jCJfBlGeMyjUuFtuSi7HTDdOF_t2lniey5F1j1Hlqzk,590
33
+ dependency_injection_pattern-0.0.0.dist-info/METADATA,sha256=p-rU2BDxMzR8UKeAAgDGqmld_sLxOxyUPMRrtwHyNfs,5110
34
+ dependency_injection_pattern-0.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ dependency_injection_pattern-0.0.0.dist-info/top_level.txt,sha256=w4rm_1b_AMaR2CMWSKiKJ0JFWNHgEYIUil2ob6kFiMM,3
36
+ dependency_injection_pattern-0.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Dagrofa BI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
di/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from di.core.container import Container
2
+ from di.core.provider import Provider
3
+ from di.core.scope import Scope
4
+ from di.core.default_scope import DefaultScope
5
+ from di.core.factory import Factory
6
+
7
+ __all__ = [
8
+ 'Container',
9
+ 'Provider',
10
+ 'Scope',
11
+ 'DefaultScope',
12
+ 'Factory'
13
+ ]
di/core/container.py ADDED
@@ -0,0 +1,336 @@
1
+ from __future__ import annotations
2
+ from typing import TypeVar, Callable, Any, cast, overload, TYPE_CHECKING
3
+ from typingutils import is_type, isinstance_typing, get_optional_type, AnyFunction
4
+ from types import FunctionType
5
+
6
+ from di.core.log import LOG
7
+ from di.core.reflection import reflect_function
8
+ from di.core.scope import Scope
9
+ from di.core.service_request import ServiceRequest
10
+ from di.core.factories.singleton_factory import SingletonFactory
11
+ from di.core.factories.transient_factory import TransientFactory
12
+ from di.core.factories.scoped_factory import ScopedFactory
13
+ from di.core.default_scope import DefaultScope
14
+ from di.core.named_service import NamedService
15
+ from di.core.helpers import clean_service_name, get_service_name, is_context
16
+ from di.core.exceptions.provide_exception import ProvideException
17
+ from di.core.exceptions.add_exception import AddException
18
+ from di.core.exceptions.seal_exception import SealException
19
+ from di.core.exceptions.container_sealed_error import ContainerSealedError
20
+
21
+ if TYPE_CHECKING:
22
+ from di.core.provider import Provider
23
+ from di.core.factories.factory import Factory
24
+
25
+ T = TypeVar('T')
26
+
27
+ class Container:
28
+ """
29
+ The Container handles the definition of services and implementations.
30
+
31
+ Example:
32
+ container = Container()
33
+ container.add_transient(Service)
34
+ provider = container.provider() # container is sealed, and no more services can be added beyound this point
35
+ provider.provide(Service) # => Service
36
+ """
37
+ __slots__ = ["__factories", "__named_factories", "__list_factories", "__default_scope", "__provider"]
38
+
39
+ def __init__(self):
40
+ self.__factories: dict[type[Any], Factory[Any]] = {}
41
+ self.__named_factories: dict[str, tuple[type[Any], Factory[Any]]] = {}
42
+ self.__provider: Provider | None = None
43
+ self.__default_scope = DefaultScope()
44
+
45
+ @property
46
+ def default_scope(self) -> Scope:
47
+ return self.__default_scope
48
+
49
+ @overload
50
+ def add_singleton(self, service: type, /) -> None:
51
+ """Adds a singleton service to provider, which will result in same instance every time it's provided.
52
+
53
+ Args:
54
+ service (type): The service interface
55
+ """
56
+ ...
57
+ @overload
58
+ def add_singleton(self, implementation: AnyFunction, /) -> None:
59
+ """Adds a singleton service to provider, which will result in same instance every time it's provided.
60
+
61
+ Args:
62
+ implementation (AnyFunction) The service implementation
63
+ """
64
+ ...
65
+ @overload
66
+ def add_singleton(self, service: type[T], implementation: type[T] | Callable[..., T] | T, /) -> None:
67
+ """Adds a singleton service to provider, which will result in same instance every time it's provided.
68
+ Implementation may be a type derived from service, a callable or an object derived from, service.
69
+
70
+ Args:
71
+ service (type[T]) The service interface
72
+ implementation (type[T] | Callable[..., T] | T) The service implementation
73
+ """
74
+ ...
75
+ @overload
76
+ def add_singleton(self, service: str, implementation: type[T] | Callable[..., T] | T, /) -> None:
77
+ """Adds a singleton service to provider, which will result in same instance every time it's provided.
78
+ Implementation may be a type derived from service, a callable or an object derived from, service.
79
+
80
+ Args:
81
+ service (str) The service name
82
+ implementation (type[T] | Callable[..., T] | T) The service implementation
83
+ """
84
+ ...
85
+ def add_singleton(self, *args: Any) -> None:
86
+ self.__add_factory_pre(SingletonFactory, *args)
87
+
88
+ @overload
89
+ def add_transient(self, service: type, /) -> None:
90
+ """Adds a transient service to provider, which will result in a new instance every time it's provided.
91
+
92
+ Args:
93
+ service (type) The service interface
94
+
95
+ """
96
+ ...
97
+ @overload
98
+ def add_transient(self, implementation: AnyFunction, /) -> None:
99
+ """Adds a transient service to provider, which will result in a new instance every time it's provided.
100
+
101
+ Args:
102
+ implementation (AnyFunction) The service implementation
103
+ """
104
+ ...
105
+ @overload
106
+ def add_transient(self, service: type[T], implementation: type[T] | Callable[..., T], /) -> None:
107
+ """Adds a transient service to provider, which will result in a new instance every time it's provided.
108
+ Implementation may be a type derived from service or a callable.
109
+
110
+ Args:
111
+ service (str | type[T]) The service interface
112
+ implementation (type[T] | Callable[..., T]) The service implementation
113
+ """
114
+ ...
115
+ @overload
116
+ def add_transient(self, service: str, implementation: type[T] | Callable[..., T] | T, /) -> None:
117
+ """Adds a transient service to provider, which will result in a new instance every time it's provided.
118
+ Implementation may be a type derived from service or a callable.
119
+
120
+ Args:
121
+ service (str) The service name
122
+ implementation (type[T] | Callable[..., T] | T) The service implementation
123
+ """
124
+ ...
125
+ def add_transient(self, *args: Any) -> None:
126
+ self.__add_factory_pre(TransientFactory, *args)
127
+
128
+ @overload
129
+ def add_scoped(self, service: type, /) -> None:
130
+ """Adds a scoped service to provider, which will result in one instance every time it's provided in the same scope.
131
+ The default scope is thread-based, and bound to the lifetime of the thread in which a given service is provided.
132
+
133
+ Note: Scoped services should implement the ContextManager pattern.
134
+
135
+ Args:
136
+ service (type) The service interface
137
+ """
138
+ ...
139
+ @overload
140
+ def add_scoped(self, implementation: AnyFunction, /) -> None:
141
+ """Adds a scoped service to provider, which will result in one instance every time it's provided in the same scope.
142
+ The default scope is thread-based, and bound to the lifetime of the thread in which a given service is provided.
143
+
144
+ Note: Scoped services should implement the ContextManager pattern.
145
+
146
+ Args:
147
+ implementation (AnyFunction) The service implementation
148
+ """
149
+ ...
150
+ @overload
151
+ def add_scoped(self, service: type[T], implementation: type[T] | Callable[..., T], /) -> None:
152
+ """Adds a scoped service to provider, which will result in one instance every time it's provided in the same scope.
153
+ The default scope is thread-based, and bound to the lifetime of the thread in which a given service is provided.
154
+ Implementation may be a type derived from service or a callable.
155
+
156
+ Note: Scoped services should implement the ContextManager pattern.
157
+
158
+ Args:
159
+ service (str | type[T]) The service interface
160
+ implementation (type[T] | Callable[..., T]) The service implementation
161
+ """
162
+ ...
163
+ @overload
164
+ def add_scoped(self, service: str, implementation: type[T] | Callable[..., T] | T, /) -> None:
165
+ """Adds a scoped service to provider, which will result in one instance every time it's provided in the same scope.
166
+ The default scope is thread-based, and bound to the lifetime of the thread in which a given service is provided.
167
+ Implementation may be a type derived from service or a callable.
168
+
169
+ Note: Scoped services should implement the ContextManager pattern.
170
+
171
+ Args:
172
+ service (str) The service name
173
+ implementation (type[T] | Callable[..., T] | T) The service implementation
174
+ """
175
+ ...
176
+ def add_scoped(self, *args: Any) -> None:
177
+ service, svc_type = self.__add_factory_pre(ScopedFactory, *args)
178
+
179
+ if not is_context(svc_type):
180
+ LOG.warning(f"Scoped service '{service}' does not implement the context-manager pattern")
181
+
182
+ def provider(self) -> Provider:
183
+ """Seals Container and returns a Provider
184
+
185
+ Returns:
186
+ Provider -- The Provider instance
187
+ """
188
+ if self.__provider is None:
189
+ from di.core.provider import Provider
190
+ self.__provider = Provider(self)
191
+ self.__add_factory(SingletonFactory, Provider, self.__provider)
192
+
193
+ factories = { **self.__factories, **{ service: factory for service, factory in self.__named_factories.values() } }
194
+ for service, factory in factories.items():
195
+ for dep in factory.dependencies.values():
196
+ if not dep.service in factories:
197
+ if not dep.is_optional and not dep.has_default:
198
+ raise SealException(f"Service '{get_service_name(service)}' depends on service '{get_service_name(dep.service)}' which is not defined in container.")
199
+ else:
200
+ LOG.warning(f"Service '{get_service_name(service)}' depends on service '{get_service_name(dep.service)}' which is not defined in container.")
201
+ elif not dep.is_factory and isinstance(factory, SingletonFactory):
202
+ if isinstance(factories[dep.service], ScopedFactory):
203
+ raise SealException(f"Singleton service '{get_service_name(service)}' depends on scoped service '{get_service_name(dep.service)}' which is not permitted.")
204
+ elif isinstance(factories[dep.service], TransientFactory):
205
+ raise SealException(f"Singleton service '{get_service_name(service)}' depends on transient service {get_service_name(dep.service)}' which is not permitted.")
206
+
207
+ return self.__provider
208
+
209
+ def __add_factory_pre(self, factory_type: type[Factory[T]], *args: Any) -> tuple[str, type[Any]]:
210
+ if self.__provider != None:
211
+ raise ContainerSealedError
212
+
213
+ if len(args) == 2:
214
+ if not is_type(args[0]) and not isinstance_typing(args[0], str):
215
+ raise AddException("Service must be a string or a type")
216
+
217
+ service = cast(str | type, args[0])
218
+ implementation = cast(type | Callable[..., Any] | Any | None, args[1])
219
+ elif is_type(args[0]) or isinstance(args[0], str):
220
+ service = cast(str | type, args[0])
221
+ implementation = None
222
+ else:
223
+ if not isinstance(args[0], FunctionType):
224
+ raise AddException("Implementation must be a function or a type when no service is specified.")
225
+ implementation = cast(Callable[..., Any], args[0])
226
+ signature = reflect_function(implementation)
227
+ service, _ = get_optional_type(cast(type, signature.return_type))
228
+
229
+ return get_service_name(service), self.__add_factory(factory_type, cast(str | type[Any], service), implementation)
230
+
231
+
232
+ def __add_factory(self, factory_type: type[Factory[T]], service: str | type[T], implementation: type[T] | Callable[..., T] | T | None = None) -> type[T]:
233
+ if isinstance(service, str):
234
+ service = clean_service_name(service)
235
+ if service in self.__named_factories:
236
+ raise AddException(f"Service '{get_service_name(service)}' is already defined in provider")
237
+ elif implementation is None:
238
+ raise AddException(f"Cannot add named service '{get_service_name(service)}' without providing an implementation.")
239
+
240
+ if is_type(implementation):
241
+ svc_type = cast(type[T], implementation)
242
+ elif callable(implementation):
243
+ signature = reflect_function(implementation)
244
+ svc_type = cast(type[T], signature.return_type)
245
+ else:
246
+ svc_type = cast(type[T], type(implementation))
247
+
248
+ factory = factory_type(self, NamedService, implementation) # pyright: ignore[reportArgumentType]
249
+ self.__named_factories[service] = (svc_type, factory)
250
+ return svc_type
251
+
252
+ else:
253
+ if service in self.__factories:
254
+ raise AddException(f"Service '{get_service_name(service)}' is already defined in provider.")
255
+ else:
256
+ factory = factory_type(self, service, implementation)
257
+ self.__factories[cast(type, service)] = factory
258
+
259
+ return service
260
+
261
+ def _defines(self, service: str | type) -> bool:
262
+ """Indicates whether or not the specified service interface is defined.
263
+
264
+ Args:
265
+ service (str | type[T]) The service interface or name
266
+
267
+ Returns:
268
+ True or false (bool)
269
+ """
270
+ from di.core.provider import Provider
271
+
272
+ if isinstance(service, str):
273
+ return clean_service_name(service) in self.__named_factories
274
+ elif isinstance(service, type) and issubclass(service, Provider): # pyright: ignore[reportUnnecessaryIsInstance]
275
+ return True
276
+ elif is_type(service):
277
+ return service in self.__factories
278
+
279
+ return False
280
+
281
+ def _get_factory(self, service: type[T] | ServiceRequest[T]) -> tuple[Factory[T] | None, T | None, bool]:
282
+ svc_name: str | None = None
283
+ svc_type: type | None = None
284
+ is_optional = False
285
+ default: T | None = None
286
+ factory: Factory[T] | None = None
287
+
288
+ if isinstance(service, ServiceRequest):
289
+ svc_name = service.name
290
+ svc_type = service.type
291
+ default = service.default
292
+ svc_type, is_optional = cast(tuple[type[Any], bool], get_optional_type(svc_type))
293
+ else:
294
+ service, is_optional = get_optional_type(service)
295
+
296
+
297
+ for svc in [svc_type, svc_name, service]: # pyright: ignore[reportUnknownVariableType]
298
+ if svc is None:
299
+ pass
300
+ elif isinstance(svc, str):
301
+ svc = clean_service_name(svc)
302
+ if svc in self.__named_factories:
303
+ svc_def = self.__named_factories[svc]
304
+ if svc_def[0] is svc_type:
305
+ factory = svc_def[1]
306
+ break
307
+ elif is_type(svc):
308
+ if svc in self.__factories:
309
+ factory = self.__factories[cast(type, svc)]
310
+ break
311
+
312
+ return factory, default, is_optional
313
+
314
+ def _get(self, service: type[T] | ServiceRequest[T]) -> Factory[T] | T | None:
315
+ """Returns the factory for the specified service interface
316
+
317
+ Arguments:
318
+ service {type[T] | ServiceRequest[T]} -- The service interface
319
+
320
+ Raises:
321
+ ProvideException: If service cannot be resolved
322
+
323
+ Returns:
324
+ Factory[T] | T | None -- The service factory
325
+ """
326
+ factory, default, is_optional = self._get_factory(service)
327
+
328
+ if factory:
329
+ return factory
330
+ elif default != None:
331
+ return default
332
+ elif not is_optional:
333
+ if isinstance(service, ServiceRequest):
334
+ raise ProvideException(service.type)
335
+ else:
336
+ raise ProvideException(service)
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+ from typing import TypeVar, Callable, MutableMapping, cast
3
+ from threading import Thread, local, current_thread, main_thread
4
+
5
+ from di.core.log import LOG
6
+ from di.core.scope import Scope
7
+ from di.core.exceptions.provide_exception import ProvideException
8
+ from di.core.helpers import is_context, get_service_name
9
+
10
+ T = TypeVar('T')
11
+
12
+
13
+ class DefaultScope(Scope):
14
+ """
15
+ DefaultScope is as the name implies the default scope. This scope is thread-based, meaning it will supply
16
+ the same instance of a service, each time it's requested on the same thread.
17
+ It will automatically handle object lifetime of scoped services, which are required to implement
18
+ the Contextmanager pattern (supplying standard Python __enter__ and __exit__ methods),
19
+ by finalizing them at the end of scope (i.e. thread exit).
20
+ """
21
+ __slots__ = ["__local"]
22
+
23
+ def __init__(self):
24
+ if not hasattr(self, "_DefaultScope__local"): # pragma: no cover
25
+ self.__local = local()
26
+
27
+ def provide(self, service: type[T], factory: Callable[[], T]) -> T:
28
+ if current_thread() == main_thread():
29
+ raise ProvideException(service,
30
+ """Using DefaultScope on main thread is not permitted.
31
+ If referencing a scoped service on a transient or singleton service,
32
+ consider using the Factory[Service] method""")
33
+
34
+ if not hasattr(self.__local, "dict"):
35
+ LOG.debug(f"Initializing scope for thread '{current_thread().name}'")
36
+ dict: MutableMapping[type[T], T] = {}
37
+ self.__local.dict = dict
38
+
39
+ def finalize(thread: Thread, dict: MutableMapping[type[T], T]) -> None:
40
+ thread.join() # wait until thread is finished
41
+ LOG.debug(f"Finalizing scope for thread '{current_thread().name}'")
42
+ for service, implementation in dict.items():
43
+ try:
44
+ if is_context(service):
45
+ LOG.debug(f"Disposing service '{get_service_name(service)}' on thread '{current_thread().name}'")
46
+ getattr(implementation, "__exit__")()
47
+
48
+ except Exception as ex: # pragma: no cover
49
+ # swallow exception, as it's caught in the finalizer thread
50
+ LOG.error(f"Disposing of service '{get_service_name(service)}' failed: {ex}")
51
+
52
+
53
+ dict.clear()
54
+
55
+ if current_thread() != main_thread(): # only implement finalizer if thread is not main thread # pragma: no cover
56
+ self.__local.finalizer = Thread(target = finalize, args = [ current_thread(), dict ])
57
+ self.__local.finalizer.start()
58
+
59
+ if not service in self.__local.dict:
60
+ impl = factory()
61
+ if current_thread() != main_thread() and is_context(service):
62
+ # finalizer is only implemented when thread is not main thread, otherwise object would never be exited (eg. finalized)
63
+ LOG.debug(f"Initializing service '{get_service_name(service)}' on thread '{current_thread().name}'")
64
+ impl = cast(T, getattr(impl, "__enter__")())
65
+ if not isinstance(impl, service): # pragma: no cover
66
+ raise ProvideException(service, f"Service '{get_service_name(service)}' implementation's __enter__() method returned an object of different type than service")
67
+
68
+ self.__local.dict[service] = impl
69
+
70
+ return cast(T, self.__local.dict[service])
71
+
@@ -0,0 +1,7 @@
1
+ # pragma: no cover
2
+ class AddException(Exception):
3
+ """
4
+ The AddException is raised when container cannot add a service either because service is already registered
5
+ or because the container encountered an error with the specified service and implementation arguments.
6
+ """
7
+
@@ -0,0 +1,5 @@
1
+ # pragma: no cover
2
+ class ContainerSealedError(Exception):
3
+ """
4
+ The ContainerSealedError is raised when container has been sealed, and additional services are being added afterwards.
5
+ """
@@ -0,0 +1,25 @@
1
+ # pragma: no cover
2
+ from typing import Mapping, overload
3
+ from typingutils import AnyType
4
+
5
+ from di.core.helpers import get_service_name
6
+
7
+ class FactoryProvideException(Exception):
8
+ """
9
+ The FactoryProvideException is raised when factory cannot provide service, because one or more
10
+ of it's arguments cannot be provided, or when implementation returns an unexpected value
11
+ or raises an exception.
12
+ """
13
+ @overload
14
+ def __init__(self, service: AnyType | str) -> None:
15
+ ...
16
+ @overload
17
+ def __init__(self, service: AnyType | str, failed_args: Mapping[str, type]) -> None:
18
+ ...
19
+ def __init__(self, service: AnyType | str, failed_args: Mapping[str, type] | None = None):
20
+ service = get_service_name(service)
21
+ if failed_args:
22
+ f_args = ", ".join([ f"'{arg}'({get_service_name(arg_type)})" for arg, arg_type in failed_args.items() ])
23
+ super().__init__(f"Cannot provide service '{service}' because argument(s) {f_args} cannot be provided")
24
+ else:
25
+ super().__init__(f"Cannot provide service '{service}'")
@@ -0,0 +1,13 @@
1
+ # pragma: no cover
2
+ from typing import Any
3
+
4
+ from di.core.helpers import get_service_name
5
+
6
+ class ImplementationException(Exception):
7
+ """
8
+ The ImplementationException is raised at initialization time when factory cannot provide service using the
9
+ specified implementation. This could be due to implementation not resolving to a class derived from service,
10
+ or that implementaion cannot be resolved at all due to missing type annotations.
11
+ """
12
+ def __init__(self, service: type | str, implementation: Any, error: str):
13
+ super().__init__(f"Factory cannot implement service '{get_service_name(service)}' via '{implementation}': {error}")