diff --git a/README.rst b/README.rst index 1567c1b..8c62fb2 100644 --- a/README.rst +++ b/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') diff --git a/injector.py b/injector.py index ec87adc..9eafc23 100644 --- a/injector.py +++ b/injector.py @@ -8,9 +8,181 @@ # # Author: Alec Thomas -"""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 @@ -142,14 +314,14 @@ class Binder(object): self._bindings = {} #def install(self, module): - #"""Install bindings from another :class:`Module`.""" + #"""Install bindings from another :class:`Module` .""" ## TODO(alec) Confirm this is sufficient... #self._bindings.update(module._bindings) 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: @@ -332,7 +529,7 @@ def provides(interface, annotation=None, scope=None): def extends(interface, annotation=None, scope=None): """A decorator for :class:`Module` methods, extending a - :meth:`Module.multibind`. + :meth:`Module.multibind` . :param interface: Interface to provide. :param annotation: Optional annotation value. @@ -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. diff --git a/setup.cfg b/setup.cfg index 795786f..9e3d0af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,5 @@ [egg_info] [nosetests] +tests = injector,test with-doctest = 1