Make (Class)AssistedBuilder and ProviderOf generic classes

This doesn't provide much benefit to Injector itself but now
(Class)AssistedBuilder[T] and ProviderOf can be used in type hints[1]
like this:

    class Class:
        def __init__(self, builder: AssistedBuilder[OtherClass]):
            # ...

By being able to do that projects using Injector will gain more static
type safety when tool like mypy is used to lint the code.

Python 2.6 support is dropped because the typing module requires Python
2.7 or newer.

[1] https://docs.python.org/3/library/typing.html
This commit is contained in:
Jakub Stasiak 2016-10-18 13:11:27 +02:00
parent 33bc8f336b
commit 2b14948aa7
7 changed files with 68 additions and 66 deletions

View File

@ -1,7 +1,6 @@
sudo: false
language: python
python:
- "2.6"
- "2.7"
- "3.2"
- "3.3"
@ -12,5 +11,5 @@ python:
- "pypy"
- "pypy3"
install:
- pip install pytest
- pip install pytest typing
script: py.test -vv

14
CHANGES
View File

@ -34,6 +34,20 @@ Backwards incompatible:
Now you need to move the function with injectable dependencies to a class.
* Removed support for getting AssistedBuilder(callable=...)
* Dropped Python 2.6 support
* Changed the way AssistedBuilder and ProviderOf are used.
Previously:
builder1 = injector.get(AssistedBuilder(Something))
# or: builder1 = injector.get(AssistedBuilder(interface=Something))
builder2 = injector.get(AssistedBuilder(cls=SomethingElse))
provider = injector.get(ProviderOf(SomeOtherThing))
Now:
builder1 = injector.get(AssistedBuilder[Something])
builder2 = injector.get(ClassAssistedBuilder[cls=SomethingElse])
provider = injector.get(ProviderOf[SomeOtherThing])
0.10.1
------

View File

@ -24,7 +24,7 @@ While being inspired by Guice, it does not slavishly replicate its API. Providin
* Documentation: http://injector.readthedocs.org
Injector works with CPython 2.6+/3.2+ and PyPy 1.9+.
Injector works with CPython 2.7/3.2+ and PyPy 1.9+.
A Quick Example
---------------

View File

@ -162,27 +162,28 @@ You may want to have database connection `db` injected into `UserUpdater` constr
In this situation there's technique called Assisted injection::
from injector import AssistedBuilder
from injector import ClassAssistedBuilder
injector = Injector()
builder = injector.get(AssistedBuilder(cls=UserUpdater))
builder = injector.get(ClassAssistedBuilder[UserUpdater])
user = User('John')
user_updater = builder.build(user=user)
This way we don't get `UserUpdater` directly but rather a builder object. Such builder has `build(**kwargs)` method which takes non-injectable parameters, combines them with injectable dependencies of `UserUpdater` and calls `UserUpdater` initializer using all of them.
`AssistedBuilder(...)` is injectable just as anything else, if you need instance of it you just ask for it like that::
`AssistedBuilder[T]` and `ClassAssistedBuilder[T]` are injectable just as anything
else, if you need instance of it you just ask for it like that::
class NeedsUserUpdater(object):
@inject(updater_builder=AssistedBuilder(cls=UserUpdater))
@inject(updater_builder=ClassAssistedBuilder[UserUpdater])
def __init__(self, builder):
self.updater_builder = builder
def method(self):
updater = self.updater_builder.build(user=None)
`cls` needs to be a concrete class and no bindings will be used.
`ClassAssistedBuilder` means it'll construct a concrete class and no bindings will be used.
If you want `AssistedBuilder` to follow bindings and construct class pointed to by a key you can do it like this::
If you want to follow bindings and construct class pointed to by a key you use `AssistedBuilder` and can do it like this::
>>> DB = Key('DB')
>>> class DBImplementation(object):
@ -193,12 +194,10 @@ If you want `AssistedBuilder` to follow bindings and construct class pointed to
... binder.bind(DB, to=DBImplementation)
...
>>> injector = Injector(configure)
>>> builder = injector.get(AssistedBuilder(interface=DB))
>>> builder = injector.get(AssistedBuilder[DB])
>>> isinstance(builder.build(uri='x'), DBImplementation)
True
Note: ``AssistedBuilder(X)`` is a shortcut for ``AssistedBuilder(interface=X)``
More information on this topic:
- `"How to use Google Guice to create objects that require parameters?" on Stack Overflow <http://stackoverflow.com/questions/996300/how-to-use-google-guice-to-create-objects-that-require-parameters>`_

View File

