diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/README.rst b/README.rst index d088308..7dbbc82 100644 --- a/README.rst +++ b/README.rst @@ -245,6 +245,11 @@ dependency graph:: >>> from injector import Injector >>> injector = Injector([UserModule(), UserAttributeModule()]) +You can also pass classes instead of instances to ``Injector``, it will +instantiate them for you:: + + >>> injector = Injector([UserModule, UserAttributeModule]) + The injector can then be used to acquire instances of a type, either directly:: >>> injector.get(Name) @@ -331,6 +336,35 @@ scope is "entered" in some low-level code by calling a method on the scope instance that creates this cache. Once the request is complete, the scope is "left" and the cache cleared. +Tests +===== + +When you use unit test framework such as ``unittest2`` or ``nose`` you can also +profit from ``injector``. However, manually creating injectors and test classes +can be quite annoying. There is, however, ``with_injector`` method decorator which +has parameters just as ``Injector`` construtor and installes configured injector into +class instance on the time of method call:: + + >>> from injector import Module, with_injector + >>> class UsernameModule(Module): + ... def configure(self, binder): + ... binder.bind(str, 'Maria') + ... + >>> class TestSomethingClass(object): + ... @with_injector(UsernameModule()) + ... def setup(self): + ... pass + ... + ... @inject(username=str) + ... def test_username(self, username): + ... assert (username == 'Maria') + +*Each* method call re-initializes ``Injector`` - if you want to you can also put +``with_injector`` decorator on class constructor. + +After such call all ``inject``-decorated methods will work just as you'd expect +them to work. + Footnote ======== This framework is similar to snake-guice, but aims for simplification. diff --git a/injector.py b/injector.py index 14bfda7..0ad40b0 100644 --- a/injector.py +++ b/injector.py @@ -393,9 +393,13 @@ class Injector(object): def __init__(self, modules=None, auto_bind=True): """Construct a new Injector. - :param modules: A callable, or list of callables, used to configure the + :param modules: A callable, class, or list of callables/classes, used to configure the Binder associated with this Injector. Typically these - callables will be subclasses of :class:`Module` . + callables will be subclasses of :class:`Module`. + + In case of class, it's instance will be created using parameterless + constructor before the configuration process begins. + Signature is ``configure(binder)``. :param auto_bind: Whether to automatically bind missing types. """ @@ -420,6 +424,9 @@ class Injector(object): self.binder.bind(Binder, to=self.binder) # Initialise modules for module in modules: + if isinstance(module, type): + module = module() + module(self.binder) def get(self, interface, annotation=None, scope=None): @@ -449,13 +456,37 @@ class Injector(object): """Create a new instance, satisfying any dependencies on cls.""" instance = cls.__new__(cls) try: - instance.__injector__ = self + self.install_into(instance) except AttributeError: # Some builtin types can not be modified. pass instance.__init__() return instance + def install_into(self, instance): + """ + Puts injector reference in given object. + """ + instance.__injector__ = self + +def with_injector(*injector_args, **injector_kwargs): + """ + Decorator for a method. Installs Injector object which the method belongs + to before the decorated method is executed. + + Parameters are the same as for Injector constructor. + """ + def wrapper(f): + @functools.wraps(f) + def setup(self_, *args, **kwargs): + injector = Injector(*injector_args, **injector_kwargs) + injector.install_into(self_) + return f(self_, *args, **kwargs) + + return setup + + return wrapper + def provides(interface, annotation=None, scope=None): """Decorator for :class:`Module` methods, registering a provider of a type. diff --git a/injector_test.py b/injector_test.py index 0ac21fd..1a02e7b 100644 --- a/injector_test.py +++ b/injector_test.py @@ -19,7 +19,7 @@ import pytest from injector import (Binder, Injector, Scope, InstanceProvider, ClassProvider, inject, singleton, threadlocal, UnsatisfiedRequirement, CircularDependency, Module, provides, Key, extends, SingletonScope, - ScopeDecorator) + ScopeDecorator, with_injector) class TestBasicInjection(object): @@ -329,6 +329,28 @@ def test_module_provides(): injector = Injector(module) assert (injector.get(str, annotation='name') == 'Bob') +def test_module_class_gets_instantiated(): + name = 'Meg' + class MyModule(Module): + def configure(self, binder): + binder.bind(str, to=name) + + injector = Injector(MyModule) + assert (injector.get(str) == name) + +def test_with_injector_works(): + name = 'Victoria' + def configure(binder): + binder.bind(str, to=name) + + class Aaa(object): + @with_injector(configure) + @inject(username=str) + def __init__(self, username): + self.username = username + + aaa = Aaa() + assert (aaa.username == name) def test_bind_using_key(): Name = Key('name')