cdi
cdi
stands for "cute dependency injector" I guess(?)
install
pip install cdi-di
AI statement
AI was not used nor will be used for any part of the library code, PRs generated by AI, look like AI generated code (low quality) will be rejected, only documentation AI PRs will be accepted
Dependency injection made easy
while some python dependency injectors require some setup and make some things
harder to understand for a simple dependency injection, cdi aims to simplify
dependency injection and be fast (relativly to python)
batteries
- forward reference resolver
- union support
Lazytype for circular depsContextvarsupportTransientfor nonsingleton instancesGenerics/TypeVarsupport- limited instances
- scope inheritance
- no injectable default implementation support
Explicit is better then implicit
the library tries to make you explicit with your typing without compromising readability or ease of use
Examples
import cdi
# this container will contain its own registered types
ctr = cdi.Container()
# register this function as a factory
# for the `int` type
@cdi.Injectable(ctr)
def get_int() -> int:
return 100
# register `Foo` as injectable
# so we can create instances
@cdi.Injectable(ctr)
class Foo:
def __init__(self, number: int) -> None:
self.number = number
# create a scope that will have access to registered
# types in `ctr` then get an instance of `Foo`
scope = cdi.Scope(cdi)
instance = scope.get_instance(Foo)
assert instance.number == 100
import cdi
from typing import Generic, TypeVar
from collection.abc import Sequence
T = TypeVar('T')
ctr = cdi.Container()
class MyBase(Generic[T]):
def __init__(self, field: T) -> None:
self.field = field
@cdi.Injectable(ctr)
class MyType(MyBase[str]):
pass
@cdi.Injectable(ctr)
def name_generator() -> str:
return "foo"
scope = cdi.Scope(__name__, container=ctr)
instance = scope.get_instance(MyType)
assert instance.field == "foo"
ctr = cdi.Container()
class Foo(Generic[T]):
def __init__(self, v: T):
self.v = v
cdi.Injectable(ctr).register("hello world")
cdi.Injectable(ctr).register(100)
scope = Scope(__name__, container=ctr)
assert scope.get_instance(Foo[int]).v == 100
assert scope.get_instance(Foo[str]).v == "hello world"
# even nested
assert scope.get_instance(Foo[Foo[int]]).v.v == 100
scope = cdi.Scope(__name__, container=ctr)
with scope.lifetime() as lifetime:
# bounded to the `lifetime` scope, but for non new
# instances, they will be fetched from `scope`
foo = scope.get_instance(Foo[int])
assert not scope.has_instance(Foo[int])
scope = cdi.Scope(__name__, container=ctr)
@cdi.Injectable(ctr)
class A:
def __init__(self, b: B) -> None:
self.b = b
@cdi.Injectable(ctr)
class B:
def __init__(self, a: cdi.Lazy[A]) -> None:
self.a = a
ctr.update_forward_refs(sys.modules[__name__])
a = scope.get_instance(A)
b = scope.get_instance(B)
assert a.b.a.wake() is a
what is not supported
- list values
- Typevars as injectable return type
Async
although cdi doesn't have an explicit async interface, it does support async code, cdi have
no internal io (does not yield back control) so it doesn't make sense to implement async interface for code
that would not benifit from it in any way
running multiple asyncio executors with cdi is supported because each executor is a separate thread
and cdi has internal RLock on internal objects in case they are shared across threads
Documentation
Container
container contains registered types and their factories, types are registered to a container via cdi.Injectable,
cdi.Injecatble will create the appropriate internal factory in the container
if a type is not registered in the container, cdi.Scope will not attempt to create that type and raise an error instead, unless a EvaluateUnknownTypesPolicy was provided to the scope explicit is bettern then implicit
ctr = cdi.Container()
you can check if a type was registered with the container by calling cdi.Container.has_registered
Forward references
some types may have unresolved forward references in their return type or parameters, when evaluated it is impossible to know what type sits behind those forward ref strings
class Foo:
# what is the `Boo` type? we just see a string
def __init__(self, boo: 'Boo') -> None: ...
such factories will not be usable for injection, to resolve forward refs
the container class provide cid.Container.update_forward_refs which takes the module you want to update the forward refs for, this takes insperation
from Pydantic/v1
the update_forward_refs has to be called after there is a class that can evaluate the forward ref name
import sys
@cdi.Injectable(ctr)
class Foo:
# references `Boo` which is not defined yet
def __init__(self, boo: 'Boo') -> None: ...
@cdi.Injectable(ctr)
class Boo: ...
# now that `Boo` is defined, we can update the factories
# in our current module
ctr.update_forward_refs(sys.modules[__name__])
# works fine
instance = Scope(__name__, container=ctr).get_instance(Foo)
Injectable
Injectable is responsible to take your type and register it into a container, the injector will create an intenal Factory for the provided type and register it into the bounded container
ctr = cdi.Container()
injector = cdi.Injectable(ctr)
injector.register(Foo)
injector.register(my_func)
it can also be used as a decorator
ctr = cdi.Container()
@cdi.Injectable(ctr)
class Foo: ...
@cdi.Injectable(ctr)
def my_func() -> int: ...
when creating the factory, the injector relys on the provided type hints
Classes
when registering a class, the dependencies are taken from the class __init__ signature, and the factory
implementation (what is called to return the type) uses the class __call__
Functions
on functions, the function signature will be used to determin the parameters and return types, calling the factory will call the provided function at the end
Constant
a constant can be injected into the container, the constant type will be the factory return type, and all scopes that require this type will evaluate to the constant, acting as a "global variable" in a container for example
ctr = cdi.Container()
cdi.Injectable().register("hello world")
scope = cdi.Scope(__name__, container=ctr)
assert scope.get_instance(str) == "hello world"
Scopes
scope defines the lifetime or bounderies of an instance, the Scope only contains the instance, and it is bounded to a cdi.Container
the Scope uses the bounded container to get factories and create instances for the types, all instances are singletones, meaning when a type is created once, it will not be created again, and the same instance will be injected
ctr = cdi.Container()
# we inject the `Foo` class into the `ctr` container
@cdi.Injectable(ctr)
class Foo:
def __init__(self, number: int) -> None:
self.number = number
cdi.Injectable(ctr).register(100)
# we define an instance scope that has access to the injectable
# registered in `ctr`
scope = cdi.Scope(__name__, container=ctr)
instance = scope.get_instance(Foo)
instance2 = scope.get_instance(Foo)
# the `Foo` will be evaluated only once and be reused
# for future calls
assert instance is instance2
assert instance.number == 100
scope2 = Scope(__name__ + '2', container=ctr)
scope2_instance = scope.get_instance(Foo)
# a different scopes don't have access to each other instances
# although they are using the same container
assert scope2_instance is not instance
inheritance
scopes can inherit parent and child like inheritance, the parent has no access to the child but the child does have access to the parent
there is no unique behavior for the child/parent scope when they aquire the relevant roles, this is mostly
for ease of use, the real inheritance comes into play via cdi.InjectableMetadata
annotation Metadata
you can change some default behaviors of the injectable type but in a way that make
sense, meaning, if you annotate str you cannot return int
types annotated with a metdata class InjectableMetadata is able to control some default behavior
of the scope
provider_scope
accepts a Callable[[Scope], Scope], this effect which scope will instantiate the annotated type
the returned scope will be used for the type instanciation
ctr = cdi.Container()
ctr2 = cdi.Container()
cdi.Injctable(ctr).register("hello world")
cdi.Injctable(ctr2).register("what?")
scope = Scope(__name__, container=ctr)
scope2 = scope.fork()
@cdi.Injectable(ctr2)
class Foo:
def __init__(
self,
value1: str,
value2: Annotated[
str,
cdi.InjectableMetadata(provider_scope=lambda scope: scope.parent) # get the str from the parent scope
]
) -> None:
self.value1 = value1
self.value2 = value2
instance = scope2.get_instance(Foo)
assert instance.value1 == "what?"
assert instance.value2 == "hello world"
1""" 2.. include:: ../../README.md 3""" 4 5from . import policy 6from ._typing import InjectableMetadata 7from ._container import Container 8from ._exceptions import ( 9 CdiError, 10 IncorrectStackPopping, 11 CircularDependencyError, 12 TypeEvaluationError, 13) 14from ._scope import Scope 15from ._builtins import Lazy, Transient, ContextVar 16from ._decorators import Injectable 17 18 19__all__ = ( 20 "Container", 21 "Scope", 22 "Injectable", 23 "InjectableMetadata", 24 "Lazy", 25 "ContextVar", 26 "policy", 27 "Transient", 28 "CdiError", 29 "TypeEvaluationError", 30 "IncorrectStackPopping", 31 "CircularDependencyError", 32)
76class Container: 77 """ 78 container is a box that can store given factories of types but cannot create 79 instances, container can be thought as a type factory for a `Scope`, if a scope need to create 80 a type, it fetches the registered factory for the required type and uses 81 that factory to create the type 82 83 ### THREAD SAFETY 84 containers are thread safe, they can be used in multiple different places that use 85 threading, for example, multiple scopes that run on different threads 86 """ 87 88 def __init__(self) -> None: 89 self._factories: PrefixTree[type, Factory] = PrefixTree( 90 find_strategy=PrefixTreeTypeFindStrategy() 91 ) 92 self._partially_initialized: list[Factory] = [] 93 self._lock = threading.Lock() 94 self._setup() 95 96 def has_registered(self, type_: Any) -> bool: 97 """ 98 returns a boolean value indicating if the type was registered 99 for the current container 100 """ 101 return self._get_factory(type_) is not None 102 103 def _register(self, factory: Factory) -> None: 104 """ 105 registers the given factory with the scope, the factory return type 106 will be used as key when calling the container `get_factory` and providing a type 107 """ 108 is_partial = _is_partial_factory(factory) 109 with self._lock: 110 if is_partial: 111 self._factories.insert( 112 type_as_prefix_steps(factory.return_type), factory 113 ) 114 else: 115 self._partially_initialized.append(factory) 116 117 def update_forward_refs(self, module: ModuleType) -> None: 118 # TODO: should we call `insepct.currentframe` to have visibility 119 # for locals? 120 with self._lock: 121 self._update_forward_ref(module) 122 123 def _update_forward_ref(self, module: ModuleType) -> None: 124 partially_initialized = [] 125 126 for factory in self._partially_initialized: 127 if factory.module is module: 128 self._factories.insert( 129 type_as_prefix_steps(factory.return_type), 130 _evaluate_partial_factory(factory), 131 ) 132 else: 133 partially_initialized.append(factory) 134 self._partially_initialized = partially_initialized 135 136 def _get_factory( 137 self, type_: type | GenericAlias | TypeAliasType 138 ) -> Factory | None: 139 """ 140 returns the correct factory for the requested type 141 """ 142 prefix = type_as_prefix_steps(type_) 143 with self._lock: 144 return self._factories.find(prefix) 145 146 def _setup(self) -> None: 147 from ._builtins import Lazy 148 149 Injectable(self).register(Lazy)
container is a box that can store given factories of types but cannot create
instances, container can be thought as a type factory for a Scope, if a scope need to create
a type, it fetches the registered factory for the required type and uses
that factory to create the type
THREAD SAFETY
containers are thread safe, they can be used in multiple different places that use
threading, for example, multiple scopes that run on different threads
96 def has_registered(self, type_: Any) -> bool: 97 """ 98 returns a boolean value indicating if the type was registered 99 for the current container 100 """ 101 return self._get_factory(type_) is not None
returns a boolean value indicating if the type was registered for the current container
38class Scope: 39 """ 40 hold the live instances, different scope do not share instances 41 between them unless explicitly annotated via `cdi.InjectableMetadata(provider_scope=...)` 42 43 live instances can be inserted into the scope more about it look at `Scope.insert_instance`, 44 the scope cannot manipulate the bounded container in any way 45 46 scopes can inherit from different scopes, aquiring those roles do not effect the scope 47 behavior, it is more for conviniance when using `cdi.InjectableMetadata(provider_scope=...)` 48 49 the scope name is mostly for easier debugging and clearer errors 50 51 ## evaluation 52 the scope uses the given `Container` to look up for factories that can produce the desired type 53 if the container has no such type the type evaluation will be moved to the provided `no_factory_policy` 54 that is called when a type has no factory and continue the evaluation from there 55 56 by default the policy is `cdi.policy.ErrorNoFactoryPolicy()` 57 58 ## thread safety 59 scope is threadsafe, for every instance an internal lock is aquired preventing 60 2 threads evaluating at the same time 61 62 if the evaluation requires the parent, and no evaluation operation is required on the child scope 63 then the child scope lock will not be acquired 64 """ 65 66 def __init__( 67 self, 68 __name: str, 69 /, 70 *, 71 container: Container, 72 parent: Scope | None = None, 73 no_factory_policy: NoFactoryPolicy | None = None, 74 ) -> None: 75 self._name = __name 76 self._parent = parent 77 self._container = container 78 self._instances: PrefixTree[type, Any] = PrefixTree( 79 find_strategy=PrefixTreeTypeFindStrategy() 80 ) 81 self._no_factory_policy = no_factory_policy or ErrorNoFactoryPolicy() 82 83 self._stack: list[type] = [] 84 self._lock = threading.RLock() 85 self._setup() 86 87 @property 88 def name(self) -> str: 89 return self._name 90 91 @property 92 def container(self) -> Container: 93 return self._container 94 95 @property 96 def parent(self) -> Scope | None: 97 """returns the parent of the scope if any""" 98 return self._parent 99 100 def fork(self, __name: str | None = None, /) -> Scope: 101 """ 102 forks the scope, creating a sub scope that has the same container 103 as the current scope, and set the current scope as the parent 104 """ 105 return Scope( 106 __name or (self.name + "-fork"), 107 container=self._container, 108 parent=self, 109 no_factory_policy=self._no_factory_policy, 110 ) 111 112 def has_instance(self, __type: Any, /) -> bool: 113 """ 114 returns a boolean value indicating if the scope has a live 115 instance of the given type 116 117 it doesn't check if the scope can create such type, for possibility 118 of type creation refer to the scope `cdi.Container` from `scope.container` 119 """ 120 prefix = type_as_prefix_steps(__type) 121 with self._lock: 122 return self._instances.find(prefix) is not None 123 124 def get_instance(self, __type: TypeForm[_T], /) -> _T: 125 """ 126 returns an instance of the given type, assuming some factory 127 is registered at the container level that can create the type 128 """ 129 return self._get_instance_impl(__type, typevars={}) 130 131 @contextlib.contextmanager 132 def lifetime(self, label: str | None = None, /) -> Iterator[Scope]: 133 """ 134 a context manager that returns a scope, for existing instance, the scope will attempt 135 to get the existing instance 136 137 newly created instances will be bounded to the lifetime scope, so when the lifetime scope 138 is dropped, all the instances created in the lifetime will drop as well 139 140 the lifetime scope will not insert newly created instances into the parent scope 141 142 ```py 143 scope = cdi.Scope(ctr) 144 assert not scope.has_instance(str) 145 146 with scope.lifetime() as lifetime: 147 lifetime.get_instance(str) 148 149 # although `lifetime` called `get_instance`, the `scope` has no 150 # such instance because it was created under the lifetime context 151 assert not scope.has_instance(str) 152 ``` 153 """ 154 yield Scope( 155 self.name + "-lifetime" + (("-" + label) if label else ""), 156 parent=self, 157 # we create a new container to trick the scope 158 # to always call the `no_factory_policy` 159 container=Container(), 160 no_factory_policy=_LifetimePolicy(), 161 ) 162 163 def insert_instance(self, __instance: Any, /) -> None: 164 """ 165 inserts the live instance into the scope, mapping between the instance 166 type to the instance itself 167 168 ```py 169 my_obj = object() 170 171 scope = cdi.Scope(...) 172 scope.insert_instance(100) 173 scope.insert_instance(my_obj) 174 175 assert scope.get_instance(int) is 100 176 assert scope.get_instance(object) is my_obj 177 ``` 178 """ 179 prefix = type_as_prefix_steps(type(__instance)) 180 with self._lock: 181 self._instances.insert(prefix, __instance) 182 183 def _get_instance_impl( 184 self, 185 type_: Any, 186 typevars: dict[TypeVar, Any], 187 ) -> Any: 188 """ 189 responsible to take any possible type and unwrap it to the real 190 value to check annotations metadata before evaluation 191 192 type evaluation should not be here, but prepare the type 193 for the next step 194 """ 195 exceptions = [] 196 197 # deque is used to prevent extra stack calls if the `type_` is a 198 # union or annotated, the stack calls can add up for deep injections 199 # with a lot of unions 200 types_: deque[tuple[Any, InjectableMetadata]] = deque() 201 types_.append((type_, InjectableMetadata())) 202 203 while types_: 204 type_, metadata = types_.popleft() 205 origin = get_origin(type_) 206 207 if origin is not None: 208 if typevars: 209 # if we are in a typealias and we have typevars forwarded 210 # we should try to resolve possible generics in the current 211 # `type_` if any, to their real value 212 type_ = _resolve_type_generics(type_, typevars) 213 if is_union(type_): 214 types_.extendleft((t, metadata) for t in _unwrap_union(type_)) 215 continue 216 if origin is Annotated: 217 # look for the relevant metadata and also update the real 218 # value of the annontated type 219 inner, annotated_metadata = _get_annotated_injectable_metadata( 220 type_ 221 ) 222 types_.appendleft( 223 ( 224 inner, 225 # try to merge previous metadata with the annotated metadata 226 # but give more priority to the annotation truthy values 227 annotated_metadata.merge(metadata) 228 if annotated_metadata 229 else metadata, 230 ) 231 ) 232 continue 233 234 scope = self 235 custom_error_message = None 236 237 try: 238 if metadata._error_message: 239 custom_error_message = metadata._error_message 240 if metadata._provider_scope is not None: 241 scope = metadata._provider_scope(self) 242 243 if scope is not self: 244 # if need to call a different scope to evaluate the 245 # type, we don't want the provider scope to call `_provider_scope` 246 # the results can be unexpected 247 metadata_copy = copy.copy(metadata) 248 metadata_copy._provider_scope = None 249 250 # if the type was wrapped with `Annotated` we want to 251 # call `_get_instance` to instantiate the real type 252 return scope._get_instance_impl( 253 Annotated[type_, metadata_copy], # type: ignore 254 typevars=typevars, 255 ) 256 257 dry_instance = self._get_dry_type(type_) 258 if dry_instance is not _miss: 259 return dry_instance 260 elif metadata._transient: 261 return scope._create_instance(type_) 262 else: 263 return scope._get_unwrapped_type(type_) 264 except NoFactoryForTypeError as e: 265 exception = TypeEvaluationError( 266 f"{self} failed to evaluate type `{type_.__name__}` due to error: " 267 + str(e) 268 + "\n" 269 + (custom_error_message or "") 270 ) 271 272 if not types_: 273 raise exception 274 exceptions.append(exception) 275 276 error_message = ( 277 f"{self} failed to evaluate type {type_} due to errors:\n" 278 + "\n - ".join(map(str, exceptions)) 279 ) 280 raise TypeEvaluationError(error_message) 281 282 def _get_unwrapped_type(self, type_: Any) -> Any: 283 with self._lock: 284 tree_prefix = type_as_prefix_steps(type_) 285 if instance := self._instances.find(tree_prefix): 286 return instance 287 288 if type_ in self._stack: 289 raise CircularDependencyError(tuple(self._stack), type_) 290 291 # TODO: I am pretty sure there is a bug with generic aliases 292 # types since each generic alias is a different instance so in `in` operation will be falsy 293 # check in future 294 self._stack.append(type_) 295 296 try: 297 instance = self._create_instance(type_) 298 self._instances.insert(tree_prefix, instance) 299 return instance 300 finally: 301 popped = self._stack.pop() 302 if popped is not type_: 303 raise IncorrectStackPopping( 304 f"{self} incorrect scope stack popping for {self}, expected `{type_.__name__}`, popped `{popped.__name__}`" 305 ) 306 307 def _get_dry_type(self, type_: Any) -> Any: 308 """ 309 the term "dry" here means that the type doesn't require touching 310 the `self` (scope) instances, so no locking is required 311 or getting anything from the scope 312 313 this method can be `staticmethod` for all I care, but it is better 314 keeping it here 315 """ 316 if type_ is NoneType: 317 return None 318 if type_ is Scope: 319 return self 320 321 if is_generic_alias(type_): 322 type_ = cast(GenericAlias, type_) 323 origin = get_origin(type_) 324 325 if origin is type: 326 return get_args(type_)[0] 327 return _miss 328 329 def _create_instance(self, type_: Any) -> Any: 330 if factory := self._container._get_factory(type_): 331 return self._instantiate_from_factory(factory, _get_typevar_mapping(type_)) 332 333 try: 334 return self._no_factory_policy.handle(self, type_) 335 except Exception as e: 336 raise NoFactoryForTypeError(tuple(self._stack), type_) from e 337 338 def _instantiate_from_factory( 339 self, factory: Factory, typevars: dict[TypeVar, Any] 340 ) -> Any: 341 positional_arguments = [] 342 keyword_arguments = {} 343 344 for name, parameter in factory.parameters.items(): 345 annotation = typevars.get(parameter.annotation, parameter.annotation) 346 value = self._get_instance_impl(annotation, typevars=typevars) 347 348 match parameter.kind: 349 case ParameterKind.POSITIONAL: 350 positional_arguments.append(value) 351 case ParameterKind.KEYWORD: 352 keyword_arguments[name] = value 353 return factory(*positional_arguments, **keyword_arguments) 354 355 def _setup(self) -> None: 356 from ._builtins import ContextVar 357 358 self.insert_instance(ContextVar(self.name + "-contextvar")) 359 360 def __str__(self) -> str: 361 return f"Scope<{self.name}>"
hold the live instances, different scope do not share instances
between them unless explicitly annotated via cdi.InjectableMetadata(provider_scope=...)
live instances can be inserted into the scope more about it look at Scope.insert_instance,
the scope cannot manipulate the bounded container in any way
scopes can inherit from different scopes, aquiring those roles do not effect the scope
behavior, it is more for conviniance when using cdi.InjectableMetadata(provider_scope=...)
the scope name is mostly for easier debugging and clearer errors
evaluation
the scope uses the given Container to look up for factories that can produce the desired type
if the container has no such type the type evaluation will be moved to the provided no_factory_policy
that is called when a type has no factory and continue the evaluation from there
by default the policy is cdi.policy.ErrorNoFactoryPolicy()
thread safety
scope is threadsafe, for every instance an internal lock is aquired preventing 2 threads evaluating at the same time
if the evaluation requires the parent, and no evaluation operation is required on the child scope then the child scope lock will not be acquired
66 def __init__( 67 self, 68 __name: str, 69 /, 70 *, 71 container: Container, 72 parent: Scope | None = None, 73 no_factory_policy: NoFactoryPolicy | None = None, 74 ) -> None: 75 self._name = __name 76 self._parent = parent 77 self._container = container 78 self._instances: PrefixTree[type, Any] = PrefixTree( 79 find_strategy=PrefixTreeTypeFindStrategy() 80 ) 81 self._no_factory_policy = no_factory_policy or ErrorNoFactoryPolicy() 82 83 self._stack: list[type] = [] 84 self._lock = threading.RLock() 85 self._setup()
95 @property 96 def parent(self) -> Scope | None: 97 """returns the parent of the scope if any""" 98 return self._parent
returns the parent of the scope if any
100 def fork(self, __name: str | None = None, /) -> Scope: 101 """ 102 forks the scope, creating a sub scope that has the same container 103 as the current scope, and set the current scope as the parent 104 """ 105 return Scope( 106 __name or (self.name + "-fork"), 107 container=self._container, 108 parent=self, 109 no_factory_policy=self._no_factory_policy, 110 )
forks the scope, creating a sub scope that has the same container as the current scope, and set the current scope as the parent
112 def has_instance(self, __type: Any, /) -> bool: 113 """ 114 returns a boolean value indicating if the scope has a live 115 instance of the given type 116 117 it doesn't check if the scope can create such type, for possibility 118 of type creation refer to the scope `cdi.Container` from `scope.container` 119 """ 120 prefix = type_as_prefix_steps(__type) 121 with self._lock: 122 return self._instances.find(prefix) is not None
returns a boolean value indicating if the scope has a live instance of the given type
it doesn't check if the scope can create such type, for possibility
of type creation refer to the scope cdi.Container from scope.container
124 def get_instance(self, __type: TypeForm[_T], /) -> _T: 125 """ 126 returns an instance of the given type, assuming some factory 127 is registered at the container level that can create the type 128 """ 129 return self._get_instance_impl(__type, typevars={})
returns an instance of the given type, assuming some factory is registered at the container level that can create the type
131 @contextlib.contextmanager 132 def lifetime(self, label: str | None = None, /) -> Iterator[Scope]: 133 """ 134 a context manager that returns a scope, for existing instance, the scope will attempt 135 to get the existing instance 136 137 newly created instances will be bounded to the lifetime scope, so when the lifetime scope 138 is dropped, all the instances created in the lifetime will drop as well 139 140 the lifetime scope will not insert newly created instances into the parent scope 141 142 ```py 143 scope = cdi.Scope(ctr) 144 assert not scope.has_instance(str) 145 146 with scope.lifetime() as lifetime: 147 lifetime.get_instance(str) 148 149 # although `lifetime` called `get_instance`, the `scope` has no 150 # such instance because it was created under the lifetime context 151 assert not scope.has_instance(str) 152 ``` 153 """ 154 yield Scope( 155 self.name + "-lifetime" + (("-" + label) if label else ""), 156 parent=self, 157 # we create a new container to trick the scope 158 # to always call the `no_factory_policy` 159 container=Container(), 160 no_factory_policy=_LifetimePolicy(), 161 )
a context manager that returns a scope, for existing instance, the scope will attempt to get the existing instance
newly created instances will be bounded to the lifetime scope, so when the lifetime scope is dropped, all the instances created in the lifetime will drop as well
the lifetime scope will not insert newly created instances into the parent scope
scope = cdi.Scope(ctr)
assert not scope.has_instance(str)
with scope.lifetime() as lifetime:
lifetime.get_instance(str)
# although `lifetime` called `get_instance`, the `scope` has no
# such instance because it was created under the lifetime context
assert not scope.has_instance(str)
163 def insert_instance(self, __instance: Any, /) -> None: 164 """ 165 inserts the live instance into the scope, mapping between the instance 166 type to the instance itself 167 168 ```py 169 my_obj = object() 170 171 scope = cdi.Scope(...) 172 scope.insert_instance(100) 173 scope.insert_instance(my_obj) 174 175 assert scope.get_instance(int) is 100 176 assert scope.get_instance(object) is my_obj 177 ``` 178 """ 179 prefix = type_as_prefix_steps(type(__instance)) 180 with self._lock: 181 self._instances.insert(prefix, __instance)
inserts the live instance into the scope, mapping between the instance type to the instance itself
my_obj = object()
scope = cdi.Scope(...)
scope.insert_instance(100)
scope.insert_instance(my_obj)
assert scope.get_instance(int) is 100
assert scope.get_instance(object) is my_obj
24class Injectable: 25 """ 26 responsible for creating factories from given types to inject 27 into the bounded container 28 29 the class can be used as standalone or as a decorator 30 ```py 31 ctr = cdi.Container() 32 33 @cdi.Injectable(ctr) 34 class Foo: 35 def __init__(self, name: str) -> None: ... 36 ``` 37 38 to read more about how the Injectable behaves for different data types 39 look at `cdi.Injectable.register` 40 """ 41 42 def __init__(self, __ctr: Container, /) -> None: 43 self._ctr = __ctr 44 45 @property 46 def container(self) -> Container: 47 """returns the container the injector is bounded to""" 48 return self._ctr 49 50 def register(self, injectable: T) -> None: 51 """ 52 registers the given type into the provided container, an internal factory 53 will be generated based on the given type 54 55 ### Constant values 56 when registering an instance (value) and not a class or a function, a wrapper factory 57 will be built for that instance, calling the factory will always yield the same instance 58 59 in a way it is like creating a global instances for a container that any scope 60 will have access to 61 62 ```py 63 ctr = cdi.Container() 64 65 class Foo: 66 pass 67 68 instance = Foo() 69 cdi.Injectable(ctr).register(instance) 70 71 scope1 = cdi.Scope(__name__ + "1", container=ctr) 72 scope2 = cdi.Scope(__name__ + "2", container=ctr) 73 74 # both share the same `instance` of `Foo` 75 assert scope1.get_instance(Foo) is scope2.get_instance(Foo) 76 ``` 77 78 ### Classes 79 when injecting a class, the parameters will be fetched from the class `__init__` 80 and on instanciation time, the correct type will be injected based on the type hint, 81 the factory implementation will call the class `__call__` method 82 83 inheritance is supported, including `*args, **kwargs` in your `__init__` class will cause 84 the injector to move up the parent classes with respect to the `MRO` and evaluate the parent 85 parameters too 86 87 ```py 88 ctr = cdi.Container() 89 90 class Parent: 91 def __init__(self, parent_field: int) -> None: ... 92 93 class Child(Parent): 94 def __init__(self, child_field: int, *args, **kwargs) -> None: ... 95 96 # register int so it will be injected to `parent_field` and `child_field` 97 cdi.Injectable(ctr).register(100) 98 99 # the instance is able to be created without error, `parent_field` and `child_field` 100 # are required fields, and they will be injected 101 cdi.Scope(__name__, container=ctr).get_instance(Child) 102 ``` 103 104 FUNCTIONS: 105 parameters and return type are calculated based on the function signature 106 """ 107 if inspect.isfunction(injectable): 108 self._inject_func_factory(injectable) 109 elif inspect.isclass(injectable): 110 self._inject_class_factory(injectable) 111 else: 112 self._inject_constant(injectable) 113 114 def __call__(self, injectable: T) -> T: 115 self.register(injectable) 116 return injectable 117 118 def _inject_class_factory(self, cls: type) -> None: 119 parameters = MroParameters().get_parameters(inspect.getmro(cls), "__init__") 120 self._ctr._register( 121 FactoryBuilder() 122 .with_name(f"{cls.__name__}.__init__") 123 .with_func_impl(getattr(cls, "__call__")) 124 .with_module(cast(ModuleType, inspect.getmodule(cls))) 125 .with_parameters(parameters) 126 .with_return_type(cls) 127 .build() 128 ) 129 130 def _inject_func_factory(self, func: Callable[..., Any]) -> None: 131 parameters = FuncParameters().get_parameters(func) 132 module = cast(ModuleType, inspect.getmodule(func)) 133 rt = inspect.signature(func).return_annotation 134 135 if is_forward_ref(rt): 136 try: 137 rt = ForwardRefResolver( 138 strategy=ForwardRefResolveByModuleStrategy(module) 139 ).resolve(rt) 140 except ResolveForwardRefError: 141 pass 142 143 self._ctr._register( 144 FactoryBuilder() 145 .with_name(func.__name__) # type: ignore 146 .with_module(module) 147 .with_parameters(parameters) 148 .with_func_impl(func) 149 .with_return_type(rt) 150 .build() 151 ) 152 153 def _inject_constant(self, constant: Any) -> None: 154 self._ctr._register( 155 FactoryBuilder() 156 .with_name(str(constant)) 157 .with_func_impl(lambda: constant) 158 .with_return_type(type(constant)) 159 .with_module(cast(ModuleType, inspect.getmodule(constant))) 160 .build() 161 )
responsible for creating factories from given types to inject into the bounded container
the class can be used as standalone or as a decorator
ctr = cdi.Container()
@cdi.Injectable(ctr)
class Foo:
def __init__(self, name: str) -> None: ...
to read more about how the Injectable behaves for different data types
look at cdi.Injectable.register
45 @property 46 def container(self) -> Container: 47 """returns the container the injector is bounded to""" 48 return self._ctr
returns the container the injector is bounded to
50 def register(self, injectable: T) -> None: 51 """ 52 registers the given type into the provided container, an internal factory 53 will be generated based on the given type 54 55 ### Constant values 56 when registering an instance (value) and not a class or a function, a wrapper factory 57 will be built for that instance, calling the factory will always yield the same instance 58 59 in a way it is like creating a global instances for a container that any scope 60 will have access to 61 62 ```py 63 ctr = cdi.Container() 64 65 class Foo: 66 pass 67 68 instance = Foo() 69 cdi.Injectable(ctr).register(instance) 70 71 scope1 = cdi.Scope(__name__ + "1", container=ctr) 72 scope2 = cdi.Scope(__name__ + "2", container=ctr) 73 74 # both share the same `instance` of `Foo` 75 assert scope1.get_instance(Foo) is scope2.get_instance(Foo) 76 ``` 77 78 ### Classes 79 when injecting a class, the parameters will be fetched from the class `__init__` 80 and on instanciation time, the correct type will be injected based on the type hint, 81 the factory implementation will call the class `__call__` method 82 83 inheritance is supported, including `*args, **kwargs` in your `__init__` class will cause 84 the injector to move up the parent classes with respect to the `MRO` and evaluate the parent 85 parameters too 86 87 ```py 88 ctr = cdi.Container() 89 90 class Parent: 91 def __init__(self, parent_field: int) -> None: ... 92 93 class Child(Parent): 94 def __init__(self, child_field: int, *args, **kwargs) -> None: ... 95 96 # register int so it will be injected to `parent_field` and `child_field` 97 cdi.Injectable(ctr).register(100) 98 99 # the instance is able to be created without error, `parent_field` and `child_field` 100 # are required fields, and they will be injected 101 cdi.Scope(__name__, container=ctr).get_instance(Child) 102 ``` 103 104 FUNCTIONS: 105 parameters and return type are calculated based on the function signature 106 """ 107 if inspect.isfunction(injectable): 108 self._inject_func_factory(injectable) 109 elif inspect.isclass(injectable): 110 self._inject_class_factory(injectable) 111 else: 112 self._inject_constant(injectable)
registers the given type into the provided container, an internal factory will be generated based on the given type
Constant values
when registering an instance (value) and not a class or a function, a wrapper factory will be built for that instance, calling the factory will always yield the same instance
in a way it is like creating a global instances for a container that any scope will have access to
ctr = cdi.Container()
class Foo:
pass
instance = Foo()
cdi.Injectable(ctr).register(instance)
scope1 = cdi.Scope(__name__ + "1", container=ctr)
scope2 = cdi.Scope(__name__ + "2", container=ctr)
# both share the same `instance` of `Foo`
assert scope1.get_instance(Foo) is scope2.get_instance(Foo)
Classes
when injecting a class, the parameters will be fetched from the class __init__
and on instanciation time, the correct type will be injected based on the type hint,
the factory implementation will call the class __call__ method
inheritance is supported, including *args, **kwargs in your __init__ class will cause
the injector to move up the parent classes with respect to the MRO and evaluate the parent
parameters too
ctr = cdi.Container()
class Parent:
def __init__(self, parent_field: int) -> None: ...
class Child(Parent):
def __init__(self, child_field: int, *args, **kwargs) -> None: ...
# register int so it will be injected to `parent_field` and `child_field`
cdi.Injectable(ctr).register(100)
# the instance is able to be created without error, `parent_field` and `child_field`
# are required fields, and they will be injected
cdi.Scope(__name__, container=ctr).get_instance(Child)
FUNCTIONS: parameters and return type are calculated based on the function signature
99class InjectableMetadata: 100 def __init__( 101 self, 102 *, 103 transient: bool = False, 104 provider_scope: Callable[[Scope], Scope] | None = None, 105 error_message: str | None = None, 106 ) -> None: 107 self._transient = transient 108 self._provider_scope = provider_scope 109 self._error_message = error_message 110 111 def merge(self, other: InjectableMetadata) -> InjectableMetadata: 112 """ 113 merges data from `other` into `self`, wherever `self` has a falsy value, the value will 114 be taken from the given `other` 115 """ 116 return InjectableMetadata( 117 transient=self._transient or other._transient, 118 provider_scope=self._provider_scope or other._provider_scope, 119 error_message=self._error_message or other._error_message, 120 ) 121 122 def __repr__(self) -> str: 123 return ( 124 "InjectableMetadata(" 125 f"transient={self._transient}, " 126 f"provider_scope={self._provider_scope}, " 127 f"error_message={self._error_message}, " 128 ")" 129 )
111 def merge(self, other: InjectableMetadata) -> InjectableMetadata: 112 """ 113 merges data from `other` into `self`, wherever `self` has a falsy value, the value will 114 be taken from the given `other` 115 """ 116 return InjectableMetadata( 117 transient=self._transient or other._transient, 118 provider_scope=self._provider_scope or other._provider_scope, 119 error_message=self._error_message or other._error_message, 120 )
merges data from other into self, wherever self has a falsy value, the value will
be taken from the given other
9class Lazy(Generic[_T]): 10 """ 11 the lazy type is a builtin generic type that can be used in 12 cases of circular dependency 13 14 ```py 15 @cdi.Injectable(ctr) 16 class A: 17 def __init__(self, b: B) -> None: ... 18 19 @cdi.Injectable(ctr) 20 class B: 21 def __init__(self, a: A) -> None: ... 22 23 ctr.update_forward_refs(sys.modules[__name__]) 24 25 scope = cdi.Scope(ctr) 26 scope.get_instance(A) # error due to circular dep 27 ``` 28 29 lazy solves the issue by making one of the dependencies a lazy 30 object 31 ```py 32 @cdi.Injectable(ctr) 33 class A: 34 def __init__(self, b: B) -> None: ... 35 36 @cdi.Injectable(ctr) 37 class B: 38 def __init__(self, a: cdi.Lazy[A]) -> None: ... 39 40 ctr.update_forward_refs(sys.modules[__name__]) 41 42 scope = cdi.Scope(ctr) 43 instance = scope.get_instance(A) # ok 44 assert instance.b.a.wake() is instance 45 ``` 46 """ 47 48 def __init__( 49 self, 50 type_: type[_T], 51 scope: Annotated[ 52 cdi.Scope, 53 cdi.InjectableMetadata( 54 error_message="to use `Lazy` it is required to insert the `Scope` instance" 55 ), 56 ], 57 ) -> None: 58 """@private""" 59 self._type = type_ 60 self._scope = scope 61 self._instance: _T | None = None 62 63 def wake(self) -> _T: 64 """wakes up the lazy type for instance evaluation""" 65 if self._instance is None: 66 self._instance = self._scope.get_instance(self._type) 67 return self._instance
the lazy type is a builtin generic type that can be used in cases of circular dependency
@cdi.Injectable(ctr)
class A:
def __init__(self, b: B) -> None: ...
@cdi.Injectable(ctr)
class B:
def __init__(self, a: A) -> None: ...
ctr.update_forward_refs(sys.modules[__name__])
scope = cdi.Scope(ctr)
scope.get_instance(A) # error due to circular dep
lazy solves the issue by making one of the dependencies a lazy object
@cdi.Injectable(ctr)
class A:
def __init__(self, b: B) -> None: ...
@cdi.Injectable(ctr)
class B:
def __init__(self, a: cdi.Lazy[A]) -> None: ...
ctr.update_forward_refs(sys.modules[__name__])
scope = cdi.Scope(ctr)
instance = scope.get_instance(A) # ok
assert instance.b.a.wake() is instance
12class ContextVar: 13 """ 14 injectable representation of python builtin `contextvars`, providing thread local arguments 15 or task local arguments (in async) 16 17 ### memory consumption 18 contextvars has a limition that a `ContextVar` is holding a hard reference to its inner value 19 and when a thread is dropped, the contextvar is not dropped from the global `Context` causing the inner 20 value to never be dropped 21 22 and if the inner value is a list or a dict that holds references, the dict/list items will not be dropped, because 23 of that all values can be set only via `limited` contextmanager, so it will ensure a cleanup inside 24 the contextvar inner dict 25 """ 26 27 def __init__( 28 self, 29 name: str, 30 ) -> None: 31 """@private""" 32 self._name = name 33 self._ctx: contextvars.ContextVar[dict[Hashable, Any]] = contextvars.ContextVar( 34 name 35 ) 36 37 @property 38 def name(self) -> str: 39 return self._name 40 41 def get(self, key: Hashable, default: _T | None = None) -> Any | _T | None: 42 """ 43 tries to return the value for the given key in the thread local contextvar 44 if not found returns `default` 45 """ 46 if ctx := self._ctx.get(None): 47 return ctx.get(key, default) 48 return default 49 50 def _insert(self, key: Hashable, value: Any) -> None: 51 current = self._get_value() 52 current[key] = value 53 54 def _delete(self, key: Hashable) -> None: 55 current = self._get_value() 56 current.pop(key, None) 57 58 def _get_value(self) -> dict[Hashable, Any]: 59 if (value := self._ctx.get(None)) is None: 60 value = {} 61 self._ctx.set(value) 62 return value 63 64 @contextlib.contextmanager 65 def limited(self, **kwargs) -> Iterator[None]: 66 """ 67 binds the given `kwargs` to the thread local contextvar, when leaving the contextmanager 68 the `kwargs` are cleaned 69 """ 70 71 try: 72 for key, value in kwargs.items(): 73 self._insert(key, value) 74 yield 75 finally: 76 for key in kwargs: 77 self._delete(key)
injectable representation of python builtin contextvars, providing thread local arguments
or task local arguments (in async)
memory consumption
contextvars has a limition that a ContextVar is holding a hard reference to its inner value
and when a thread is dropped, the contextvar is not dropped from the global Context causing the inner
value to never be dropped
and if the inner value is a list or a dict that holds references, the dict/list items will not be dropped, because
of that all values can be set only via limited contextmanager, so it will ensure a cleanup inside
the contextvar inner dict
41 def get(self, key: Hashable, default: _T | None = None) -> Any | _T | None: 42 """ 43 tries to return the value for the given key in the thread local contextvar 44 if not found returns `default` 45 """ 46 if ctx := self._ctx.get(None): 47 return ctx.get(key, default) 48 return default
tries to return the value for the given key in the thread local contextvar
if not found returns default
64 @contextlib.contextmanager 65 def limited(self, **kwargs) -> Iterator[None]: 66 """ 67 binds the given `kwargs` to the thread local contextvar, when leaving the contextmanager 68 the `kwargs` are cleaned 69 """ 70 71 try: 72 for key, value in kwargs.items(): 73 self._insert(key, value) 74 yield 75 finally: 76 for key in kwargs: 77 self._delete(key)
binds the given kwargs to the thread local contextvar, when leaving the contextmanager
the kwargs are cleaned
47class TypeEvaluationError(CdiError): 48 """raise when the given type could not be evaluated, couldn't create an instance"""
raise when the given type could not be evaluated, couldn't create an instance
33class IncorrectStackPopping(CdiError): 34 """ 35 should not be experianced by end user, if you do get this 36 errro raised, please open and issue 37 """
should not be experianced by end user, if you do get this errro raised, please open and issue
62class CircularDependencyError(CdiError): 63 """ 64 raise when injectable have circular dependancy, A requires B while B requires 65 A, this case is impossible and explicit handling is required, the evaluation 66 stacktrace is also printed 67 """ 68 69 def __init__(self, stack: tuple[type, ...], type_: Any) -> None: 70 """@private""" 71 message = ( 72 f"Circular dependency detected when trying to resolve `{stack[0].__name__}` by type `{type_.__name__}`, traceback:\n" 73 + _stack_traceback_message(itertools.chain(stack, (type_,))) 74 ) 75 super().__init__(message)
raise when injectable have circular dependancy, A requires B while B requires A, this case is impossible and explicit handling is required, the evaluation stacktrace is also printed