Skip to content

Commit

Permalink
Merge pull request #41 from mmerickel/pe-wired-factory
Browse files Browse the repository at this point in the history
pe-wired-factory
  • Loading branch information
mmerickel authored Nov 29, 2020
2 parents 7614c9d + 87c34fb commit 4bc208d
Show file tree
Hide file tree
Showing 38 changed files with 580 additions and 43 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ include .coveragerc .flake8
include tox.ini readthedocs.yml

recursive-exclude * __pycache__ *.py[cod]
recursive-include examples *.py
3 changes: 3 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
.. autoclass:: ServiceRegistry
:members:

.. autoclass:: service_factory
:members:

:mod:`wired.dataclasses` API
============================

Expand Down
97 changes: 97 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
========
Examples
========

.. _examples-decorators:

Decorators
~~~~~~~~~~

Let's show the use of `venusian <https://pypi.org/project/venusian/>`_ and the :class:`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.

.. literalinclude:: ../examples/decorators/no_decorator.py

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 :meth:`wired.ServiceRegistry.register_factory` can be the class itself.

.. literalinclude:: ../examples/decorators/no_decorator_class.py
:emphasize-lines: 25

``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.

.. literalinclude:: ../examples/decorators/basic_class.py

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``.

.. literalinclude:: ../examples/decorators/decorator_with_wired_factory.py

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:

.. literalinclude:: ../examples/decorators/decorator_args.py

.. _examples-wired-factory:

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

.. literalinclude:: ../examples/wired_factory/register_wired_factory.py

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

1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ More Information
:maxdepth: 1

usage
examples
tutorial/index
dc/index
dc/usage
Expand Down
4 changes: 0 additions & 4 deletions docs/tutorial/context/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,3 @@ def main():
# then test the result.
french_customer = FrenchCustomer(name='Henri')
assert 'Bonjour Henri !!' == greet_customer(registry, french_customer)


if __name__ == '__main__':
main()
4 changes: 0 additions & 4 deletions docs/tutorial/datastore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,3 @@ def main():
registry = app_bootstrap(settings)
greetings = sample_interactions(registry)
assert greetings == ['Hello Mary !!', 'Bonjour Henri !!']


if __name__ == '__main__':
main()
4 changes: 0 additions & 4 deletions docs/tutorial/decoupled/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,3 @@ def main():

french_customer = FrenchCustomer(name='Henri')
assert 'Bonjour Henri !!' == greet_customer(registry, french_customer)


if __name__ == '__main__':
main()
4 changes: 0 additions & 4 deletions docs/tutorial/factory/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,3 @@ def main():
registry = setup()
greeting = greet_a_customer(registry)
assert greeting == 'Hello !!'


if __name__ == '__main__':
main()
9 changes: 2 additions & 7 deletions docs/tutorial/overrides/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def setup(registry: ServiceRegistry, settings: Settings):
# Make the greeter factory, using punctuation from settings
punctuation = settings.punctuation

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

Expand All @@ -59,7 +59,6 @@ def app_bootstrap(settings: Settings) -> ServiceRegistry:
from .custom import setup as addon_setup

addon_setup(registry, settings)

return registry


Expand Down Expand Up @@ -97,8 +96,4 @@ def main():
settings = Settings(punctuation='!!')
registry = app_bootstrap(settings)
greetings = sample_interactions(registry)
assert greetings == ['Hello Mary !!', 'Bonjour Henri !!']


if __name__ == '__main__':
main()
assert ['Override Mary !!', 'Bonjour Henri !!'] == greetings
4 changes: 0 additions & 4 deletions docs/tutorial/requests_views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,3 @@ def main():
registry = app_bootstrap(settings)
greetings = sample_interactions(registry)
assert greetings == ['Hello Mary !!', 'Bonjour Henri !!']


if __name__ == '__main__':
main()
4 changes: 0 additions & 4 deletions docs/tutorial/settings/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,3 @@ def main():
registry = setup(settings)
greeting = greet_a_customer(registry)
assert greeting == 'Hello !!'


if __name__ == '__main__':
main()
4 changes: 0 additions & 4 deletions docs/tutorial/simple/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,3 @@ def main():
registry = setup()
greeting = greet_a_customer(registry)
assert greeting == 'Hello !!'


if __name__ == '__main__':
main()
44 changes: 44 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,53 @@ However, a service factory that is registered to provide instances of the ``Logi
Anyone else registering such a service factory is directly competing for control of that type.
It is possible to register for both type and name.

As a note, when calling :meth:`wired.ServiceRegistry.register_factory`, the first argument is a callable.
It doesn't have to be a function: it could be a class that accepts the container as an argument.
In such a case, the class might be *both* arguments.
This kind of usage is helpful when combined with the ``__wired_factory__`` protocol and the ``@service_factory`` decorator discussed below.

Service factories accept one argument, a :class:`wired.ServiceContainer` instance.
The container may be used to get any dependencies required to create the service and return it from the factory.
The service is then cached on the container, available for any other factories or code to get.

The ``@service_factory`` decorator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

It's quite convenient to register your factories with decorators, as you can just point at a module or package and not manually do each registration.
``wired`` has optional support for the `venusian <https://pypi.org/project/venusian/>`_ package for deferred scanning and evaluation of decorators.
You can then register your services with the :class:`wired.service_factory` decorator:

.. literalinclude:: ../examples/decorators/basic_class.py
:start-at: import service_factory
:end-at: Hello from

The decorator can take arguments of ``for_``, ``context``, and ``name``, to mimic the arguments to ``register_factory``.

You can find more variations, including setup of the scanner, on this in the :ref:`examples-decorators` examples.

The ``__wired_factory__`` protocol
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

When you call :meth:`wired.ServiceRegistry.register_factory` the first argument is the factory "function".
It takes a :class:`wired.ServiceContainer`, makes the desired object, and returns it.
This means a separation: a "function" that makes the class instance, and the class.

As we saw above, the first argument is really a callable that returns an instance, and that means the class itself can be the first argument.
But what if you want custom logic to pick apart the container then construct the class?

Enter the ``__wired_factory__`` protocol.
This is an attribute -- for example, a ``classmethod`` -- on the factory callable.
It is passed the container and returns the class.

.. literalinclude:: ../examples/wired_factory/register_wired_factory.py
:start-at: @classmethod
:end-at: return cls

This class method is then used as the first argument to :meth:`wired.ServiceRegistry.register_factory`.
It doesn't have to be just for classes and class methods: a function/class/instance could have a ``__wired_factory__`` attribute stamped on it, possibly via an intermediate decorator.

More examples are available in :ref:`examples-wired-factory`.

Example
~~~~~~~

Expand Down Expand Up @@ -130,3 +173,4 @@ For example, imagine binding the web request itself as a service, or the active
# later ...
user = container.get(IUser)
1 change: 1 addition & 0 deletions examples/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#
1 change: 1 addition & 0 deletions examples/decorators/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#
42 changes: 42 additions & 0 deletions examples/decorators/basic_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
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()
77 changes: 77 additions & 0 deletions examples/decorators/decorator_args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
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()
Loading

0 comments on commit 4bc208d

Please sign in to comment.