cdi-di 0.0.1b1__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.
@@ -0,0 +1,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Ignore uv lock
13
+ uv.lock
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,271 @@
1
+ Metadata-Version: 2.5
2
+ Name: cdi-di
3
+ Version: 0.0.1b1
4
+ Summary: Python dependency injection made easy
5
+ Author-email: daniel sonbolian <dsal3389@gmail.com>
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+
9
+ # cdi
10
+ stands for "cute dependency injector" I guess?
11
+
12
+
13
+ ## Dependency injection made easy
14
+ while some python dependency injectors require some setup and make some things
15
+ harder to understand for a simple dependency injection, `cdi` aims to simplify
16
+ dependency injection and be fast (relativly to python)
17
+
18
+ ```py
19
+ import cdi
20
+
21
+ # this container will contain its own registered types
22
+ ctr = cdi.Container()
23
+
24
+
25
+ # register this function as a factory
26
+ # for the `int` type
27
+ @cdi.Injectable(ctr=ctr)
28
+ def get_int() -> int:
29
+ return 100
30
+
31
+
32
+ # register `Foo` as injectable
33
+ # so we can create instances
34
+ @cdi.Injectable(ctr=ctr)
35
+ class Foo:
36
+ def __init__(self, number: int) -> None:
37
+ self.number = number
38
+
39
+
40
+ # create a scope that will have access to registered
41
+ # types in `ctr` then get an instance of `Foo`
42
+ scope = cdi.Scope(cdi)
43
+ instance = scope.get_instance(Foo)
44
+ assert instance.number == 100
45
+ ```
46
+
47
+ ## Support Generics
48
+
49
+ ```py
50
+ import cdi
51
+ from typing import Generic, TypeVar
52
+ from collection.abc import Sequence
53
+
54
+
55
+ T = TypeVar('T')
56
+
57
+ ctr = cdi.Container()
58
+
59
+
60
+ class MyBase(Generic[T]):
61
+ def __init__(self, field: T) -> None:
62
+ self.field = field
63
+
64
+
65
+ @cdi.Injectable(ctr)
66
+ class MyType(MyBase[str]):
67
+ pass
68
+
69
+
70
+ @cdi.Injectable(ctr)
71
+ def name_generator() -> str:
72
+ return "foo"
73
+
74
+
75
+ scope = cdi.Scope(ctr)
76
+ instance = scope.get_instance(MyType)
77
+ assert instance.field == "foo"
78
+ ```
79
+
80
+ ### what is not supported with generics (at least yet)
81
+ * TypeAliases as parameters are not supported (i.e `list[int]`)
82
+ * TypeAliases as injectable return type
83
+ * TypeVars as parameters
84
+ * Typevars as injectable return type
85
+
86
+
87
+ ### Explicit is better then implicit
88
+ the library tries to make you explicit with your typing without compromising
89
+ readability or ease of use
90
+
91
+ # Documentation
92
+
93
+ ## Container
94
+ container defines the scope of available types for injections, if you have
95
+ a `Scope` that want `int`, it will try to get the `int` factory from his container
96
+
97
+ thus you can have multiple `Containers` containing different types and provide separation
98
+ but most of the time you will be using a single global container
99
+
100
+ ```py
101
+ ctr = cdi.Container()
102
+ ```
103
+
104
+ ### Inject factories
105
+ Containers contain is almost like a register of `Factories`, a factory can
106
+ be injected only through the `Injectable` class
107
+
108
+ ## Forward references
109
+ some factories may have unresolved forward references in their return type or parameters, when evaluated
110
+ it is impossible to know what type sits behind those forward ref strings
111
+
112
+ ```py
113
+ class Foo:
114
+ # what is the `Boo` type? we just see a string
115
+ def __init__(self, boo: 'Boo') -> None: ...
116
+ ```
117
+
118
+ such factories will not be usable for injection, to resolve forward refs
119
+ the container class provide `update_forward_ref` which takes the module you want to update the forward refs for, this takes insperation
120
+ from `Pydantic/v1`
121
+
122
+ the `update_forward_ref` has to be called after there is a class that can evaluate the forward ref
123
+
124
+ ```py
125
+ import sys
126
+
127
+ @cdi.Injectable(ctr=ctr)
128
+ class Foo:
129
+ # references `Boo` which is not defined yet
130
+ def __init__(self, boo: 'Boo') -> None: ...
131
+
132
+
133
+ @cdi.Injectable(ctr=ctr)
134
+ class Boo: ...
135
+
136
+
137
+ # now that `Boo` is defined, we can update the factories
138
+ # in our current module
139
+ ctr.update_forward_ref(sys.modules[__name__])
140
+
141
+ # works fine
142
+ instance = Scope(__name__, ctr=ctr).get_instance(Foo)
143
+ ```
144
+
145
+ ## Injectable
146
+ injectable is a type that creates factories based on the given type and registers
147
+ them with the given container
148
+
149
+ ```py
150
+ ctr = cdi.Container()
151
+ injector = cdi.Injectable(ctr=ctr)
152
+
153
+ injector.register(Foo)
154
+ injector.register(my_func)
155
+ ```
156
+
157
+ it can also be used as a decorator
158
+
159
+ ```py
160
+ ctr = cdi.Container()
161
+
162
+ @cdi.Injectable(ctr=ctr)
163
+ class Foo: ...
164
+
165
+ @cdi.Injectable(ctr=ctr)
166
+ def my_func() -> int: ...
167
+ ```
168
+
169
+ when creating the factory, the injector relys on the provided type hints
170
+
171
+ ### Classes
172
+ when registering a class, the dependencies are taken from the class `__init__` signature, and the factory
173
+ implementation (what is called to return the type) uses the class `__call__`
174
+
175
+ ### Functions
176
+ on functions, the function signature will be used to determin the parameters and return types, calling the factory
177
+ will call the provided function at the end
178
+
179
+ ### Constant
180
+ a constant can be injected into the container, the constant type will be the factory return type, and all scopes
181
+ that require this type will evaluate to the constant, acting as a "global variable" in a container
182
+ for example
183
+ ```py
184
+ ctr = cdi.Container()
185
+ cdi.Injectable().register("hello world")
186
+
187
+ scope = cdi.Scope(__name__, ctr=ctr)
188
+ assert scope.get_instance(str) == "hello world"
189
+ ```
190
+
191
+ ## Scopes
192
+ scopes provide sepration of live instances, they use the containers to get the factory, and they call
193
+ the factory to create a live instance that will be injected
194
+
195
+ ```py
196
+ ctr = cdi.Container()
197
+
198
+ # we inject the `Foo` class into the `ctr` container
199
+ @cdi.Injectable(ctr=ctr)
200
+ class Foo:
201
+ def __init__(self, number: int) -> None:
202
+ self.number = number
203
+
204
+ cdi.Injectable(ctr=ctr).register(100)
205
+
206
+ # we define an instance scope that has access to the injectable
207
+ # registered in `ctr`
208
+ scope = cdi.Scope(__name__, ctr)
209
+ instance = scope.get_instance(Foo)
210
+ instance2 = scope.get_instance(Foo)
211
+
212
+ # the `Foo` will be evaluated only once and be reused
213
+ # for future calls
214
+ assert instance is instance2
215
+ assert instance.number == 100
216
+
217
+ scope2 = Scope(__name__ + '2', ctr)
218
+ scope2_instance = scope.get_instance(Foo)
219
+
220
+ # a different scopes don't have access to each other instances
221
+ # although they are using the same container
222
+ assert scope2_instance is not instance
223
+ ```
224
+
225
+ ### inheritance
226
+ scopes can inherit parent and child like inheritance, the parent has no access to the child
227
+ but the child does have access to the parent
228
+
229
+ there is no unique behavior for the child/parent scope when they aquire the relevant roles, this is mostly
230
+ for ease of use, the real inheritance comes into play via `InjectableMetadata`
231
+
232
+ ## annotation Metadata
233
+ you can change some default behaviors of the injectable type but in a way that make
234
+ sense, meaning, if you annotate `str` you cannot return `int`
235
+
236
+ types annotated with a metdata class `InjectableMetadata` is able to control some default behavior
237
+ of the scope
238
+
239
+ ### provider_scope
240
+ accepts a `Callable[[Scope], Scope]`, this effect which scope will instantiate the annotated type
241
+ the returned scope will be used for the type instanciation
242
+
243
+ ```py
244
+ ctr = cdi.Container()
245
+ ctr2 = cdi.Container()
246
+
247
+ cdi.Injctable(ctr=ctr).register("hello world")
248
+ cdi.Injctable(ctr=ctr2).register("what?")
249
+
250
+ scope = Scope(__name__, ctr=ctr)
251
+ scope2 = scope.fork()
252
+
253
+
254
+ @cdi.Injectable(ctr=ctr2)
255
+ class Foo:
256
+ def __init__(
257
+ self,
258
+ value1: str,
259
+ value2: Annotated[
260
+ str,
261
+ cdi.InjectableMetadata(provider_scope=lambda scope: scope.parent) # get the str from the parent scope
262
+ ]
263
+ ) -> None:
264
+ self.value1 = value1
265
+ self.value2 = value2
266
+
267
+
268
+ instance = scope2.get_instance(Foo)
269
+ assert instance.value1 == "what?"
270
+ assert instance.value2 == "hello world"
271
+ ```
@@ -0,0 +1,263 @@
1
+ # cdi
2
+ stands for "cute dependency injector" I guess?
3
+
4
+
5
+ ## Dependency injection made easy
6
+ while some python dependency injectors require some setup and make some things
7
+ harder to understand for a simple dependency injection, `cdi` aims to simplify
8
+ dependency injection and be fast (relativly to python)
9
+
10
+ ```py
11
+ import cdi
12
+
13
+ # this container will contain its own registered types
14
+ ctr = cdi.Container()
15
+
16
+
17
+ # register this function as a factory
18
+ # for the `int` type
19
+ @cdi.Injectable(ctr=ctr)
20
+ def get_int() -> int:
21
+ return 100
22
+
23
+
24
+ # register `Foo` as injectable
25
+ # so we can create instances
26
+ @cdi.Injectable(ctr=ctr)
27
+ class Foo:
28
+ def __init__(self, number: int) -> None:
29
+ self.number = number
30
+
31
+
32
+ # create a scope that will have access to registered
33
+ # types in `ctr` then get an instance of `Foo`
34
+ scope = cdi.Scope(cdi)
35
+ instance = scope.get_instance(Foo)
36
+ assert instance.number == 100
37
+ ```
38
+
39
+ ## Support Generics
40
+
41
+ ```py
42
+ import cdi
43
+ from typing import Generic, TypeVar
44
+ from collection.abc import Sequence
45
+
46
+
47
+ T = TypeVar('T')
48
+
49
+ ctr = cdi.Container()
50
+
51
+
52
+ class MyBase(Generic[T]):
53
+ def __init__(self, field: T) -> None:
54
+ self.field = field
55
+
56
+
57
+ @cdi.Injectable(ctr)
58
+ class MyType(MyBase[str]):
59
+ pass
60
+
61
+
62
+ @cdi.Injectable(ctr)
63
+ def name_generator() -> str:
64
+ return "foo"
65
+
66
+
67
+ scope = cdi.Scope(ctr)
68
+ instance = scope.get_instance(MyType)
69
+ assert instance.field == "foo"
70
+ ```
71
+
72
+ ### what is not supported with generics (at least yet)
73
+ * TypeAliases as parameters are not supported (i.e `list[int]`)
74
+ * TypeAliases as injectable return type
75
+ * TypeVars as parameters
76
+ * Typevars as injectable return type
77
+
78
+
79
+ ### Explicit is better then implicit
80
+ the library tries to make you explicit with your typing without compromising
81
+ readability or ease of use
82
+
83
+ # Documentation
84
+
85
+ ## Container
86
+ container defines the scope of available types for injections, if you have
87
+ a `Scope` that want `int`, it will try to get the `int` factory from his container
88
+
89
+ thus you can have multiple `Containers` containing different types and provide separation
90
+ but most of the time you will be using a single global container
91
+
92
+ ```py
93
+ ctr = cdi.Container()
94
+ ```
95
+
96
+ ### Inject factories
97
+ Containers contain is almost like a register of `Factories`, a factory can
98
+ be injected only through the `Injectable` class
99
+
100
+ ## Forward references
101
+ some factories may have unresolved forward references in their return type or parameters, when evaluated
102
+ it is impossible to know what type sits behind those forward ref strings
103
+
104
+ ```py
105
+ class Foo:
106
+ # what is the `Boo` type? we just see a string
107
+ def __init__(self, boo: 'Boo') -> None: ...
108
+ ```
109
+
110
+ such factories will not be usable for injection, to resolve forward refs
111
+ the container class provide `update_forward_ref` which takes the module you want to update the forward refs for, this takes insperation
112
+ from `Pydantic/v1`
113
+
114
+ the `update_forward_ref` has to be called after there is a class that can evaluate the forward ref
115
+
116
+ ```py
117
+ import sys
118
+
119
+ @cdi.Injectable(ctr=ctr)
120
+ class Foo:
121
+ # references `Boo` which is not defined yet
122
+ def __init__(self, boo: 'Boo') -> None: ...
123
+
124
+
125
+ @cdi.Injectable(ctr=ctr)
126
+ class Boo: ...
127
+
128
+
129
+ # now that `Boo` is defined, we can update the factories
130
+ # in our current module
131
+ ctr.update_forward_ref(sys.modules[__name__])
132
+
133
+ # works fine
134
+ instance = Scope(__name__, ctr=ctr).get_instance(Foo)
135
+ ```
136
+
137
+ ## Injectable
138
+ injectable is a type that creates factories based on the given type and registers
139
+ them with the given container
140
+
141
+ ```py
142
+ ctr = cdi.Container()
143
+ injector = cdi.Injectable(ctr=ctr)
144
+
145
+ injector.register(Foo)
146
+ injector.register(my_func)
147
+ ```
148
+
149
+ it can also be used as a decorator
150
+
151
+ ```py
152
+ ctr = cdi.Container()
153
+
154
+ @cdi.Injectable(ctr=ctr)
155
+ class Foo: ...
156
+
157
+ @cdi.Injectable(ctr=ctr)
158
+ def my_func() -> int: ...
159
+ ```
160
+
161
+ when creating the factory, the injector relys on the provided type hints
162
+
163
+ ### Classes
164
+ when registering a class, the dependencies are taken from the class `__init__` signature, and the factory
165
+ implementation (what is called to return the type) uses the class `__call__`
166
+
167
+ ### Functions
168
+ on functions, the function signature will be used to determin the parameters and return types, calling the factory
169
+ will call the provided function at the end
170
+
171
+ ### Constant
172
+ a constant can be injected into the container, the constant type will be the factory return type, and all scopes
173
+ that require this type will evaluate to the constant, acting as a "global variable" in a container
174
+ for example
175
+ ```py
176
+ ctr = cdi.Container()
177
+ cdi.Injectable().register("hello world")
178
+
179
+ scope = cdi.Scope(__name__, ctr=ctr)
180
+ assert scope.get_instance(str) == "hello world"
181
+ ```
182
+
183
+ ## Scopes
184
+ scopes provide sepration of live instances, they use the containers to get the factory, and they call
185
+ the factory to create a live instance that will be injected
186
+
187
+ ```py
188
+ ctr = cdi.Container()
189
+
190
+ # we inject the `Foo` class into the `ctr` container
191
+ @cdi.Injectable(ctr=ctr)
192
+ class Foo:
193
+ def __init__(self, number: int) -> None:
194
+ self.number = number
195
+
196
+ cdi.Injectable(ctr=ctr).register(100)
197
+
198
+ # we define an instance scope that has access to the injectable
199
+ # registered in `ctr`
200
+ scope = cdi.Scope(__name__, ctr)
201
+ instance = scope.get_instance(Foo)
202
+ instance2 = scope.get_instance(Foo)
203
+
204
+ # the `Foo` will be evaluated only once and be reused
205
+ # for future calls
206
+ assert instance is instance2
207
+ assert instance.number == 100
208
+
209
+ scope2 = Scope(__name__ + '2', ctr)
210
+ scope2_instance = scope.get_instance(Foo)
211
+
212
+ # a different scopes don't have access to each other instances
213
+ # although they are using the same container
214
+ assert scope2_instance is not instance
215
+ ```
216
+
217
+ ### inheritance
218
+ scopes can inherit parent and child like inheritance, the parent has no access to the child
219
+ but the child does have access to the parent
220
+
221
+ there is no unique behavior for the child/parent scope when they aquire the relevant roles, this is mostly
222
+ for ease of use, the real inheritance comes into play via `InjectableMetadata`
223
+
224
+ ## annotation Metadata
225
+ you can change some default behaviors of the injectable type but in a way that make
226
+ sense, meaning, if you annotate `str` you cannot return `int`
227
+
228
+ types annotated with a metdata class `InjectableMetadata` is able to control some default behavior
229
+ of the scope
230
+
231
+ ### provider_scope
232
+ accepts a `Callable[[Scope], Scope]`, this effect which scope will instantiate the annotated type
233
+ the returned scope will be used for the type instanciation
234
+
235
+ ```py
236
+ ctr = cdi.Container()
237
+ ctr2 = cdi.Container()
238
+
239
+ cdi.Injctable(ctr=ctr).register("hello world")
240
+ cdi.Injctable(ctr=ctr2).register("what?")
241
+
242
+ scope = Scope(__name__, ctr=ctr)
243
+ scope2 = scope.fork()
244
+
245
+
246
+ @cdi.Injectable(ctr=ctr2)
247
+ class Foo:
248
+ def __init__(
249
+ self,
250
+ value1: str,
251
+ value2: Annotated[
252
+ str,
253
+ cdi.InjectableMetadata(provider_scope=lambda scope: scope.parent) # get the str from the parent scope
254
+ ]
255
+ ) -> None:
256
+ self.value1 = value1
257
+ self.value2 = value2
258
+
259
+
260
+ instance = scope2.get_instance(Foo)
261
+ assert instance.value1 == "what?"
262
+ assert instance.value2 == "hello world"
263
+ ```
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "cdi-di"
3
+ version = "0.0.1-b1"
4
+ description = "Python dependency injection made easy"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "daniel sonbolian", email = "dsal3389@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = []
11
+
12
+ [tool.hatch.build.targets.wheel]
13
+ packages = ["src/cdi"]
14
+
15
+ [build-system]
16
+ requires = ["hatchling"]
17
+ build-backend = "hatchling.build"
18
+
19
+ [dependency-groups]
20
+ dev = [
21
+ "pytest>=8.4.2",
22
+ ]
@@ -0,0 +1,22 @@
1
+ from ._container import Container
2
+ from ._exceptions import (
3
+ CdiError,
4
+ IncorrectStackPopping,
5
+ CircularDependencyError,
6
+ TypeEvaluationError,
7
+ )
8
+ from ._scope import Scope
9
+ from ._decorators import Injectable
10
+ from ._typing import InjectableMetadata
11
+
12
+
13
+ __all__ = (
14
+ "Scope",
15
+ "Container",
16
+ "Injectable",
17
+ "InjectableMetadata",
18
+ "CdiError",
19
+ "TypeEvaluationError",
20
+ "IncorrectStackPopping",
21
+ "CircularDependencyError",
22
+ )