diff --git a/dependency_injector/injections.py b/dependency_injector/injections.py index 747babcb..fde45105 100644 --- a/dependency_injector/injections.py +++ b/dependency_injector/injections.py @@ -1,11 +1,21 @@ """Injections module.""" +import sys import six from .utils import is_provider from .utils import ensure_is_injection from .utils import get_injectable_kwargs +from .errors import Error + + +IS_PYPY = '__pypy__' in sys.builtin_module_names +if IS_PYPY or six.PY3: # pragma: no cover + OBJECT_INIT = six.get_unbound_function(object.__init__) +else: # pragma: no cover + OBJECT_INIT = None + class Injection(object): @@ -60,8 +70,22 @@ def inject(*args, **kwargs): injections += tuple(ensure_is_injection(injection) for injection in args) - def decorator(callback): + def decorator(callback_or_cls): """Dependency injection decorator.""" + if isinstance(callback_or_cls, six.class_types): + cls = callback_or_cls + try: + cls_init = six.get_unbound_function(cls.__init__) + assert cls_init is not OBJECT_INIT + except (AttributeError, AssertionError): + raise Error( + 'Class {0}.{1} has no __init__() '.format(cls.__module__, + cls.__name__) + + 'method and could not be decorated with @inject decorator') + cls.__init__ = decorator(cls_init) + return cls + + callback = callback_or_cls if hasattr(callback, 'injections'): callback.injections += injections return callback diff --git a/docs/advanced_usage/index.rst b/docs/advanced_usage/index.rst index 148d6d29..bc010888 100644 --- a/docs/advanced_usage/index.rst +++ b/docs/advanced_usage/index.rst @@ -18,10 +18,23 @@ called to provide injectable values. Example: -.. literalinclude:: ../../examples/advanced_usage/inject_decorator_simple.py +.. literalinclude:: ../../examples/advanced_usage/inject_simple.py :language: python Example of usage ``@di.inject()`` decorator with Flask: -.. literalinclude:: ../../examples/advanced_usage/inject_decorator_flask.py +.. literalinclude:: ../../examples/advanced_usage/inject_flask.py + :language: python + + +@inject decorator with classes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``@di.inject()`` could be applied for classes. In such case, it will look for +class ``__init__()`` method and pass injection to it. If decorated class has +no ``__init__()`` method, appropriate ``di.Error`` will be raised. + +Example of usage ``@di.inject()`` with Flask class-based view: + +.. literalinclude:: ../../examples/advanced_usage/inject_flask_class_based.py :language: python diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 06afa4b0..7bfa3905 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -11,7 +11,7 @@ follows `Semantic versioning`_ Development version ------------------- -- No featues. +- Add functionality for decorating classes with ``@di.inject``. 0.9.5 ----- diff --git a/examples/advanced_usage/inject_decorator_flask.py b/examples/advanced_usage/inject_flask.py similarity index 100% rename from examples/advanced_usage/inject_decorator_flask.py rename to examples/advanced_usage/inject_flask.py diff --git a/examples/advanced_usage/inject_flask_class_based.py b/examples/advanced_usage/inject_flask_class_based.py new file mode 100644 index 00000000..09086417 --- /dev/null +++ b/examples/advanced_usage/inject_flask_class_based.py @@ -0,0 +1,42 @@ +"""`@di.inject()` decorator with classes example.""" + +import sqlite3 +import flask +import flask.views +import dependency_injector as di + + +database = di.Singleton(sqlite3.Connection, + database=':memory:', + timeout=30, + detect_types=True, + isolation_level='EXCLUSIVE') + +app = flask.Flask(__name__) + + +@di.inject(database=database) +@di.inject(some_setting=777) +class HelloView(flask.views.View): + + """Example flask class-based view.""" + + def __init__(self, database, some_setting): + """Initializer.""" + self.database = database + self.some_setting = some_setting + + def dispatch_request(self): + """Handle example request.""" + one = self.database.execute('SELECT 1').fetchone()[0] + one *= self.some_setting + return 'Query returned {0}, db connection {1}'.format(one, database) + + +app.add_url_rule('/', view_func=HelloView.as_view('hello_view')) + +if __name__ == '__main__': + app.run() + +# Example output of "GET / HTTP/1.1" is: +# Query returned 777, db connection diff --git a/examples/advanced_usage/inject_decorator_simple.py b/examples/advanced_usage/inject_simple.py similarity index 100% rename from examples/advanced_usage/inject_decorator_simple.py rename to examples/advanced_usage/inject_simple.py diff --git a/tests/test_injections.py b/tests/test_injections.py index ad17b533..a3bb0903 100644 --- a/tests/test_injections.py +++ b/tests/test_injections.py @@ -151,3 +151,47 @@ class InjectTests(unittest.TestCase): def test_decorate_with_not_injection(self): """Test `inject()` decorator with not an injection instance.""" self.assertRaises(di.Error, di.inject, object) + + def test_decorate_class_method(self): + """Test `inject()` decorator with class method.""" + class Test(object): + + """Test class.""" + + @di.inject(arg1=123) + @di.inject(arg2=456) + def some_method(self, arg1, arg2): + """Some test method.""" + return arg1, arg2 + + test_object = Test() + arg1, arg2 = test_object.some_method() + + self.assertEquals(arg1, 123) + self.assertEquals(arg2, 456) + + def test_decorate_class_with_init(self): + """Test `inject()` decorator that decorate class with __init__.""" + @di.inject(arg1=123) + @di.inject(arg2=456) + class Test(object): + + """Test class.""" + + def __init__(self, arg1, arg2): + """Init.""" + self.arg1 = arg1 + self.arg2 = arg2 + + test_object = Test() + + self.assertEquals(test_object.arg1, 123) + self.assertEquals(test_object.arg2, 456) + + def test_decorate_class_without_init(self): + """Test `inject()` decorator that decorate class without __init__.""" + with self.assertRaises(di.Error): + @di.inject(arg1=123) + class Test(object): + + """Test class."""