Skip to main content
Pre-release

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

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
  • Lazy type for circular deps
  • Contextvar support
  • Transient for nonsingleton instances
  • Generics/TypeVar support
  • 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

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

what is not supported

  • TypeVars as parameters that are not used in return type
  • Typevars as injectable return type

Documentation

Container

container contains registered types, types are registered to a container via cdi.Injectable

if a type is not registered in the container, cdi will not attempt to create that type and raise an error instead, explicit is bettern then implicit

you can have multiple different container instances contaning different types, or they contain the same types but they have different providers

ctr = cdi.Container()

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_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 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_ref(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"

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cdi_di-0.0.1b7.tar.gz (20.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cdi_di-0.0.1b7-py3-none-any.whl (23.8 kB view details)

Uploaded Python 3

File details

Details for the file cdi_di-0.0.1b7.tar.gz.

File metadata

  • Download URL: cdi_di-0.0.1b7.tar.gz
  • Upload date:
  • Size: 20.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.24

File hashes

Hashes for cdi_di-0.0.1b7.tar.gz
Algorithm Hash digest
SHA256 8f27f42e4e925f39263cb9f6b9ba82f3e2730f5f78534694c8ff308c99cc6b78
MD5 b499da971a4d5f80771f700bf56b5f0e
BLAKE2b-256 4e4aa960ff3a4f4081a99803b3d10551d26c4c6e954db28a3e866fbd06eddbae

See more details on using hashes here.

File details

Details for the file cdi_di-0.0.1b7-py3-none-any.whl.

File metadata

  • Download URL: cdi_di-0.0.1b7-py3-none-any.whl
  • Upload date:
  • Size: 23.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.24

File hashes

Hashes for cdi_di-0.0.1b7-py3-none-any.whl
Algorithm Hash digest
SHA256 190bf60de97f11d259113ae57f6b790f3064171c1a3107fb3a8ae5f9895c925d
MD5 ecaa8cfa3c7b50b7f4f7ce33aad525bd
BLAKE2b-256 105bb2b3369d891f635bb6164de60f0cc994b12b8d9991fe5aa28330e705de48

See more details on using hashes here.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page