Skip to content
Permalink
Browse files
Add support for using Py3 annotations in lieu of @Inject.
  • Loading branch information
alecthomas committed Aug 9, 2013
1 parent 2e7485f commit 7c1aa98aeaab405c2d5a7f9c4ce5926766ec684b
Showing with 94 additions and 9 deletions.
  1. +1 −1 .travis.yml
  2. +6 −0 CHANGES.md
  3. +23 −0 README.md
  4. +12 −0 conftest.py
  5. +16 −5 injector.py
  6. +33 −0 injector_test_py3.py
  7. +3 −3 pytest.ini
@@ -6,5 +6,5 @@ python:
- "3.3"
- "pypy"
install:
- pip install pytest
- pip install pytest --use-mirrors
script: py.test -vv
@@ -0,0 +1,6 @@
# ChangeLog for Injector

## 0.7.4

- Added a ChangeLog!
- Added support for using Python3 annotations as binding types.
@@ -20,6 +20,29 @@ Injector work with the following Python interpreters:
- CPython 2.6, 2.7, 3.2, 3.3
- PyPy 1.9

Recent Notable Changes
----------------------
Added support for using Python3 annotations instead of @inject.

eg. The following code:

```python
class B(object):
@inject(a=A):
def __init__(self, a):
self.a = a
```

Can now be written as:

```python3
class B(object):
def __init__(self, a:A):
self.a = a
```

To enable this support, instantiate your `Injector` with `Injector(..., use_annotations=True)`

A Quick Example
---------------

@@ -0,0 +1,12 @@
import sys
import os.path

test_sources = ['injector.py', 'injector_test.py', 'README.md']


if sys.version_info[0] >= 3:
test_sources.append('injector_test_py3.py')


def pytest_ignore_collect(path, config):
return not os.path.basename(str(path)) in test_sources
@@ -34,7 +34,7 @@ def emit(self, record):
pass

__author__ = 'Alec Thomas <alec@swapoff.org>'
__version__ = '0.7.4'
__version__ = '0.7.5'
__version_tag__ = ''

log = logging.getLogger('injector')
@@ -494,7 +494,7 @@ def configure(self, binder):
class Injector(object):
"""Initialise and use an object dependency graph."""

def __init__(self, modules=None, auto_bind=True, parent=None):
def __init__(self, modules=None, auto_bind=True, parent=None, use_annotations=False):
"""Construct a new Injector.
:param modules: A callable, class, or list of callables/classes, used to configure the
@@ -507,12 +507,14 @@ def __init__(self, modules=None, auto_bind=True, parent=None):
Signature is ``configure(binder)``.
:param auto_bind: Whether to automatically bind missing types.
:param parent: Parent injector.
:param use_annotations: Attempt to infer injected arguments using Python3 argument annotations.
"""
# Stack of keys currently being injected. Used to detect circular
# dependencies.
self._stack = []

self.parent = parent
self.use_annotations = use_annotations

# Binder
self.binder = Binder(self, auto_bind=auto_bind, parent=parent and parent.binder)
@@ -574,6 +576,10 @@ def create_object(self, cls, additional_kwargs=None):
additional_kwargs = additional_kwargs or {}
log.debug('%sCreating %r object with %r', self._log_prefix, cls, additional_kwargs)

if self.use_annotations and hasattr(cls, '__init__') and not hasattr(cls.__init__, '__binding__'):
bindings = self._infer_injected_bindings(cls.__init__)
cls.__init__ = inject(**bindings)(cls.__init__)

instance = cls.__new__(cls)
try:
self.install_into(instance)
@@ -593,6 +599,12 @@ def create_object(self, cls, additional_kwargs=None):
)
return instance

def _infer_injected_bindings(self, callable):
if not getfullargspec or not self.use_annotations:
return None
spec = getfullargspec(callable)
return dict(spec.annotations.items())

def install_into(self, instance):
"""
Puts injector reference in given object.
@@ -716,11 +728,10 @@ def extends(interface, annotation=None, scope=None):


if hasattr(inspect, 'getfullargspec'):
getargspec = inspect.getfullargspec
getfullargspec = getargspec = inspect.getfullargspec
else:
getargspec = inspect.getargspec


getfullargspec = None


def inject(**bindings):
@@ -0,0 +1,33 @@
import pytest

from injector import Injector, CallError


def test_implicit_injection_for_python3():
class A(object):
def __init__(self):
pass

class B(object):
def __init__(self, a:A):
self.a = a

class C(object):
def __init__(self, b:B):
self.b = b

injector = Injector(use_annotations=True)
c = injector.get(C)
assert isinstance(c, C)
assert isinstance(c.b, B)
assert isinstance(c.b.a, A)


def test_implicit_injection_fails_when_annotations_are_missing():
class A(object):
def __init__(self, n):
self.n = n

injector = Injector(use_annotations=True)
with pytest.raises(CallError):
injector.get(A)
@@ -1,4 +1,4 @@
[pytest]
python_files = injector_test.py
addopts = -v --tb=native --doctest-glob=*.md --doctest-modules injector.py injector_test.py README.md
norecursedirs = __pycache__ *venv* .git
python_files = injector_test.py injector_test_py3.py
addopts = -v --tb=native --doctest-glob=*.md --doctest-modules
norecursedirs = __pycache__ *venv* .git build

0 comments on commit 7c1aa98

Please sign in to comment.