diff --git a/.travis.yml b/.travis.yml index fb534dd..bdab40c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ matrix: include: - { python: "3.7", dist: xenial, sudo: true } install: - - pip install --upgrade coveralls pytest "typing$TYPING_VERSION" "pytest-cov>=2.5.1" + - pip install --upgrade coveralls pytest "typing$TYPING_VERSION" "pytest-cov>=2.5.1" dataclasses # mypy can't be installed on pypy - if [[ "${TRAVIS_PYTHON_VERSION}" != "pypy"* ]] ; then pip install mypy ; fi # Black is Python 3.6+-only diff --git a/CHANGES b/CHANGES index 0976c6b..619c750 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,13 @@ Injector Change Log =================== +0.16.2 +------ + +- (Re)added support for decorating classes themselves with :func:`@inject `. This is the same + as decorating their constructors. Among other things this gives us + `dataclasses `_ integration. + 0.16.1 ------ diff --git a/README.md b/README.md index cefc014..b9cf62c 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,26 @@ A Quick Example ``` +Or with `dataclasses` if you like: + +```python +from dataclasses import dataclass +from injector import Injector, inject +class Inner: + def __init__(self): + self.forty_two = 42 + +@inject +@dataclass +class Outer: + inner: Inner + +injector = Injector() +outer = injector.get(Outer) +print(outer.inner.forty_two) # Prints 42 +``` + + A Full Example -------------- diff --git a/injector/__init__.py b/injector/__init__.py index 5337da7..3fb9be0 100644 --- a/injector/__init__.py +++ b/injector/__init__.py @@ -899,7 +899,7 @@ def provider(function): return function -def inject(function): +def inject(constructor_or_class): """Decorator declaring parameters to be injected. eg. @@ -923,15 +923,40 @@ def inject(function): >>> a = Injector(configure).get(A) [123, 'Bob', [1, 2, 3]] + As a convenience one can decorate a class itself: + + >>> @inject + ... class B: + ... def __init__(self, dependency: Dependency): + ... self.dependency = dependency + + This is equivalent to decorating its constructor. In particular this provides integration with + `dataclasses `_ (the order of decorator + application is important here): + + >>> @inject + ... @dataclass + ... class C: + ... dependency: Dependency + .. note:: - This decorator is to be used on class constructors. Using it on non-constructor - methods worked in the past but it was an implementation detail rather than - a design decision. + This decorator is to be used on class constructors (or, as a convenience, on classes). + Using it on non-constructor methods worked in the past but it was an implementation + detail rather than a design decision. Third party libraries may, however, provide support for injecting dependencies into non-constructor methods or free functions in one form or another. + + .. versionchanged:: 0.16.2 + + (Re)added support for decorating classes with @inject. """ + if isinstance(constructor_or_class, type) and hasattr(constructor_or_class, '__init__'): + inject(constructor_or_class.__init__) + return constructor_or_class + + function = constructor_or_class try: bindings = _infer_injected_bindings(function) except _BindingNotYetAvailable: diff --git a/injector_test.py b/injector_test.py index 2d7eb2a..3175677 100644 --- a/injector_test.py +++ b/injector_test.py @@ -13,6 +13,7 @@ from contextlib import contextmanager from typing import Any, NewType import abc +import sys import threading import traceback import warnings @@ -1393,3 +1394,26 @@ def test_newtype_integration_works(): injector = Injector([configure]) assert injector.get(UserID) == 123 + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason="Requires Python 3.6+") +def test_dataclass_integration_works(): + import dataclasses + + # Python 3.6+-only syntax below + exec( + """ +@inject +@dataclasses.dataclass +class Data: + name: str + """, + locals(), + globals(), + ) + + def configure(binder): + binder.bind(str, to='data') + + injector = Injector([configure]) + assert injector.get(Data).name == 'data'