Injected Field

Our DI system now lets us use dataclasses to give the system instructions on how to create our dataclass instances. Sometimes we want to give more instruction. Let’s see how to use the injected field to do so.

In the previous example, we had to pass in the customer to the __call__. Can’t we get that from injection? After all, we made a Customer and a FrenchCustomer and added it to the container in request.py.

First, we’ll make a small change in request.py to pass in a customer name during construction, rather than have a dataclass field default value:

# request.py
from .models import Customer, FrenchCustomer, Greeter


def process_request(registry):
    # Handle two requests: first a regular Customer and then a
    # FrenchCustomer. In both cases:
    # - Make a request, meaning a container
    # - Make a customer instance matching that request
    # - Stash the customer in the container as the context

    # Handle a regular customer by setting the container's context
    # to an instance of Customer
    regular_customer = Customer(name='Billy')
    container = registry.create_container(context=regular_customer)
    greeter = container.get(Greeter)
    greeting = greeter()

    # Handle a French customer by making a container with
    # a "context" that is a FrenchCustomer
    french_customer = FrenchCustomer(name='Sophie')
    container = registry.create_container(context=french_customer)
    french_greeter = container.get(Greeter)
    french_greeting = french_greeter()

    return greeting, french_greeting

The Customer and FrenchCustomer dataclasses change slightly in models.py, to not have a field default. The bigger changes are in Greeter and FrenchGreeter:

# models.py
from dataclasses import dataclass

from wired.dataclasses import Context, factory, injected


@factory()
@dataclass
class Settings:
    """Store some configuration settings for the app"""

    punctuation: str = '.'


@dataclass
class Customer:
    """A basic customer"""

    name: str = 'Larry'


@dataclass
class FrenchCustomer:
    """A certain kind of customer"""

    name: str = 'Anne'


@factory(context=Customer)
@dataclass
class Greeter:
    """A basic greeter"""

    settings: Settings
    customer: Customer = injected(Context)
    name: str = 'Mary'

    def __call__(self):
        punctuation = self.settings.punctuation
        m = f'my name is {self.name}{punctuation}'
        return f'Hello {self.customer.name} {m}'


@factory(for_=Greeter, context=FrenchCustomer)
@dataclass
class FrenchGreeter:
    """A greeter to use when the customer (context) is French"""

    settings: Settings
    customer: FrenchCustomer = injected(Context)
    name: str = 'Henri'

    def __call__(self):
        punctuation = self.settings.punctuation
        m = f'je m\'apelle {self.name}{punctuation}'
        return f'Salut {self.customer.name} {m}'

Foremost, we gain a new field that asks for a customer of the correct type (Customer or FrenchCustomer.) We use the injected field helper to tell the injection system to get the value from the Context in the container. As such, our factory decorator needs to ask for a context.

With that, __call__ no longer needs to have a customer name passed into it. We have the correct customer on the dataclass instance, thanks to injection.

Note

We could even eliminate __call__ by using the dataclasses support for __post_init__ to stash a customer name on the instance during construction.

What’s happening behind the scenes? injected is a subclass of dataclasses.field which packs some information into the field metadata. This metadata lets us give more instructions to the DI system.

Everything else in our app is the same, which shows our dataclasses can use DI to drive what they receive.