Flesh out documentation.
This commit is contained in:
parent
f6ef41e2ea
commit
c68c87dc20
53
README.rst
53
README.rst
|
@ -1,37 +1,44 @@
|
|||
Injector - Python dependency injection framework, inspired by Guice
|
||||
===================================================================
|
||||
======================================================================
|
||||
Dependency injection as a formal pattern is less useful in Python than in other
|
||||
languages, primarily due to its support for keyword arguments, the ease with
|
||||
which objects can be mocked, and its dynamic nature.
|
||||
|
||||
This framework is also similar to snake-guice, but aims for simplification.
|
||||
That said, a framework for assisting in this process can remove a lot of
|
||||
boiler-plate from larger applications. That's where Injector can help. As an
|
||||
added benefit, Injector encourages nicely compartmentalised code through the
|
||||
use of :class:`Module` s.
|
||||
|
||||
``foo``
|
||||
|
||||
While being inspired by Guice, it does not slavishly replicate its API.
|
||||
Providing a Pythonic API trumps faithfulness.
|
||||
|
||||
An Example
|
||||
----------
|
||||
Concepts
|
||||
--------
|
||||
For those new to dependency-injection and/or Guice, some of the terminology may
|
||||
not be obvious. For clarification:
|
||||
|
||||
*TODO: Write a more useful example.*
|
||||
Injector:
|
||||
pass
|
||||
|
||||
Here's a brief, completely contrived, example from the unit tests::
|
||||
:class:`Binding`:
|
||||
|
||||
from injector import Injector, Module, Key, injects, provides
|
||||
:class:`Provider`:
|
||||
A means of providing an instance of a type. Built-in providers include
|
||||
:class:`ClassProvider` (creates a new instance from a class),
|
||||
:class:`InstanceProvider` (returns an instance directly)
|
||||
|
||||
Weight = Key('Weight')
|
||||
Age = Key('Age')
|
||||
Description = Key('Description')
|
||||
At its heart, the :class:`Injector` is simply a dictionary, mapping types to
|
||||
providers of instances of those types. This could be as simple as::
|
||||
|
||||
class MyModule(Module):
|
||||
@provides(Weight)
|
||||
def provide_weight(self):
|
||||
return 50.0
|
||||
{str: str}
|
||||
|
||||
@provides(Age)
|
||||
def provide_age(self):
|
||||
return 25
|
||||
|
||||
@provides(Description)
|
||||
@inject(age=Age, weight=Weight)
|
||||
def provide_description(self, age, weight):
|
||||
return 'Bob is %d and weighs %0.1fkg' % (age, weight)
|
||||
Footnote
|
||||
--------
|
||||
This framework is similar to snake-guice, but aims for simplification.
|
||||
|
||||
:copyright: (c) 2010 by Alec Thomas
|
||||
:license: BSD
|
||||
|
||||
injector = Injector(MyModule())
|
||||
assert_equal(injector.get(Description), 'Bob is 25 and weighs 50.0kg')
|
||||
|
|
220
injector.py
220
injector.py
|
@ -8,9 +8,181 @@
|
|||
#
|
||||
# Author: Alec Thomas <alec@swapoff.org>
|
||||
|
||||
"""Dependency injection framework.
|
||||
"""Injector - Python dependency injection framework, inspired by Guice
|
||||
######################################################################
|
||||
Dependency injection as a formal pattern is less useful in Python than in other
|
||||
languages, primarily due to its support for keyword arguments, the ease with
|
||||
which objects can be mocked, and its dynamic nature.
|
||||
|
||||
This is based heavily on snake-guice, but is hopefully much simplified.
|
||||
That said, a framework for assisting in this process can remove a lot of
|
||||
boiler-plate from larger applications. That's where Injector can help. It
|
||||
automatically and transitively provides keyword arguments with their values. As
|
||||
an added benefit, Injector encourages nicely compartmentalised code through the
|
||||
use of :class:`Module` s.
|
||||
|
||||
While being inspired by Guice, it does not slavishly replicate its API.
|
||||
Providing a Pythonic API trumps faithfulness.
|
||||
|
||||
Terminology
|
||||
===========
|
||||
At its heart, Injector is simply a dictionary for mapping types to things that
|
||||
create instances of those types. This could be as simple as::
|
||||
|
||||
{str: 'an instance of a string'}
|
||||
|
||||
For those new to dependency-injection and/or Guice, though, some of the
|
||||
terminology used may not be obvious.
|
||||
|
||||
Provider
|
||||
--------
|
||||
A means of providing an instance of a type. Built-in providers include
|
||||
:class:`ClassProvider` (creates a new instance from a class),
|
||||
:class:`InstanceProvider` (returns an existing instance directly) and
|
||||
:class:`CallableProvider` (provides an instance by calling a function).
|
||||
|
||||
Scope
|
||||
-----
|
||||
By default, providers are executed each time an instance is required. Scopes
|
||||
allow this behaviour to be customised. For example, :class:`SingletonScope`
|
||||
(typically used through the class decorator :mvar:`singleton`), can be used to
|
||||
always provide the same instance of a class.
|
||||
|
||||
Other examples of where scopes might be a threading scope, where instances are
|
||||
provided per-thread, or a request scope, where instances are provided
|
||||
per-HTTP-request.
|
||||
|
||||
The default scope is :class:`NoScope`.
|
||||
|
||||
Binding Key
|
||||
-----------
|
||||
A binding key uniquely identifies a provider of a type. It is effectively a
|
||||
tuple of ``(type, annotation)`` where ``type`` is the type to be provided and
|
||||
``annotation`` is additional, optional, uniquely identifying information for
|
||||
the type.
|
||||
|
||||
For example, the following are all unique binding keys for ``str``::
|
||||
|
||||
(str, 'name')
|
||||
(str, 'description')
|
||||
|
||||
For a generic type such as ``str``, annotations are very useful for unique
|
||||
identification.
|
||||
|
||||
As an *alternative* convenience to using annotations, :func:`Key` may be used
|
||||
to create unique types as necessary::
|
||||
|
||||
>>> Name = Key('name')
|
||||
>>> Description = Key('description')
|
||||
|
||||
Which may then be used as binding keys, without annotations, as they already
|
||||
uniquely identify a particular provider::
|
||||
|
||||
(Name, None)
|
||||
(Description, None)
|
||||
|
||||
Though of course, annotations may still be used with these types, like any
|
||||
other type.
|
||||
|
||||
Annotation
|
||||
----------
|
||||
An annotation is additional unique information about a type to avoid binding
|
||||
key collisions. It creates a new unique binding key for an existing type.
|
||||
|
||||
Binding
|
||||
-------
|
||||
A binding is the mapping of a unique binding key to a corresponding provider.
|
||||
For example::
|
||||
|
||||
>>> bindings = {
|
||||
... (Name, None): InstanceProvider('Sherlock'),
|
||||
... (Description, None): InstanceProvider('A man of astounding insight')}
|
||||
... }
|
||||
|
||||
Binder
|
||||
------
|
||||
The :class:`Binder` is simply a convenient wrapper around the dictionary
|
||||
that maps types to providers. It provides methods that make declaring bindings
|
||||
easier.
|
||||
|
||||
Module
|
||||
------
|
||||
A :class:`Module` configures bindings. It provides methods that simplify the
|
||||
process of binding a key to a provider. For example the above bindings would be
|
||||
created with::
|
||||
|
||||
>>> class MyModule(Module):
|
||||
... def configure(self, binder):
|
||||
... binder.bind(Name, to='Sherlock')
|
||||
... binder.bind(Description, to='A man of astounding insight')
|
||||
|
||||
For more complex instance construction, methods decorated with
|
||||
``@provides`` will be called to resolve binding keys::
|
||||
|
||||
>>> class MyModule(Module):
|
||||
... def configure(self, binder):
|
||||
... binder.bind(Name, to='Sherlock')
|
||||
...
|
||||
... @provides(Description)
|
||||
... def describe(self):
|
||||
... return 'A man of astounding insight (at %s)' % time.time()
|
||||
|
||||
Injection
|
||||
---------
|
||||
Injection is the process of providing an instance of a type, to a method that
|
||||
uses that instance. It is achieved with the :func:`inject` decorator. Keyword
|
||||
arguments to inject define which arguments in its decorated method should be
|
||||
injected, and with what.
|
||||
|
||||
Here is an example of injection on a module provider method, and on the
|
||||
constructor of a normal class::
|
||||
|
||||
>>> class User(object):
|
||||
... @inject(name=Name, description=Description)
|
||||
... def __init__(self, name, description):
|
||||
... self.name = name
|
||||
... self.description = description
|
||||
|
||||
>>> class UserModule(Module):
|
||||
... def configure(self, binder):
|
||||
... binder.bind(User)
|
||||
|
||||
>>> class UserAttributeModule(Module):
|
||||
... def configure(self, binder):
|
||||
... binder.bind(Name, to='Sherlock')
|
||||
...
|
||||
... @provides(Description)
|
||||
... @inject(name=Name)
|
||||
... def describe(self, name):
|
||||
... return '%s is a man of astounding insight' % name
|
||||
|
||||
Injector
|
||||
--------
|
||||
The :class:`Injector` brings everything together. It takes a list of
|
||||
:class:`Module` s, and configures them with a binder, effectively creating a
|
||||
dependency graph::
|
||||
|
||||
>>> injector = Injector([UserModule(), UserAttributeModule()])
|
||||
|
||||
The injector can then be used to acquire instances of a type, either directly::
|
||||
|
||||
>>> injector.get(Name)
|
||||
'Sherlock'
|
||||
>>> injector.get(Description)
|
||||
'Sherlock is a man of astounding insight'
|
||||
|
||||
Or transitively::
|
||||
|
||||
>>> user = injector.get(User)
|
||||
>>> isinstance(user, User)
|
||||
True
|
||||
>>> user.name
|
||||
'Sherlock'
|
||||
>>> user.description
|
||||
'Sherlock is a man of astounding insight'
|
||||
|
||||
Footnote
|
||||
========
|
||||
This framework is similar to snake-guice, but aims for simplification.
|
||||
|
||||
:copyright: (c) 2010 by Alec Thomas
|
||||
:license: BSD
|
||||
|
@ -149,7 +321,7 @@ class Binder(object):
|
|||
def bind(self, interface, to=None, annotation=None, scope=None):
|
||||
"""Bind an interface to an implementation.
|
||||
|
||||
:param interface: Interface or Key to bind.
|
||||
:param interface: Interface or :func:`Key` to bind.
|
||||
:param to: Instance or class to bind to, or an explicit
|
||||
:class:`Provider` subclass.
|
||||
:param annotation: Optional global annotation of interface.
|
||||
|
@ -162,9 +334,14 @@ class Binder(object):
|
|||
def multibind(self, interface, to, annotation=None, scope=None):
|
||||
"""Creates or extends a multi-binding.
|
||||
|
||||
A multi-binding is a mapping from an interface or Key to
|
||||
A multi-binding maps from a key to a sequence, where each element in
|
||||
the sequence is provided separately.
|
||||
|
||||
See :meth:`bind` for argument descriptions.
|
||||
:param interface: Interface or :func:`Key` to bind.
|
||||
:param to: Instance or class to bind to, or an explicit
|
||||
:class:`Provider` subclass.
|
||||
:param annotation: Optional global annotation of interface.
|
||||
:param scope: Optional Scope in which to bind.
|
||||
"""
|
||||
key = BindingKey(interface, annotation)
|
||||
if key not in self._bindings:
|
||||
|
@ -208,7 +385,8 @@ class Binder(object):
|
|||
class Scope(object):
|
||||
"""A Scope looks up the Provider for a binding.
|
||||
|
||||
By default (ie. NoScope) this simply returns the default Provider.
|
||||
By default (ie. :class:`NoScope` ) this simply returns the default
|
||||
:class:`Provider` .
|
||||
"""
|
||||
def get(self, key, provider):
|
||||
raise NotImplementedError
|
||||
|
@ -220,16 +398,30 @@ class Scope(object):
|
|||
|
||||
|
||||
class NoScope(Scope):
|
||||
"""A binding without scope.
|
||||
"""A binding that always returns the provider.
|
||||
|
||||
This is the default. Every :meth:`Injector.get` results in a new instance
|
||||
being created.
|
||||
|
||||
The global instance :mvar:`noscope` can be used as a convenience.
|
||||
"""
|
||||
def get(self, unused_key, provider):
|
||||
return provider
|
||||
|
||||
|
||||
class SingletonScope(Scope):
|
||||
"""A :class:`Scope` that returns a single instance for a particular key.
|
||||
|
||||
The global instance :mvar:`singleton` can be used as a convenience.
|
||||
|
||||
>>> class A(object): pass
|
||||
>>> injector = Injector()
|
||||
>>> provider = ClassProvider(A, injector)
|
||||
>>> a = singleton.get(A, provider)
|
||||
>>> b = singleton.get(A, provider)
|
||||
>>> a is b
|
||||
True
|
||||
"""
|
||||
def __init__(self):
|
||||
self._cache = {}
|
||||
|
||||
|
@ -270,15 +462,20 @@ class Module(object):
|
|||
|
||||
|
||||
class Injector(object):
|
||||
"""Creates object graph and injects dependencies."""
|
||||
"""Initialise the object dependency graph from a set of :class:`Module` s,
|
||||
and allow consumers to create instances from the graph.
|
||||
"""
|
||||
|
||||
def __init__(self, modules=None):
|
||||
"""Construct a new Injector.
|
||||
|
||||
:param modules: A callable, or list of callables, used to configure the
|
||||
Binder associated with this Injector. Signature is
|
||||
``configure(binder)``.
|
||||
Binder associated with this Injector. Typically these
|
||||
callables will be subclasses of :class:`Module` .
|
||||
Signature is ``configure(binder)``.
|
||||
"""
|
||||
# Stack of keys currently being injected. Used to detect circular
|
||||
# dependencies.
|
||||
self._stack = []
|
||||
self._binder = Binder(self)
|
||||
if not modules:
|
||||
|
@ -445,6 +642,9 @@ class BaseKey(object):
|
|||
def Key(name):
|
||||
"""Create a new type key.
|
||||
|
||||
Typically when using Injector, complex types can be bound to providers
|
||||
directly.
|
||||
|
||||
Keys are a convenient alternative to binding to (type, annotation) pairs,
|
||||
particularly when non-unique types such as str or int are being bound.
|
||||
|
||||
|
|
Loading…
Reference in New Issue