injex 0.1.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.
- injex-0.1.0/LICENSE +21 -0
- injex-0.1.0/PKG-INFO +60 -0
- injex-0.1.0/README.md +52 -0
- injex-0.1.0/injex/__init__.py +410 -0
- injex-0.1.0/injex.egg-info/PKG-INFO +60 -0
- injex-0.1.0/injex.egg-info/SOURCES.txt +9 -0
- injex-0.1.0/injex.egg-info/dependency_links.txt +1 -0
- injex-0.1.0/injex.egg-info/top_level.txt +1 -0
- injex-0.1.0/pyproject.toml +14 -0
- injex-0.1.0/setup.cfg +4 -0
- injex-0.1.0/tests/test_container.py +883 -0
injex-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Vlad Shulcz
|
|
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.
|
injex-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: injex
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DI container for Python applications
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# DI Container for Python
|
|
15
|
+
|
|
16
|
+
Injex is a lightweight and easy-to-use DI container for Python applications. This library aims to simplify the management of dependencies in your projects, making your code more modular, testable, and maintainable. This library inspired by popular DI frameworks in other programming languages.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
* 🌟 Simple API: Easy to understand and use.
|
|
22
|
+
* 🔄 Support for singleton, transient, and scoped services.
|
|
23
|
+
* Register multiple implementations of the same interface using names.
|
|
24
|
+
* 🔍 Inject dependencies into properties after object creation.
|
|
25
|
+
* 🛠 Handle optional dependencies gracefully.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Why Use Dependency Injection?
|
|
29
|
+
|
|
30
|
+
**Dependency Injection is a design pattern that helps in:**
|
|
31
|
+
|
|
32
|
+
* Modularity: Breaking down your application into interchangeable components.
|
|
33
|
+
* Testability: Facilitating unit testing by allowing dependencies to be mocked or stubbed.
|
|
34
|
+
* Maintainability: Making it easier to update, replace, or refactor components without affecting other parts of the application.
|
|
35
|
+
* Flexibility: Configuring different implementations of the same interface for various scenarios (e.g., testing, production).
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
Here's a simple example of usage Injex:
|
|
40
|
+
```python
|
|
41
|
+
from abc import ABC, abstractmethod
|
|
42
|
+
from injex import Container
|
|
43
|
+
|
|
44
|
+
class IService(ABC):
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def perform_action(self):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
class ServiceImplementation(IService):
|
|
50
|
+
def perform_action(self):
|
|
51
|
+
print("Service is performing an action.")
|
|
52
|
+
|
|
53
|
+
container = Container()
|
|
54
|
+
|
|
55
|
+
container.add_transient(IService, ServiceImplementation)
|
|
56
|
+
|
|
57
|
+
service = container.resolve(IService)
|
|
58
|
+
service.perform_action() # output: Service is performing an action.
|
|
59
|
+
```
|
|
60
|
+
Another examples in [examples folder](./examples).
|
injex-0.1.0/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# DI Container for Python
|
|
7
|
+
|
|
8
|
+
Injex is a lightweight and easy-to-use DI container for Python applications. This library aims to simplify the management of dependencies in your projects, making your code more modular, testable, and maintainable. This library inspired by popular DI frameworks in other programming languages.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
* 🌟 Simple API: Easy to understand and use.
|
|
14
|
+
* 🔄 Support for singleton, transient, and scoped services.
|
|
15
|
+
* Register multiple implementations of the same interface using names.
|
|
16
|
+
* 🔍 Inject dependencies into properties after object creation.
|
|
17
|
+
* 🛠 Handle optional dependencies gracefully.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Why Use Dependency Injection?
|
|
21
|
+
|
|
22
|
+
**Dependency Injection is a design pattern that helps in:**
|
|
23
|
+
|
|
24
|
+
* Modularity: Breaking down your application into interchangeable components.
|
|
25
|
+
* Testability: Facilitating unit testing by allowing dependencies to be mocked or stubbed.
|
|
26
|
+
* Maintainability: Making it easier to update, replace, or refactor components without affecting other parts of the application.
|
|
27
|
+
* Flexibility: Configuring different implementations of the same interface for various scenarios (e.g., testing, production).
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
Here's a simple example of usage Injex:
|
|
32
|
+
```python
|
|
33
|
+
from abc import ABC, abstractmethod
|
|
34
|
+
from injex import Container
|
|
35
|
+
|
|
36
|
+
class IService(ABC):
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def perform_action(self):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
class ServiceImplementation(IService):
|
|
42
|
+
def perform_action(self):
|
|
43
|
+
print("Service is performing an action.")
|
|
44
|
+
|
|
45
|
+
container = Container()
|
|
46
|
+
|
|
47
|
+
container.add_transient(IService, ServiceImplementation)
|
|
48
|
+
|
|
49
|
+
service = container.resolve(IService)
|
|
50
|
+
service.perform_action() # output: Service is performing an action.
|
|
51
|
+
```
|
|
52
|
+
Another examples in [examples folder](./examples).
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import types
|
|
3
|
+
from typing import (
|
|
4
|
+
Any,
|
|
5
|
+
Callable,
|
|
6
|
+
Dict,
|
|
7
|
+
List,
|
|
8
|
+
Optional,
|
|
9
|
+
Set,
|
|
10
|
+
Tuple,
|
|
11
|
+
Type,
|
|
12
|
+
Union,
|
|
13
|
+
get_args,
|
|
14
|
+
get_origin,
|
|
15
|
+
get_type_hints,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DIException(Exception):
|
|
20
|
+
"""Base exception class for dependency injection errors."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ServiceNotRegisteredException(DIException):
|
|
24
|
+
def __init__(self, interface_description: str):
|
|
25
|
+
super().__init__(
|
|
26
|
+
f"Service for interface '{interface_description}' is not registered."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CyclicDependencyException(DIException):
|
|
31
|
+
def __init__(self, cls: Type):
|
|
32
|
+
super().__init__(f"Cyclic dependency detected: {cls}.")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MissingTypeAnnotationException(DIException):
|
|
36
|
+
def __init__(self, param_name: str, cls: Type):
|
|
37
|
+
super().__init__(
|
|
38
|
+
f"Missing type annotation for parameter '{param_name}' in class '{cls.__name__}'."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class InvalidLifestyleException(DIException):
|
|
43
|
+
def __init__(self, lifestyle: str):
|
|
44
|
+
super().__init__(
|
|
45
|
+
f"Invalid lifestyle '{lifestyle}'. Valid options are 'transient' or 'singleton'."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def inject(func: Callable) -> Callable:
|
|
50
|
+
func.__annotations__["_inject"] = True
|
|
51
|
+
return func
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_injectable(func: Callable) -> bool:
|
|
55
|
+
return hasattr(func, "__annotations__") and func.__annotations__.get(
|
|
56
|
+
"_inject", False
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class LifeStyle:
|
|
61
|
+
TRANSIENT = "transient"
|
|
62
|
+
SINGLETON = "singleton"
|
|
63
|
+
SCOPED = "scoped"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class RegistrationType:
|
|
67
|
+
SERVICE = "service"
|
|
68
|
+
FACTORY = "factory"
|
|
69
|
+
INSTANCE = "instance"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Scope:
|
|
73
|
+
__slots__ = ("container", "_scoped_instances")
|
|
74
|
+
|
|
75
|
+
def __init__(self, container: "Container"):
|
|
76
|
+
self.container = container
|
|
77
|
+
self._scoped_instances: Dict[Any, Any] = {}
|
|
78
|
+
|
|
79
|
+
def resolve(self, interface: Union[Type, str], name: Optional[str] = None) -> Any:
|
|
80
|
+
instances = self.container._resolve_in_scope(interface, self, name)
|
|
81
|
+
if not instances:
|
|
82
|
+
interface_name = f"{interface}"
|
|
83
|
+
if name is not None:
|
|
84
|
+
interface_name += f" with name '{name}'"
|
|
85
|
+
raise ServiceNotRegisteredException(interface_name)
|
|
86
|
+
return instances[0]
|
|
87
|
+
|
|
88
|
+
def resolve_all(
|
|
89
|
+
self, interface: Union[Type, str], name: Optional[str] = None
|
|
90
|
+
) -> List[Any]:
|
|
91
|
+
return self.container._resolve_in_scope(interface, self, name)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Registration:
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
kind: str, # registration type
|
|
98
|
+
implementation: Optional[Type] = None,
|
|
99
|
+
factory: Optional[Callable[..., Any]] = None,
|
|
100
|
+
instance: Optional[Any] = None,
|
|
101
|
+
lifestyle: str = LifeStyle.TRANSIENT,
|
|
102
|
+
):
|
|
103
|
+
self.kind = kind
|
|
104
|
+
self.implementation = implementation
|
|
105
|
+
self.factory = factory
|
|
106
|
+
self.instance = instance
|
|
107
|
+
self.lifestyle = lifestyle
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Container:
|
|
111
|
+
__slots__ = ("_registrations", "_singletons", "_resolving")
|
|
112
|
+
|
|
113
|
+
def __init__(self):
|
|
114
|
+
self._registrations: Dict[
|
|
115
|
+
Tuple[Union[Type, str], Optional[str]], List[Registration]
|
|
116
|
+
] = {}
|
|
117
|
+
self._singletons: Dict[Any, Any] = {}
|
|
118
|
+
self._resolving: Set[Type] = set()
|
|
119
|
+
|
|
120
|
+
def register(
|
|
121
|
+
self,
|
|
122
|
+
interface: Type,
|
|
123
|
+
implementation: Optional[Type] = None,
|
|
124
|
+
lifestyle: str = LifeStyle.TRANSIENT,
|
|
125
|
+
name: Optional[str] = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
if lifestyle not in (
|
|
128
|
+
LifeStyle.TRANSIENT,
|
|
129
|
+
LifeStyle.SINGLETON,
|
|
130
|
+
LifeStyle.SCOPED,
|
|
131
|
+
):
|
|
132
|
+
raise InvalidLifestyleException(lifestyle)
|
|
133
|
+
if implementation is None:
|
|
134
|
+
implementation = interface
|
|
135
|
+
key = (interface, name)
|
|
136
|
+
registration = Registration(
|
|
137
|
+
kind=RegistrationType.SERVICE,
|
|
138
|
+
implementation=implementation,
|
|
139
|
+
lifestyle=lifestyle,
|
|
140
|
+
)
|
|
141
|
+
self._registrations.setdefault(key, []).append(registration)
|
|
142
|
+
|
|
143
|
+
def register_factory(
|
|
144
|
+
self,
|
|
145
|
+
interface: Type,
|
|
146
|
+
factory: Callable[..., Any],
|
|
147
|
+
lifestyle: str = LifeStyle.TRANSIENT,
|
|
148
|
+
name: Optional[str] = None,
|
|
149
|
+
) -> None:
|
|
150
|
+
if not callable(factory):
|
|
151
|
+
raise ValueError("Factory must be callable")
|
|
152
|
+
if lifestyle not in (
|
|
153
|
+
LifeStyle.TRANSIENT,
|
|
154
|
+
LifeStyle.SINGLETON,
|
|
155
|
+
LifeStyle.SCOPED,
|
|
156
|
+
):
|
|
157
|
+
raise InvalidLifestyleException(lifestyle)
|
|
158
|
+
key = (interface, name)
|
|
159
|
+
registration = Registration(
|
|
160
|
+
kind=RegistrationType.FACTORY, factory=factory, lifestyle=lifestyle
|
|
161
|
+
)
|
|
162
|
+
self._registrations.setdefault(key, []).append(registration)
|
|
163
|
+
|
|
164
|
+
def add_instance(
|
|
165
|
+
self, interface: Type, instance: Any, name: Optional[str] = None
|
|
166
|
+
) -> None:
|
|
167
|
+
key = (interface, name)
|
|
168
|
+
registration = Registration(
|
|
169
|
+
kind=RegistrationType.INSTANCE,
|
|
170
|
+
instance=instance,
|
|
171
|
+
lifestyle=LifeStyle.SINGLETON,
|
|
172
|
+
)
|
|
173
|
+
self._registrations.setdefault(key, []).append(registration)
|
|
174
|
+
|
|
175
|
+
def resolve(self, interface: Union[Type, str], name: Optional[str] = None) -> Any:
|
|
176
|
+
scope = self.create_scope()
|
|
177
|
+
return scope.resolve(interface, name)
|
|
178
|
+
|
|
179
|
+
def resolve_all(
|
|
180
|
+
self, interface: Union[Type, str], name: Optional[str] = None
|
|
181
|
+
) -> List[Any]:
|
|
182
|
+
scope = self.create_scope()
|
|
183
|
+
return scope.resolve_all(interface, name)
|
|
184
|
+
|
|
185
|
+
def create_scope(self) -> Scope:
|
|
186
|
+
return Scope(self)
|
|
187
|
+
|
|
188
|
+
def _resolve_in_scope(
|
|
189
|
+
self, interface: Union[Type, str], scope: Scope, name: Optional[str] = None
|
|
190
|
+
) -> List[Any]:
|
|
191
|
+
key = (interface, name)
|
|
192
|
+
registrations = self._registrations.get(key, [])
|
|
193
|
+
instances = []
|
|
194
|
+
for registration in registrations:
|
|
195
|
+
instance = self._get_instance_from_registration(registration, scope, key)
|
|
196
|
+
instances.append(instance)
|
|
197
|
+
return instances
|
|
198
|
+
|
|
199
|
+
def _get_instance_from_registration(
|
|
200
|
+
self,
|
|
201
|
+
registration: Registration,
|
|
202
|
+
scope: Scope,
|
|
203
|
+
key: Tuple[Union[Type, str], Optional[str]],
|
|
204
|
+
) -> Any:
|
|
205
|
+
instance_key = (key, registration)
|
|
206
|
+
if registration.kind == RegistrationType.INSTANCE:
|
|
207
|
+
return registration.instance
|
|
208
|
+
|
|
209
|
+
lifestyle = registration.lifestyle
|
|
210
|
+
|
|
211
|
+
if lifestyle == LifeStyle.SINGLETON:
|
|
212
|
+
if instance_key in self._singletons:
|
|
213
|
+
return self._singletons[instance_key]
|
|
214
|
+
instance = self._create_instance_from_registration(registration, scope)
|
|
215
|
+
self._singletons[instance_key] = instance
|
|
216
|
+
return instance
|
|
217
|
+
elif lifestyle == LifeStyle.SCOPED:
|
|
218
|
+
if instance_key in scope._scoped_instances:
|
|
219
|
+
return scope._scoped_instances[instance_key]
|
|
220
|
+
instance = self._create_instance_from_registration(registration, scope)
|
|
221
|
+
scope._scoped_instances[instance_key] = instance
|
|
222
|
+
return instance
|
|
223
|
+
else: # transient
|
|
224
|
+
return self._create_instance_from_registration(registration, scope)
|
|
225
|
+
|
|
226
|
+
def _create_instance_from_registration(
|
|
227
|
+
self, registration: Registration, scope: Scope
|
|
228
|
+
) -> Any:
|
|
229
|
+
if registration.kind == RegistrationType.SERVICE:
|
|
230
|
+
if registration.implementation is not None:
|
|
231
|
+
return self._create_instance(registration.implementation, scope)
|
|
232
|
+
else:
|
|
233
|
+
raise ValueError(
|
|
234
|
+
"Implementation cannot be None for service registration."
|
|
235
|
+
)
|
|
236
|
+
elif registration.kind == RegistrationType.FACTORY:
|
|
237
|
+
if registration.factory is not None:
|
|
238
|
+
return self._invoke_factory(registration.factory, scope)
|
|
239
|
+
else:
|
|
240
|
+
raise ValueError("Factory cannot be None for factory registration.")
|
|
241
|
+
else:
|
|
242
|
+
raise ValueError(f"Invalid registration kind: {registration.kind}")
|
|
243
|
+
|
|
244
|
+
def _invoke_factory(self, factory: Callable[..., Any], scope: Scope) -> Any:
|
|
245
|
+
sig = inspect.signature(factory)
|
|
246
|
+
params = sig.parameters
|
|
247
|
+
args = []
|
|
248
|
+
for name, param in params.items():
|
|
249
|
+
if param.annotation == inspect.Parameter.empty:
|
|
250
|
+
if name == "container":
|
|
251
|
+
dependency = self
|
|
252
|
+
else:
|
|
253
|
+
raise MissingTypeAnnotationException(name, factory) # type: ignore
|
|
254
|
+
else:
|
|
255
|
+
param_annotation = param.annotation
|
|
256
|
+
|
|
257
|
+
is_optional = False
|
|
258
|
+
origin = get_origin(param_annotation)
|
|
259
|
+
if origin in (Union, types.UnionType):
|
|
260
|
+
args_ = get_args(param_annotation)
|
|
261
|
+
if type(None) in args_:
|
|
262
|
+
is_optional = True
|
|
263
|
+
non_none_args = [a for a in args_ if a is not type(None)]
|
|
264
|
+
if non_none_args:
|
|
265
|
+
param_annotation = non_none_args[0]
|
|
266
|
+
else:
|
|
267
|
+
param_annotation = Any
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
dependency = scope.resolve(param_annotation) # type: ignore
|
|
271
|
+
except ServiceNotRegisteredException:
|
|
272
|
+
if param.default != inspect.Parameter.empty:
|
|
273
|
+
dependency = param.default
|
|
274
|
+
elif is_optional:
|
|
275
|
+
dependency = None
|
|
276
|
+
else:
|
|
277
|
+
raise
|
|
278
|
+
args.append(dependency)
|
|
279
|
+
return factory(*args)
|
|
280
|
+
|
|
281
|
+
def _inject_properties(self, instance: object, scope: Scope) -> None:
|
|
282
|
+
for name in dir(instance):
|
|
283
|
+
attr = getattr(instance, name)
|
|
284
|
+
if callable(attr) and is_injectable(attr):
|
|
285
|
+
if name in instance.__dict__:
|
|
286
|
+
continue
|
|
287
|
+
type_hints = get_type_hints(attr)
|
|
288
|
+
if (
|
|
289
|
+
"return" in type_hints
|
|
290
|
+
and type_hints["return"] != inspect.Parameter.empty
|
|
291
|
+
):
|
|
292
|
+
dependency_type = type_hints["return"]
|
|
293
|
+
|
|
294
|
+
if dependency_type in self._resolving:
|
|
295
|
+
raise CyclicDependencyException(dependency_type)
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
dependency = scope.resolve(dependency_type)
|
|
299
|
+
except ServiceNotRegisteredException as e:
|
|
300
|
+
raise e
|
|
301
|
+
|
|
302
|
+
setattr(instance, name, dependency)
|
|
303
|
+
|
|
304
|
+
def _create_instance(self, cls: Type, scope: Scope) -> Any:
|
|
305
|
+
if cls in self._resolving:
|
|
306
|
+
raise CyclicDependencyException(cls)
|
|
307
|
+
|
|
308
|
+
self._resolving.add(cls)
|
|
309
|
+
try:
|
|
310
|
+
constructor = cls.__init__
|
|
311
|
+
if constructor is object.__init__:
|
|
312
|
+
instance = cls()
|
|
313
|
+
else:
|
|
314
|
+
type_hints = get_type_hints(constructor)
|
|
315
|
+
params = inspect.signature(constructor).parameters
|
|
316
|
+
args = []
|
|
317
|
+
for name, param in params.items():
|
|
318
|
+
if name == "self":
|
|
319
|
+
continue
|
|
320
|
+
param_annotation = type_hints.get(name, param.annotation)
|
|
321
|
+
|
|
322
|
+
is_optional = False
|
|
323
|
+
origin = get_origin(param_annotation)
|
|
324
|
+
if origin in (Union, types.UnionType):
|
|
325
|
+
args_ = get_args(param_annotation)
|
|
326
|
+
if type(None) in args_:
|
|
327
|
+
is_optional = True
|
|
328
|
+
non_none_args = [a for a in args_ if a is not type(None)]
|
|
329
|
+
if non_none_args:
|
|
330
|
+
param_annotation = non_none_args[0]
|
|
331
|
+
else:
|
|
332
|
+
param_annotation = Any
|
|
333
|
+
|
|
334
|
+
if param_annotation == inspect.Parameter.empty:
|
|
335
|
+
raise MissingTypeAnnotationException(name, cls)
|
|
336
|
+
|
|
337
|
+
if param_annotation in self._resolving:
|
|
338
|
+
raise CyclicDependencyException(param_annotation)
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
dependency = scope.resolve(param_annotation) # type: ignore
|
|
342
|
+
except ServiceNotRegisteredException:
|
|
343
|
+
if param.default != inspect.Parameter.empty:
|
|
344
|
+
dependency = param.default
|
|
345
|
+
elif is_optional:
|
|
346
|
+
dependency = None
|
|
347
|
+
else:
|
|
348
|
+
raise
|
|
349
|
+
args.append(dependency)
|
|
350
|
+
instance = cls(*args)
|
|
351
|
+
self._inject_properties(instance, scope)
|
|
352
|
+
return instance
|
|
353
|
+
finally:
|
|
354
|
+
self._resolving.remove(cls)
|
|
355
|
+
|
|
356
|
+
def add_singleton(
|
|
357
|
+
self,
|
|
358
|
+
interface: Type,
|
|
359
|
+
implementation: Optional[Type] = None,
|
|
360
|
+
name: Optional[str] = None,
|
|
361
|
+
) -> None:
|
|
362
|
+
self.register(
|
|
363
|
+
interface, implementation, lifestyle=LifeStyle.SINGLETON, name=name
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def add_transient(
|
|
367
|
+
self,
|
|
368
|
+
interface: Type,
|
|
369
|
+
implementation: Optional[Type] = None,
|
|
370
|
+
name: Optional[str] = None,
|
|
371
|
+
) -> None:
|
|
372
|
+
self.register(
|
|
373
|
+
interface, implementation, lifestyle=LifeStyle.TRANSIENT, name=name
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def add_scoped(
|
|
377
|
+
self,
|
|
378
|
+
interface: Type,
|
|
379
|
+
implementation: Optional[Type] = None,
|
|
380
|
+
name: Optional[str] = None,
|
|
381
|
+
) -> None:
|
|
382
|
+
self.register(interface, implementation, lifestyle=LifeStyle.SCOPED, name=name)
|
|
383
|
+
|
|
384
|
+
def add_singleton_factory(
|
|
385
|
+
self,
|
|
386
|
+
interface: Type,
|
|
387
|
+
factory: Callable[..., Any],
|
|
388
|
+
name: Optional[str] = None,
|
|
389
|
+
) -> None:
|
|
390
|
+
self.register_factory(
|
|
391
|
+
interface, factory, lifestyle=LifeStyle.SINGLETON, name=name
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def add_transient_factory(
|
|
395
|
+
self,
|
|
396
|
+
interface: Type,
|
|
397
|
+
factory: Callable[..., Any],
|
|
398
|
+
name: Optional[str] = None,
|
|
399
|
+
) -> None:
|
|
400
|
+
self.register_factory(
|
|
401
|
+
interface, factory, lifestyle=LifeStyle.TRANSIENT, name=name
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
def add_scoped_factory(
|
|
405
|
+
self,
|
|
406
|
+
interface: Type,
|
|
407
|
+
factory: Callable[..., Any],
|
|
408
|
+
name: Optional[str] = None,
|
|
409
|
+
) -> None:
|
|
410
|
+
self.register_factory(interface, factory, lifestyle=LifeStyle.SCOPED, name=name)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: injex
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DI container for Python applications
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# DI Container for Python
|
|
15
|
+
|
|
16
|
+
Injex is a lightweight and easy-to-use DI container for Python applications. This library aims to simplify the management of dependencies in your projects, making your code more modular, testable, and maintainable. This library inspired by popular DI frameworks in other programming languages.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
* 🌟 Simple API: Easy to understand and use.
|
|
22
|
+
* 🔄 Support for singleton, transient, and scoped services.
|
|
23
|
+
* Register multiple implementations of the same interface using names.
|
|
24
|
+
* 🔍 Inject dependencies into properties after object creation.
|
|
25
|
+
* 🛠 Handle optional dependencies gracefully.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Why Use Dependency Injection?
|
|
29
|
+
|
|
30
|
+
**Dependency Injection is a design pattern that helps in:**
|
|
31
|
+
|
|
32
|
+
* Modularity: Breaking down your application into interchangeable components.
|
|
33
|
+
* Testability: Facilitating unit testing by allowing dependencies to be mocked or stubbed.
|
|
34
|
+
* Maintainability: Making it easier to update, replace, or refactor components without affecting other parts of the application.
|
|
35
|
+
* Flexibility: Configuring different implementations of the same interface for various scenarios (e.g., testing, production).
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
Here's a simple example of usage Injex:
|
|
40
|
+
```python
|
|
41
|
+
from abc import ABC, abstractmethod
|
|
42
|
+
from injex import Container
|
|
43
|
+
|
|
44
|
+
class IService(ABC):
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def perform_action(self):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
class ServiceImplementation(IService):
|
|
50
|
+
def perform_action(self):
|
|
51
|
+
print("Service is performing an action.")
|
|
52
|
+
|
|
53
|
+
container = Container()
|
|
54
|
+
|
|
55
|
+
container.add_transient(IService, ServiceImplementation)
|
|
56
|
+
|
|
57
|
+
service = container.resolve(IService)
|
|
58
|
+
service.perform_action() # output: Service is performing an action.
|
|
59
|
+
```
|
|
60
|
+
Another examples in [examples folder](./examples).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
injex
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "injex"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "DI container for Python applications"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
[tool.uv]
|
|
10
|
+
dev-dependencies = [
|
|
11
|
+
"mypy>=1.12.1",
|
|
12
|
+
"pytest>=8.3.3",
|
|
13
|
+
"ruff>=0.7.0",
|
|
14
|
+
]
|
injex-0.1.0/setup.cfg
ADDED