Examples

Decorators

Let’s show the use of venusian and the wired.service_factory decorator in building an app that scans for factories. We’ll do it piece-by-piece, starting with a regular wired app.

Basic wired app

As a starting point we use an app with no decorators. In this app we have a Greeting class that depends on a Greeter class. As such, we register a factory for each.

from wired import ServiceRegistry, ServiceContainer


class Greeter:
    def __init__(self, name):
        self.name = name


def greeter_factory(container):
    return Greeter('Marie')


class Greeting:
    def __init__(self, greeter: Greeter):
        self.greeter = greeter

    def greet(self):
        return f'Hello from {self.greeter.name}'


def greeting_factory(container: ServiceContainer):
    greeter = container.get(Greeter)
    return Greeting(greeter)


def app():
    # Do this once at startup
    registry = ServiceRegistry()
    registry.register_factory(greeter_factory, Greeter)
    registry.register_factory(greeting_factory, Greeting)

    # Do this for every "request" or operation
    container = registry.create_container()
    greeting: Greeting = container.get(Greeting)
    assert 'Hello from Marie' == greeting.greet()

This is the basics of a simple, pluggable application. As a note, everything in the app function would typically be done once as part of your app.

Class as factory

Before getting to decorators, just to emphasize…the first argument to wired.ServiceRegistry.register_factory() can be the class itself.

from wired import ServiceContainer, ServiceRegistry


class Greeter:
    def __init__(self, name):
        self.name = name


class Greeting:
    def __init__(self, container: ServiceContainer):
        self.greeter = container.get(Greeter)

    def greet(self):
        return f'Hello from {self.greeter.name}'


def greeter_factory(container):
    return Greeter('Marie')


def app():
    # Do this once at startup
    registry = ServiceRegistry()
    registry.register_factory(greeter_factory, Greeter)
    registry.register_factory(Greeting, Greeting)

    # Do this for every "request" or operation
    container = registry.create_container()
    greeting: Greeting = container.get(Greeting)
    assert 'Hello from Marie' == greeting.greet()

venusian scanner

We will now add venusian and its Scanner. We make a Scanner instance and include the registry. When we call scan on a module – in this case, the same module – it looks for the @service_factory decorator. The decorator then extracts the registry instance we stored in the Scanner and does the registration.

"""
Simplest example for ``@service_factory`` decorator: a basic class.
"""
from venusian import Scanner

from wired import service_factory, ServiceRegistry
from .. import decorators


@service_factory()
class Greeting:
    def __init__(self, container):
        self.greeter = container.get(Greeter)

    def greet(self):
        return f'Hello from {self.greeter.name}'


class Greeter:
    def __init__(self, name):
        self.name = name


def greeter_factory(container):
    return Greeter('Marie')


def app():
    # Do this once at startup
    registry = ServiceRegistry()
    scanner = Scanner(registry=registry)
    # Point the scanner at a package/module and scan
    scanner.scan(decorators.basic_class)

    registry.register_factory(greeter_factory, Greeter)
    # No longer need this line
    # registry.register_factory(Greeting, Greeting)

    # Do this for every "request" or operation
    container = registry.create_container()
    greeting: Greeting = container.get(Greeting)
    assert 'Hello from Marie' == greeting.greet()

What’s nice about this venusian approach: no module-level state globals stuff.

Another decorator plus __wired_factory__

We’ll now move the Greeter class to also use the @service_factory decorator instead of a manual registration. Since it hard-codes Marie as a value to the constructor, we use the __wired_factory__ protocol as a class method to generate the instance. This means any code that does container.get(Greeter) will run this class method to construct the Greeter.

"""
Decorators for both plus usage of the ``__wired_factory__ protocol.
"""
from venusian import Scanner

from wired import service_factory, ServiceRegistry
from .. import decorators


@service_factory()
class Greeter:
    def __init__(self, name):
        self.name = name

    @classmethod
    def __wired_factory__(cls, container):
        return cls('Marie')


@service_factory()
class Greeting:
    greeter: Greeter

    def __init__(self, greeter: Greeter):
        self.greeter = greeter

    def greet(self):
        return f'Hello from {self.greeter.name}'

    @classmethod
    def __wired_factory__(cls, container):
        greeter = container.get(Greeter)
        return cls(greeter)


