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.