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 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
+ ![Build Status](https://github.com/vshulcz/di/actions/workflows/ci.yml/badge.svg)
10
+ ![Python Versions](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13-blue)
11
+ ![License](https://img.shields.io/github/license/vshulcz/di.svg)
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
+ ![Build Status](https://github.com/vshulcz/di/actions/workflows/ci.yml/badge.svg)
2
+ ![Python Versions](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13-blue)
3
+ ![License](https://img.shields.io/github/license/vshulcz/di.svg)
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
+ ![Build Status](https://github.com/vshulcz/di/actions/workflows/ci.yml/badge.svg)
10
+ ![Python Versions](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13-blue)
11
+ ![License](https://img.shields.io/github/license/vshulcz/di.svg)
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,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ injex/__init__.py
5
+ injex.egg-info/PKG-INFO
6
+ injex.egg-info/SOURCES.txt
7
+ injex.egg-info/dependency_links.txt
8
+ injex.egg-info/top_level.txt
9
+ tests/test_container.py
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+