def app():
    # Do this once at startup
    registry = ServiceRegistry()
    scanner = Scanner(registry=registry)
    # Point the scanner at a package/module and scan
    scanner.scan(decorators.decorator_with_wired_factory)

    # Do this for every "request" or operation
    container = registry.create_container()
    greeting: Greeting = container.get(Greeting)
    assert 'Hello from Marie' == greeting.greet()

We also add a __wired_factory__ class method to Greeting to make it nicer. Now its constructor no longer uses the container, which is a huge surface area. Instead, the class is constructed just with the data it needs, which is nice for testing. The class method acts as an “adapter”, getting stuff out of the container that is needed for the class.

Decorator arguments

The @service_factory acts as a replacement for register_factory. Thus it needs to support the other arguments beyond the first one:

  • The service_or_iface, if not provided, defaults to the class the decorator is decorating

  • If you pass for_= to the decorator, it will be used as the service_or_iface argument to

  • You can also pass context= and name=

Imagine our app now has a Customer and FrenchCustomer as container contexts. Here is an example of registering different Greeter classes that are unique to those contexts:

"""
Decorators for both plus usage of the ``__wired_factory__ protocol.
"""
from venusian import Scanner

from wired import service_factory, ServiceRegistry
from .. import decorators


class Customer:
    def __init__(self):
        self.name = 'Jill'


class FrenchCustomer(Customer):
    def __init__(self):
        super().__init__()
        self.name = 'Juliette'


@service_factory(context=Customer)
class Greeter:
    def __init__(self, name):
        self.name = name

    @classmethod
    def __wired_factory__(cls, container):
        return cls('Susan')


@service_factory(for_=Greeter, context=FrenchCustomer)
class FrenchGreeter:
    """ Serves as Greeter when container.context is FrenchCustomer """
    def __init__(self, name):
        self.name = name

    @classmethod
    def __wired_factory__(cls, container):
        return cls('Marie')


@service_factory(context=Customer)
class Greeting:
    greeter: Greeter

    def __init__(self, greeter: Greeter, customer):
        self.greeter = greeter
        self.customer = customer

    def greet(self):
        return f'Hello from {self.greeter.name} to {self.customer.name}'

    @classmethod
    def __wired_factory__(cls, container):
        greeter = container.get(Greeter)
        customer = container.context
        return cls(greeter, customer)


def app():
    # Do this once at startup
    registry = ServiceRegistry()
    scanner = Scanner(registry=registry)
    # Point the scanner at a package/module and scan
    scanner.scan(decorators.decorator_args)

    # First request, for a regular Customer
    customer1 = Customer()
    container1 = registry.create_container(context=customer1)
    greeting1: Greeting = container1.get(Greeting)
    assert 'Hello from Susan to Jill' == greeting1.greet()

    # Second request, for a FrenchCustomer
    customer2 = FrenchCustomer()
    container2 = registry.create_container(context=customer2)
    greeting2: Greeting = container2.get(Greeting)
    assert 'Hello from Marie to Juliette' == greeting2.greet()

Wired Factory

Registering a factory means two things: a callable that constructs and returns an object, then the “kind” of thing the factory is registered for. You can eliminate the callable as a separate function by providing a __wired_factory__ callable on, for example, the class that gets constructed.

This is the wired factory “protocol” and the callable acts as an adapter. It is handed the container, extracts what it needs, then constructs and returns an object.

Basic wired factory callable

We start again with our simple app, with a Greeting that uses a Greeter. In this case, we do two things:

  • Both classes have a classmethod that manages construction of instances

  • The register_factory first argument is, thus, the class itself

"""
A class is a callable that can be the factory supplied to
:func:`wired.ServiceRegistry.register_factory`.
"""

from wired import ServiceContainer, ServiceRegistry


class Greeter:
    def __init__(self, name):
        self.name = name

    @classmethod
    def __wired_factory__(cls, container: ServiceContainer):
        return cls('Marie')


class Greeting:
    def __init__(self, greeter: Greeter):
        self.greeter = greeter

    def greet(self):
        return f'Hello from {self.greeter.name}'

    @classmethod
    def __wired_factory__(cls, container):
        greeter = container.get(Greeter)
        return cls(greeter)


def app():
    # Do this once at startup
    registry = ServiceRegistry()
    registry.register_factory(Greeter, Greeter)
    registry.register_factory(Greeting, Greeting)

    # Do this for every "request" or operation
    container = registry.create_container()
    greeting: Greeting = container.get(Greeting)
    assert 'Hello from Marie' == greeting.greet()

With this, when some application code calls container.get(Greeter), the construction is done by Greeter.__wired_factory__.