2. Injecting beans

Let’s explore Grundzeug’s injection capabilities in more detail.

2.1. Different types of dependency injection

2.1.1. Function kwarg injection

The most basic form of dependency injection is function kwarg injection:

from grundzeug.container.di import Inject

def perform_foo(greeting: str, contract_impl: Inject[Contract]) -> None:
    contract_impl.foo()
    print(greeting)

# Partially apply perform_foo using beans from the
# container, binding a bean that satisfies
# Contract to contract_impl.
injected_func : Callable[[str], None] = container.inject(perform_foo)
# We can still pass arguments to an injected function!
injected_func("hello world")

inject() inspects the type annotations of the function’s arguments and determines which arguments should be retrieved from the container. In this case, the call to inject() will be equivalent to the following partial application:

injected_func : Callable[[str], None] = functools.partial(perform_foo, contract_impl=container.resolve[Contract]())

2.1.2. Constructor injection

Function kwarg injection can also be used with class constructors:

from grundzeug.container.di import Inject

class FooHolder:
    def __init__(greeting: str, contract_impl: Inject[Contract]):
        self.greeting = greeting
        self.contract_impl = contract_impl

# Partially apply Foo using beans from the
# container, binding a bean that satisfies
# Contract to contract_impl.
injected_foo_holder : Callable[[str], None] = container.inject(FooHolder)
# We can still pass arguments to an injected constructor!
instance = injected_foo("hello world")
instance.contract_impl.foo()
print(instance.greeting) # hello world

You can even do the same thing with dataclasses:

import dataclasses
from grundzeug.container.di import Inject

@dataclasses.dataclass
class FooHolder:
    greeting: str
    contract_impl: Inject[Contract]

# Partially apply Foo using beans from the
# container, binding a bean that satisfies
# Contract to contract_impl.
injected_foo_holder : Callable[[str], None] = container.inject(FooHolder)
# We can still pass arguments to an injected constructor!
instance = injected_foo("hello world")
instance.contract_impl.foo()
print(instance.greeting) # hello world

2.1.3. Field injection

Sometimes, you may want to inject dependencies directly into class fields, without using dataclasses. In this case, you can decorate your class with injectable():

from grundzeug.container.di import Inject, injectable

@injectable
class FooHolder:
    contract_impl: Inject[Contract]

    def __init__(greeting: str):
        self.greeting = greeting

injected_foo_holder : Callable[[str], None] = container.inject(FooHolder)
# We can still pass arguments to an injected constructor!
instance = injected_foo("hello world")
instance.contract_impl.foo()
print(instance.greeting) # hello world

Warning

In this case, instance will actually be an instance of a wrapper class generated by injectable(), which extends FooHolder.

2.2. Named beans

It is possible to inject named beans using the InjectNamed annotation:

from grundzeug.container.di import InjectNamed

def perform_foo(greeting: str, contract_impl: InjectNamed[Contract, "named_bean"]) -> None:
    contract_impl.foo()
    print(greeting)

container.register_type[Contract, ContractImpl](bean_name="named_bean")
...
injected_func : Callable[[str], None] = container.inject(perform_foo)
injected_func("hello world")