From 1c55f9d64573b2869bdb8e375f8436956c3ed7e1 Mon Sep 17 00:00:00 2001 From: Roman Mogilatov Date: Mon, 28 Sep 2015 14:19:22 +0300 Subject: [PATCH 1/4] Add functionality for @inject decorator to work with classes --- dependency_injector/injections.py | 16 ++++++++++- tests/test_injections.py | 44 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/dependency_injector/injections.py b/dependency_injector/injections.py index 747babcb..7fc800c2 100644 --- a/dependency_injector/injections.py +++ b/dependency_injector/injections.py @@ -6,6 +6,8 @@ from .utils import is_provider from .utils import ensure_is_injection from .utils import get_injectable_kwargs +from .errors import Error + class Injection(object): @@ -60,8 +62,20 @@ def inject(*args, **kwargs): injections += tuple(ensure_is_injection(injection) for injection in args) - def decorator(callback): + def decorator(callback, cls=None): """Dependency injection decorator.""" + if isinstance(callback, six.class_types): + cls = callback + try: + cls_init = six.get_unbound_function(getattr(cls, '__init__')) + except AttributeError: + raise Error( + 'Class {0} has no __init__() '.format(cls.__module__, + cls.__name__) + + 'method and could not be decorated with @inject decorator') + cls.__init__ = decorator(cls_init) + return cls + if hasattr(callback, 'injections'): callback.injections += injections return callback 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.""" From 2c85b3811348f3e7ab926b2fe1286ab183501648 Mon Sep 17 00:00:00 2001 From: Roman Mogilatov Date: Mon, 28 Sep 2015 14:32:07 +0300 Subject: [PATCH 2/4] Fix bug with default object.__init__ in Py3 --- dependency_injector/injections.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dependency_injector/injections.py b/dependency_injector/injections.py index 7fc800c2..90a5f31a 100644 --- a/dependency_injector/injections.py +++ b/dependency_injector/injections.py @@ -67,8 +67,9 @@ def inject(*args, **kwargs): if isinstance(callback, six.class_types): cls = callback try: - cls_init = six.get_unbound_function(getattr(cls, '__init__')) - except AttributeError: + cls_init = six.get_unbound_function(cls.__init__) + assert cls_init is not object.__init__ + except (AttributeError, AssertionError): raise Error( 'Class {0} has no __init__() '.format(cls.__module__, cls.__name__) + From 512544ea9f137a5ef92f74e87d952fa736a79fff Mon Sep 17 00:00:00 2001 From: Roman Mogilatov Date: Mon, 28 Sep 2015 18:45:15 +0300 Subject: [PATCH 3/4] Add fix for @inject decorator with classes on PyPy and Py3 --- dependency_injector/injections.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/dependency_injector/injections.py b/dependency_injector/injections.py index 90a5f31a..9e282907 100644 --- a/dependency_injector/injections.py +++ b/dependency_injector/injections.py @@ -1,5 +1,6 @@ """Injections module.""" +import sys import six from .utils import is_provider @@ -9,6 +10,13 @@ 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): """Base injection class.""" @@ -68,11 +76,11 @@ def inject(*args, **kwargs): cls = callback try: cls_init = six.get_unbound_function(cls.__init__) - assert cls_init is not object.__init__ + assert cls_init is not OBJECT_INIT except (AttributeError, AssertionError): raise Error( - 'Class {0} has no __init__() '.format(cls.__module__, - cls.__name__) + + '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 From 94b2dee48adf2f8541f7ccb132d4715358d61ed9 Mon Sep 17 00:00:00 2001 From: Roman Mogilatov Date: Mon, 28 Sep 2015 21:56:36 +0300 Subject: [PATCH 4/4] Add docs for usage of @inject decorator with classes --- docs/advanced_usage/index.rst | 17 +++++++- docs/main/changelog.rst | 2 +- ...ect_decorator_flask.py => inject_flask.py} | 0 .../inject_flask_class_based.py | 42 +++++++++++++++++++ ...t_decorator_simple.py => inject_simple.py} | 0 5 files changed, 58 insertions(+), 3 deletions(-) rename examples/advanced_usage/{inject_decorator_flask.py => inject_flask.py} (100%) create mode 100644 examples/advanced_usage/inject_flask_class_based.py rename examples/advanced_usage/{inject_decorator_simple.py => inject_simple.py} (100%) diff --git a/docs/advanced_usage/index.rst b/docs/advanced_usage/index.rst index 148d6d29..73459040 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's ``__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..f843c622 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -11,7 +11,7 @@ follows `Semantic versioning`_ Development version ------------------- -- No featues. +- Add ``@di.inject`` functionality for decorating classes. 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