Skip to content
Permalink
Browse files

Introduce new, Python 3-only way to declare things to inject

This pattern makes it more obvious where do we actually expect
parameters to be injected and where do we expect them to be passed the
regular way (Injector(use_annotations=True) nas a semi-global meaning
while @Inject still covers only a single function).
  • Loading branch information
jstasiak committed Oct 18, 2016
1 parent 7af3604 commit a1a9164539cfaf880612993d79298d73a8abd09f
Showing with 213 additions and 81 deletions.
  1. +28 −2 CHANGES
  2. +6 −6 README.md
  3. +2 −2 docs/faq.rst
  4. +17 −12 docs/practices.rst
  5. +7 −8 docs/terminology.rst
  6. +60 −47 injector.py
  7. +93 −4 injector_test_py3.py
30 CHANGES
@@ -4,6 +4,32 @@ Injector Change Log
0.11.0 (not released yet)
-------------------------

* The following way to declare dependencies is introduced and recommended
now:

class SomeClass:
@inject
def __init__(self, other: OtherClass):
# ...

The following ways are still supported but are deprecated and will be
removed in the future:

# Python 2-compatible style
class SomeClass
@inject(other=OtherClass)
def __init__(self, other):
# ...

# Python 3 style without @inject-decoration but with use_annotations
class SomeClass:
def __init__(self, other: OtherClass):
# ...

injector = Injector(use_annotations=True)
# ...


Backwards incompatible:

* Removed support for decorating classes with @inject. Previously:
@@ -15,8 +41,8 @@ Backwards incompatible:
Now:

class Class:
@inject(something=Something)
def __init__(self, something):
@inject
def __init__(self, something: Something):
self.something = something

* Removed support for injecting partially applied functions, previously:
@@ -37,8 +37,8 @@ A Quick Example
... self.forty_two = 42
...
>>> class Outer(object):
... @inject(inner=Inner)
... def __init__(self, inner):
... @inject
... def __init__(self, inner: Inner):
... self.inner = inner
...
>>> injector = Injector()
@@ -72,8 +72,8 @@ And make up an imaginary `RequestHandler` class that uses the SQLite connection:

