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 decoratingIf you pass
for_=
to the decorator, it will be used as theservice_or_iface
argument toYou can also pass
context=
andname=
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 instancesThe
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__
.