@ -24,6 +24,7 @@ import types
import warnings
from abc import ABCMeta, abstractmethod
from collections import namedtuple
from typing import Generic, TypeVar
try:
@ -398,11 +399,11 @@ class Binder(object):
return Binding(interface, provider, scope)
def provider_for(self, interface, to=None):
if isinstance(interface, ProviderOf):
if isinstance(interface, type) and issubclass(interface, ProviderOf):
(target,) = interface.__args__
if to is not None:
raise Exception('ProviderOf cannot be bound to anything')
return InstanceProvider(
BoundProvider(self.injector, interface.interface))
return InstanceProvider(ProviderOf(self.injector, target))
elif isinstance(to, Provider):
return to
elif isinstance(interface, Provider):
@ -418,8 +419,9 @@ class Binder(object):
def proxy(**kwargs):
return interface.interface(**kwargs)
return CallableProvider(proxy)
elif isinstance(interface, AssistedBuilder):
builder = AssistedBuilderImplementation(self.injector, *interface)
elif isinstance(interface, type) and issubclass(interface, AssistedBuilder):
(target,) = interface.__args__
builder = interface(self.injector, target)
return InstanceProvider(builder)
elif isinstance(interface, (tuple, type)) and isinstance(to, interface):
return InstanceProvider(to)
@ -459,7 +461,7 @@ class Binder(object):
# "Special" interfaces are ones that you cannot bind yourself but
# you can request them (for example you cannot bind ProviderOf(SomeClass)
# to anything but you can inject ProviderOf(SomeClass) just fine
return isinstance(interface, (ProviderOf, AssistedBuilder))
return issubclass(interface, (ProviderOf, AssistedBuilder))
class Scope(object):
@ -1101,30 +1103,18 @@ class BoundKey(tuple):
return dict(self[1])
class AssistedBuilder(namedtuple('_AssistedBuilder', 'interface cls')):
def __new__(cls_, interface=None, cls=None):
if len([x for x in (interface, cls) if x is not None]) != 1:
raise Error('You need to specify exactly one of the following '
'arguments: interface or cls')
return super(AssistedBuilder, cls_).__new__(cls_, interface, cls)
T = TypeVar('T')
class AssistedBuilderImplementation(namedtuple(
'_AssistedBuilderImplementation', 'injector interface cls')):
class AssistedBuilder(Generic[T]):
def __init__(self, injector, target):
self._injector = injector
self._target = target
def build(self, **kwargs):
if self.interface is not None:
return self.build_interface(**kwargs)
else:
return self.build_class(self.cls, **kwargs)
def build_class(self, cls, **kwargs):
return self.injector.create_object(cls, additional_kwargs=kwargs)
def build_interface(self, **kwargs):
key = BindingKey(self.interface)
binder = self.injector.binder
key = BindingKey(self._target)
binder = self._injector.binder
binding = binder.get_binding(None, key)
provider = binding.provider
if not isinstance(provider, ClassProvider):
@ -1132,7 +1122,15 @@ class AssistedBuilderImplementation(namedtuple(
'Assisted interface building works only with ClassProviders, '
'got %r for %r' % (provider, self.interface))
return self.build_class(provider._cls, **kwargs)
return self._build_class(provider._cls, **kwargs)
def _build_class(self, cls, **kwargs):
return self._injector.create_object(cls, additional_kwargs=kwargs)
class ClassAssistedBuilder(AssistedBuilder[T]):
def build(self, **kwargs):
return self._build_class(self._target, **kwargs)
def _describe(c):
@ -1143,8 +1141,8 @@ def _describe(c):
return str(c)
class ProviderOf(object):
"""Can be used to get a :class:`BoundProvider` of an interface, for example:
class ProviderOf(Generic[T]):
"""Can be used to get a provider of an interface, for example:
>>> def provide_int():
... print('providing')
@ -1154,22 +1152,13 @@ class ProviderOf(object):
... binder.bind(int, to=provide_int)
>>>
>>> injector = Injector(configure)
>>> provider = injector.get(ProviderOf(int))
>>> type(provider)
<class 'injector.BoundProvider'>
>>> provider = injector.get(ProviderOf[int])
>>> value = provider.get()
providing
>>> value
123
"""
def __init__(self, interface):
self.interface = interface
class BoundProvider(object):
"""A :class:`Provider` tied with particular :class:`Injector` instance"""
def __init__(self, injector, interface):
self._injector = injector
self._interface = interface

View File

@ -23,7 +23,7 @@ from injector import (
inject, singleton, threadlocal, UnsatisfiedRequirement,
CircularDependency, Module, provides, Key, SingletonScope,
ScopeDecorator, with_injector, AssistedBuilder, BindingKey,
SequenceKey, MappingKey, ProviderOf,
SequenceKey, MappingKey, ProviderOf, ClassAssistedBuilder,
)
@ -150,7 +150,7 @@ def test_providers_arent_called_for_dependencies_that_are_already_provided():
pass
injector = Injector(configure)
builder = injector.get(AssistedBuilder(A))
builder = injector.get(AssistedBuilder[A])
with pytest.raises(ZeroDivisionError):
builder.build()
@ -383,7 +383,7 @@ def test_dependency_cycle_can_be_worked_broken_by_assisted_building():
self.i = i
class B(object):
@inject(a_builder=AssistedBuilder(A))
@inject(a_builder=AssistedBuilder[A])
def __init__(self, a_builder):
self.a = a_builder.build(i=self)
@ -820,14 +820,14 @@ class NeedsAssistance(object):
def test_assisted_builder_works_when_got_directly_from_injector():
injector = Injector()
builder = injector.get(AssistedBuilder(NeedsAssistance))
builder = injector.get(AssistedBuilder[NeedsAssistance])
obj = builder.build(b=123)
assert ((obj.a, obj.b) == (str(), 123))
def test_assisted_builder_works_when_injected():
class X(object):
@inject(builder=AssistedBuilder(NeedsAssistance))
@inject(builder=AssistedBuilder[NeedsAssistance])
def __init__(self, builder):
self.obj = builder.build(b=234)
@ -843,7 +843,7 @@ def test_assisted_builder_uses_bindings():
binder.bind(Interface, to=NeedsAssistance)
injector = Injector(configure)
builder = injector.get(AssistedBuilder(Interface))
builder = injector.get(AssistedBuilder[Interface])
x = builder.build(b=333)
assert ((type(x), x.b) == (NeedsAssistance, 333))
@ -857,25 +857,25 @@ def test_assisted_builder_uses_concrete_class_when_specified():
binder.bind(X, to=lambda: 1 / 0)
injector = Injector(configure)
builder = injector.get(AssistedBuilder(cls=X))
builder = injector.get(ClassAssistedBuilder[X])
builder.build()
def test_assisted_builder_injection_is_safe_to_use_with_multiple_injectors():
class X(object):
@inject(builder=AssistedBuilder(NeedsAssistance))
@inject(builder=AssistedBuilder[NeedsAssistance])
def y(self, builder):
return builder
i1, i2 = Injector(), Injector()
b1 = i1.get(X).y()
b2 = i2.get(X).y()
assert ((b1.injector, b2.injector) == (i1, i2))
assert ((b1._injector, b2._injector) == (i1, i2))
def test_assisted_builder_injection_uses_the_same_binding_key_every_time():
# if we have different BindingKey for every AssistedBuilder(...) we will get memory leak
gen_key = lambda: BindingKey(AssistedBuilder(NeedsAssistance))
gen_key = lambda: BindingKey(AssistedBuilder[NeedsAssistance])
assert gen_key() == gen_key()
@ -1018,7 +1018,7 @@ def test_providerof():
assert counter[0] == 0
provider = injector.get(ProviderOf(str))
provider = injector.get(ProviderOf[str])
assert counter[0] == 0
assert provider.get() == 'content'
@ -1030,7 +1030,7 @@ def test_providerof():
def test_providerof_cannot_be_bound():
def configure(binder):
binder.bind(ProviderOf(int), to=InstanceProvider(None))
binder.bind(ProviderOf[int], to=InstanceProvider(None))
with pytest.raises(Exception):
Injector(configure)
@ -1046,7 +1046,7 @@ def test_providerof_is_safe_to_use_with_multiple_injectors():
injector1 = Injector(configure1)
injector2 = Injector(configure2)
provider_of = ProviderOf(int)
provider_of = ProviderOf[int]
provider1 = injector1.get(provider_of)
provider2 = injector2.get(provider_of)
@ -1075,10 +1075,10 @@ def test_special_interfaces_work_with_auto_bind_disabled():
# raise UnsatisfiedRequirement(cls, key)
# UnsatisfiedRequirement: unsatisfied requirement on
# <injector.ProviderOf object at 0x10ff01550>
injector.get(ProviderOf(InjectMe))
injector.get(ProviderOf[InjectMe])
# This used to fail with an error similar to the ProviderOf one
injector.get(AssistedBuilder(cls=InjectMe))
injector.get(ClassAssistedBuilder[InjectMe])
def test_binding_an_instance_regression():

View File

@ -47,6 +47,7 @@ setup(
author_email='alec@swapoff.org',
install_requires=[
'setuptools >= 0.6b1',
'typing; python_version < "3.5"',
],
cmdclass={'test': PyTest},
keywords=[