```python
>>> class RequestHandler(object):
... @inject(db=sqlite3.Connection)
... def __init__(self, db):
... @inject
... def __init__(self, db: sqlite3.Connection):
... self._db = db
...
... def get(self):
@@ -108,8 +108,8 @@ Next we create a module that initialises the DB. It depends on the configuration
>>> class DatabaseModule(Module):
... @singleton
... @provides(sqlite3.Connection)
... @inject(configuration=Configuration)
... def provide_sqlite_connection(self, configuration):
... @inject
... def provide_sqlite_connection(self, configuration: Configuration):
... conn = sqlite3.connect(configuration['db_connection_string'])
... cursor = conn.cursor()
... cursor.execute('CREATE TABLE IF NOT EXISTS data (key PRIMARY KEY, value)')
@@ -17,8 +17,8 @@ Example code:
.. code-block:: python
class X(object):
@inject(s=str)
def __init__(self, s):
@inject
def __init__(self, s: str):
self.s = s
def configure(binder):
@@ -23,6 +23,11 @@ Injecting into constructors vs injecting into other methods
Injector 0.11+ doesn't support injecting into non-constructor methods,
this section is kept for historical reasons.

.. note::

Injector 0.11 deprecates using @inject with keyword arguments to declare
bindings, this section remains unchanged for historical reasons.

In general you should prefer injecting into constructors to injecting into
other methods because:

@@ -130,8 +135,8 @@ As an illustration:
class BadModule(Module):
@provides(A)
@inject(suba=SubA)
def provide_a(self, suba):
@inject
def provide_a(self, suba: SubA):
return suba
@provides(SubA)
@@ -185,8 +190,8 @@ Sometimes code like this is written:
pass
class C(object):
@inject(injector=Injector)
def __init__(self, injector):
@inject
def __init__(self, injector: Injector):
self.a = injector.get(A)
self.b = injector.get(B)
@@ -202,8 +207,8 @@ It is advised to use the following pattern instead:
pass
class C(object):
@inject(a=A, b=B)
def __init__(self, a, b):
@inject
def __init__(self, a: A, b: B):
self.a = a
self.b = b
@@ -232,8 +237,8 @@ A pattern similar to the one below can emerge:
self.a = a
class C(object):
@inject(a=A)
def __init__(self, a):
@inject
def __init__(self, a: A):
self.b = B(a)
Class ``C`` in this example has the responsibility of gathering dependencies of
@@ -249,11 +254,11 @@ The appropriate pattern is:
pass
class B(object):
@inject(a=A)
def __init__(self, a):
@inject
def __init__(self, a: A):
self.a = a
class C(object):
@inject(b=B)
def __init__(self, b):
@inject
def __init__(self, b: B):
self.b = b
@@ -90,8 +90,8 @@ Here is an example of injection on a module provider method, and on the construc
from injector import inject

class User(object):
@inject(name=Name, description=Description)
def __init__(self, name, description):
@inject
def __init__(self, name: Name, description: Description):
self.name = name
self.description = description

@@ -106,8 +106,8 @@ Here is an example of injection on a module provider method, and on the construc
binder.bind(Name, to='Sherlock')

@provides(Description)
@inject(name=Name)
def describe(self, name):
@inject
def describe(self, name: Name):
return '%s is a man of astounding insight' % name


@@ -154,8 +154,7 @@ Sometimes there are classes that have injectable and non-injectable parameters i


class UserUpdater(object):
@inject(db=Database)
def __init__(self, db, user):
def __init__(self, db: Database, user):
pass

You may want to have database connection `db` injected into `UserUpdater` constructor, but in the same time provide `user` object by yourself, and assuming that `user` object is a value object and there's many users in your application it doesn't make much sense to inject objects of class `User`.
@@ -174,8 +173,8 @@ This way we don't get `UserUpdater` directly but rather a builder object. Such b
else, if you need instance of it you just ask for it like that::

class NeedsUserUpdater(object):
@inject(updater_builder=ClassAssistedBuilder[UserUpdater])
def __init__(self, builder):
@inject
def __init__(self, builder: ClassAssistedBuilder[UserUpdater]):
self.updater_builder = builder

def method(self):
@@ -720,8 +720,8 @@ def create_object(self, cls, additional_kwargs=None):
# where object.__init__ is a slot wrapper that can't be inspected
if self.use_annotations and hasattr(cls, '__init__') and \
not hasattr(cls.__init__, '__binding__') and \
cls.__init__ is not object.__init__:
bindings = self._infer_injected_bindings(cls.__init__)
cls.__init__ is not object.__init__ and self.use_annotations:
bindings = _infer_injected_bindings(cls.__init__)
else:
bindings = {}

@@ -755,18 +755,6 @@ def create_object(self, cls, additional_kwargs=None):
if installed:
self._uninstall_from(instance)

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

# We don't care about the return value annotation as it doesn't matter
# injection-wise.
bindings.pop('return', None)

return bindings

def install_into(self, instance):
"""Put injector reference in given object.
@@ -903,6 +891,19 @@ def repr_key(k):
return dependencies


def _infer_injected_bindings(callable):
if not getfullargspec:
return None
spec = getfullargspec(callable)
bindings = dict(spec.annotations.items())

# We don't care about the return value annotation as it doesn't matter
# injection-wise.
bindings.pop('return', None)

return bindings


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.
@@ -950,14 +951,21 @@ def wrapper(provider):
getfullargspec = None


def inject(**bindings):
def inject(function=None, **bindings):
"""Decorator declaring parameters to be injected.
eg.
>>> Sizes = Key('sizes')
>>> Names = Key('names')
>>> # Recommended, Python 3+ style
>>> class A:
... @inject
... def __init__(self, number: int, name: str, sizes: Sizes):
... print([number, name, sizes])
>>> # Or older, Python 2-compatible style
>>> class A(object):
... @inject(number=int, name=str, sizes=Sizes)
... def __init__(self, number, name, sizes):
@@ -974,48 +982,53 @@ def inject(**bindings):
>>> a = Injector(configure).get(A)
[123, 'Bob', [1, 2, 3]]
"""

def method_wrapper(f):
for key, value in bindings.items():
bindings[key] = BindingKey(value)
argspec = getargspec(f)
if argspec.args and argspec.args[0] == 'self':
@functools.wraps(f)
def inject(self_, *args, **kwargs):
injector = getattr(self_, '__injector__', None)
if injector:
return injector.call_with_injection(
callable=f,
self_=self_,
args=args,
kwargs=kwargs
)
else:
return f(self_, *args, **kwargs)

# Propagate @provides bindings to wrapper function
if hasattr(f, '__binding__'):
inject.__binding__ = f.__binding__
else:
inject = f

function_bindings = getattr(f, '__bindings__', None) or {}
merged_bindings = dict(function_bindings, **bindings)

f.__bindings__ = merged_bindings
inject.__bindings__ = merged_bindings
return inject
if function:
assert not bindings, 'You can only pass either function or bindings here'
bindings = _infer_injected_bindings(function)
return method_wrapper(function, bindings)

def multi_wrapper(something):
if isinstance(something, type):
raise TypeError('Decorating classes with @inject is no longer supported, ' +
'provide constructor and decorate it')
else:
return method_wrapper(something)
return method_wrapper(something, bindings)

return multi_wrapper


def method_wrapper(f, bindings):
for key, value in bindings.items():
bindings[key] = BindingKey(value)
argspec = getargspec(f)
if argspec.args and argspec.args[0] == 'self':
@functools.wraps(f)
def inject(self_, *args, **kwargs):
injector = getattr(self_, '__injector__', None)
if injector:
return injector.call_with_injection(
callable=f,
self_=self_,
args=args,
kwargs=kwargs
)
else:
return f(self_, *args, **kwargs)

# Propagate @provides bindings to wrapper function
if hasattr(f, '__binding__'):
inject.__binding__ = f.__binding__
else:
inject = f

function_bindings = getattr(f, '__bindings__', None) or {}
merged_bindings = dict(function_bindings, **bindings)

f.__bindings__ = merged_bindings
inject.__bindings__ = merged_bindings
return inject


@private
class BaseKey(object):
"""Base type for binding keys."""

0 comments on commit a1a9164

Please sign in to comment.
You can’t perform that action at this time.