Datastore

Sharp readers will have spotted the flaw in decoupled: the main application had to know about and create a FrenchCustomer. We want the add-on to be the only place that knows about.

Let’s make a mini-database of Customers and let the add-on create and store a FrenchCustomer. The main app is then blissfully unaware…it just knows to that a Customer has a type and a name.

We do this by making a Datastore dataclass and registering an instance as a singleton. This lets all service factories fetch the Datastore.

Along the way we did some refactoring to make the call sequence better match how an application like this should work:

  • greet_customer doesn’t make its own container, instead, one is made in the core of the app

  • Rename greet_customer to customer_interaction to connote this might be more steps than just a greeting

  • Change the functions to receive a container (for the interaction/request) rather than the entire registry

  • Rename the __init__.setup function to app_bootstrap to make it more clear what it’s doing

  • Move some of its responsibility to a setup function to match the add-on setup, thus the core isn’t “special”

  • Refactor sample interactions out of main to make testing simpler, then decrease the test to just one integration-style test (these are sample applications, no need for unit tests)

Code

__init__.py

"""

A customer walks into a store. Do the steps to interact with them:

- Get a correct greeter

- Interact with them

Simple wired application:

- Settings that say what punctuation to use

- Registry

- A bundled Greeter and Customer

- An add-on which defines a FrenchGreeter and FrenchCustomer

- A Datastore which stores/retrieves instances of Customers

"""

from dataclasses import dataclass, field
from typing import List

from wired import ServiceContainer, ServiceRegistry


@dataclass
class Customer:
    name: str


@dataclass
class Datastore:
    customers: List[Customer] = field(default_factory=list)


@dataclass
class Settings:
    punctuation: str


@dataclass
class Greeter:
    punctuation: str
    greeting: str = 'Hello'

    def __call__(self, customer: Customer) -> str:
        return f'{self.greeting} {customer.name} {self.punctuation}'


def setup(registry: ServiceRegistry, settings: Settings):
    """Initialize the features in the core application"""

    # Make and register the Datastore
    datastore = Datastore()
    registry.register_singleton(datastore, Datastore)

    # **** Default Greeter
    # Make the greeter factory, using punctuation from settings
    punctuation = settings.punctuation

    def default_greeter_factory(container) -> Greeter:
        # Use the dataclass default for greeting
        return Greeter(punctuation=punctuation)

    # Register it as a factory using its class for the "key"
    registry.register_factory(default_greeter_factory, Greeter)

    # During bootstrap, make some Customers
    customer1 = Customer(name='Mary')
    datastore.customers.append(customer1)


def app_bootstrap(settings: Settings) -> ServiceRegistry:
    # Make the registry
    registry = ServiceRegistry()

    # Do setup for the core application features
    setup(registry, settings)

    # Import the add-on and initialize it
    from .custom import setup as addon_setup

    addon_setup(registry, settings)

    return registry


def customer_interaction(container: ServiceContainer, customer: Customer) -> str:
    """Customer comes in, handle the steps in greeting them"""

    # Get a Greeter using the customer as context. Use the Customer when
    # generating the greeting.
    greeter: Greeter = container.get(Greeter, context=customer)
    greeting = greeter(customer)

    return greeting


def sample_interactions(registry: ServiceRegistry) -> List[str]:
    """Pretend to do a couple of customer interactions"""

    greetings = []

    bootstrap_container: ServiceContainer = registry.create_container()
    datastore: Datastore = bootstrap_container.get(Datastore)
    for customer in datastore.customers:
        # Do a sample "interaction" (aka transaction, aka request) for
        # each customer. This is like handling a view for a request.
        interaction_container = registry.create_container()
        greeting = customer_interaction(interaction_container, customer)
        greetings.append(greeting)

    return greetings


def main():
    settings = Settings(punctuation='!!')
    registry = app_bootstrap(settings)
    greetings = sample_interactions(registry)
    assert greetings == ['Hello Mary !!', 'Bonjour Henri !!']

custom.py

"""

A custom add-on to our app which adds FrenchCustomer and
French Greeter.

"""

from dataclasses import dataclass

from wired import ServiceContainer, ServiceRegistry

from . import Customer, Datastore, Greeter, Settings


@dataclass
class FrenchCustomer(Customer):
    pass


@dataclass
class FrenchGreeter(Greeter):
    greeting: str = 'Bonjour'


def setup(registry: ServiceRegistry, settings: Settings):
    # The French greeter, using context of FrenchCustomer
    punctuation = settings.punctuation

    def french_greeter_factory(container) -> Greeter:
        # Use the dataclass default for greeting
        return FrenchGreeter(punctuation=punctuation)

    # Register it as a factory using its class for the "key", but
    # this time register with a "context"
    registry.register_factory(french_greeter_factory, Greeter, context=FrenchCustomer)

    # Grab the Datastore and add a FrenchCustomer
    container: ServiceContainer = registry.create_container()
    datastore: Datastore = container.get(Datastore)
    customer1 = FrenchCustomer(name='Henri')
    datastore.customers.append(customer1)