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.
- dependency_injection_pattern-0.0.0.dist-info/METADATA +104 -0
- dependency_injection_pattern-0.0.0.dist-info/RECORD +36 -0
- dependency_injection_pattern-0.0.0.dist-info/WHEEL +5 -0
- dependency_injection_pattern-0.0.0.dist-info/licenses/LICENSE +21 -0
- dependency_injection_pattern-0.0.0.dist-info/top_level.txt +1 -0
- di/__init__.py +13 -0
- di/core/container.py +336 -0
- di/core/default_scope.py +71 -0
- di/core/exceptions/add_exception.py +7 -0
- di/core/exceptions/container_sealed_error.py +5 -0
- di/core/exceptions/factory_provide_exception.py +25 -0
- di/core/exceptions/implementation_exception.py +13 -0
- di/core/exceptions/provide_exception.py +22 -0
- di/core/exceptions/seal_exception.py +9 -0
- di/core/factories/dependency.py +38 -0
- di/core/factories/factory.py +105 -0
- di/core/factories/scoped_factory.py +18 -0
- di/core/factories/singleton_factory.py +32 -0
- di/core/factories/transient_factory.py +17 -0
- di/core/factory.py +24 -0
- di/core/helpers.py +51 -0
- di/core/log.py +4 -0
- di/core/named_service.py +3 -0
- di/core/protocols/container.py +25 -0
- di/core/protocols/factory.py +7 -0
- di/core/protocols/provider.py +24 -0
- di/core/provider.py +85 -0
- di/core/reflection/__init__.py +126 -0
- di/core/reflection/parameter.py +66 -0
- di/core/reflection/parameter_kind.py +9 -0
- di/core/reflection/parameter_mapper.py +55 -0
- di/core/reflection/signature.py +45 -0
- di/core/reflection/undefined.py +24 -0
- di/core/scope.py +15 -0
- di/core/service_request.py +26 -0
- di/exceptions.py +15 -0
|
@@ -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
|
+
[](https://github.com/apmadsen/dependency-injection-pattern/actions/workflows/python-test.yml)
|
|
35
|
+
[](https://github.com/apmadsen/dependency-injection-pattern/actions/workflows/python-test-coverage.yml)
|
|
36
|
+
[](https://github.com/apmadsen/dependency-injection-pattern/releases)
|
|
37
|
+

|
|
38
|
+

|
|
39
|
+
[](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,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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
di
|
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)
|
di/core/default_scope.py
ADDED
|
@@ -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,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}")
|