This release is a pre-release and may not be stable for production use.
cdi
stands for "cute dependency injector" I guess(?)
install
pip install cdi-di
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)
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=ctr)
def get_int() -> int:
return 100
# register `Foo` as injectable
# so we can create instances
@cdi.Injectable(ctr=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
Support Generics
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(ctr)
instance = scope.get_instance(MyType)
assert instance.field == "foo"
what is not supported with generics (at least yet)
- TypeAliases as parameters are not supported (i.e
list[int]) - TypeAliases as injectable return type
- TypeVars as parameters
- Typevars as injectable return type
Explicit is better then implicit
the library tries to make you explicit with your typing without compromising readability or ease of use
Documentation
Container
container defines the scope of available types for injections, if you have
a Scope that want int, it will try to get the int factory from his container
thus you can have multiple Containers containing different types and provide separation
but most of the time you will be using a single global container
ctr = cdi.Container()
Inject factories
Containers contain is almost like a register of Factories, a factory can
be injected only through the Injectable class
Forward references
some factories 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 update_forward_ref which takes the module you want to update the forward refs for, this takes insperation
from Pydantic/v1
the update_forward_ref has to be called after there is a class that can evaluate the forward ref
import sys
@cdi.Injectable(ctr=ctr)
class Foo:
# references `Boo` which is not defined yet
def __init__(self, boo: 'Boo') -> None: ...
@cdi.Injectable(ctr=ctr)
class Boo: ...
# now that `Boo` is defined, we can update the factories
# in our current module
ctr.update_forward_ref(sys.modules[__name__])
# works fine
instance = Scope(__name__, ctr=ctr).get_instance(Foo)
Injectable
injectable is a type that creates factories based on the given type and registers them with the given container
ctr = cdi.Container()
injector = cdi.Injectable(ctr=ctr)
injector.register(Foo)
injector.register(my_func)
it can also be used as a decorator
ctr = cdi.Container()
@cdi.Injectable(ctr=ctr)
class Foo: ...
@cdi.Injectable(ctr=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__, ctr=ctr)
assert scope.get_instance(str) == "hello world"
Scopes
scopes provide sepration of live instances, they use the containers to get the factory, and they call the factory to create a live instance that will be injected
ctr = cdi.Container()
# we inject the `Foo` class into the `ctr` container
@cdi.Injectable(ctr=ctr)
class Foo:
def __init__(self, number: int) -> None:
self.number = number
cdi.Injectable(ctr=ctr).register(100)
# we define an instance scope that has access to the injectable
# registered in `ctr`
scope = cdi.Scope(__name__, 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', 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 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=ctr).register("hello world")
cdi.Injctable(ctr=ctr2).register("what?")
scope = Scope(__name__, ctr=ctr)
scope2 = scope.fork()
@cdi.Injectable(ctr=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"
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cdi_di-0.0.1b3.tar.gz.
File metadata
- Download URL: cdi_di-0.0.1b3.tar.gz
- Upload date:
- Size: 10.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.5.24
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fffcd7da0962f0939ace14c3c8ca97c1ddaa83dcf53a180977db432f7f06f321
|
|
| MD5 |
1427636c651eab52f84d6182f080b986
|
|
| BLAKE2b-256 |
0f0835e35f7ad79439e12d63e1043c1800569a5de15d817b417ced851cf2259d
|
File details
Details for the file cdi_di-0.0.1b3-py3-none-any.whl.
File metadata
- Download URL: cdi_di-0.0.1b3-py3-none-any.whl
- Upload date:
- Size: 13.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.5.24
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4c632fbd90353090ffa5212dabedbd40cadf6f18a67ded42cf8c3182d2e73b84
|
|
| MD5 |
341c512de8d186a86e30d59f10d1a193
|
|
| BLAKE2b-256 |
4ebf553a69d5387156ea31f61311cd9b67351494fb5e83a097c5b8f808d2a231
|