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 sudo: false
language: python language: python
python: python:
- "2.6"
- "2.7" - "2.7"
- "3.2" - "3.2"
- "3.3" - "3.3"
@ -12,5 +11,5 @@ python:
- "pypy" - "pypy"
- "pypy3" - "pypy3"
install: install:
- pip install pytest - pip install pytest typing
script: py.test -vv 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. Now you need to move the function with injectable dependencies to a class.
* Removed support for getting AssistedBuilder(callable=...) * 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 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 * 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 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:: In this situation there's technique called Assisted injection::
from injector import AssistedBuilder from injector import ClassAssistedBuilder
injector = Injector() injector = Injector()
builder = injector.get(AssistedBuilder(cls=UserUpdater)) builder = injector.get(ClassAssistedBuilder[UserUpdater])
user = User('John') user = User('John')
user_updater = builder.build(user=user) 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. 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): class NeedsUserUpdater(object):
@inject(updater_builder=AssistedBuilder(cls=UserUpdater)) @inject(updater_builder=ClassAssistedBuilder[UserUpdater])
def __init__(self, builder): def __init__(self, builder):
self.updater_builder = builder self.updater_builder = builder
def method(self): def method(self):
updater = self.updater_builder.build(user=None) 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') >>> DB = Key('DB')
>>> class DBImplementation(object): >>> class DBImplementation(object):
@ -193,12 +194,10 @@ If you want `AssistedBuilder` to follow bindings and construct class pointed to
... binder.bind(DB, to=DBImplementation) ... binder.bind(DB, to=DBImplementation)
... ...
>>> injector = Injector(configure) >>> injector = Injector(configure)
>>> builder = injector.get(AssistedBuilder(interface=DB)) >>> builder = injector.get(AssistedBuilder[DB])
>>> isinstance(builder.build(uri='x'), DBImplementation) >>> isinstance(builder.build(uri='x'), DBImplementation)
True True
Note: ``AssistedBuilder(X)`` is a shortcut for ``AssistedBuilder(interface=X)``
More information on this topic: 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>`_ - `"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 import warnings
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from collections import namedtuple from collections import namedtuple
from typing import Generic, TypeVar
try: try:
@ -398,11 +399,11 @@ class Binder(object):
return Binding(interface, provider, scope) return Binding(interface, provider, scope)
def provider_for(self, interface, to=None): 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: if to is not None:
raise Exception('ProviderOf cannot be bound to anything') raise Exception('ProviderOf cannot be bound to anything')
return InstanceProvider( return InstanceProvider(ProviderOf(self.injector, target))
BoundProvider(self.injector, interface.interface))
elif isinstance(to, Provider): elif isinstance(to, Provider):
return to return to
elif isinstance(interface, Provider): elif isinstance(interface, Provider):
@ -418,8 +419,9 @@ class Binder(object):
def proxy(**kwargs): def proxy(**kwargs):
return interface.interface(**kwargs) return interface.interface(**kwargs)
return CallableProvider(proxy) return CallableProvider(proxy)
elif isinstance(interface, AssistedBuilder): elif isinstance(interface, type) and issubclass(interface, AssistedBuilder):
builder = AssistedBuilderImplementation(self.injector, *interface) (target,) = interface.__args__
builder = interface(self.injector, target)
return InstanceProvider(builder) return InstanceProvider(builder)
elif isinstance(interface, (tuple, type)) and isinstance(to, interface): elif isinstance(interface, (tuple, type)) and isinstance(to, interface):
return InstanceProvider(to) return InstanceProvider(to)
@ -459,7 +461,7 @@ class Binder(object):
# "Special" interfaces are ones that you cannot bind yourself but # "Special" interfaces are ones that you cannot bind yourself but
# you can request them (for example you cannot bind ProviderOf(SomeClass) # you can request them (for example you cannot bind ProviderOf(SomeClass)
# to anything but you can inject ProviderOf(SomeClass) just fine # to anything but you can inject ProviderOf(SomeClass) just fine
return isinstance(interface, (ProviderOf, AssistedBuilder)) return issubclass(interface, (ProviderOf, AssistedBuilder))
class Scope(object): class Scope(object):
@ -1101,30 +1103,18 @@ class BoundKey(tuple):
return dict(self[1]) return dict(self[1])
class AssistedBuilder(namedtuple('_AssistedBuilder', 'interface cls')): T = TypeVar('T')
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)
class AssistedBuilderImplementation(namedtuple( class AssistedBuilder(Generic[T]):
'_AssistedBuilderImplementation', 'injector interface cls')):
def __init__(self, injector, target):
self._injector = injector
self._target = target
def build(self, **kwargs): def build(self, **kwargs):
if self.interface is not None: key = BindingKey(self._target)
return self.build_interface(**kwargs) binder = self._injector.binder
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
binding = binder.get_binding(None, key) binding = binder.get_binding(None, key)
provider = binding.provider provider = binding.provider
if not isinstance(provider, ClassProvider): if not isinstance(provider, ClassProvider):
@ -1132,7 +1122,15 @@ class AssistedBuilderImplementation(namedtuple(
'Assisted interface building works only with ClassProviders, ' 'Assisted interface building works only with ClassProviders, '
'got %r for %r' % (provider, self.interface)) '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): def _describe(c):
@ -1143,8 +1141,8 @@ def _describe(c):
return str(c) return str(c)
class ProviderOf(object): class ProviderOf(Generic[T]):
"""Can be used to get a :class:`BoundProvider` of an interface, for example: """Can be used to get a provider of an interface, for example:
>>> def provide_int(): >>> def provide_int():
... print('providing') ... print('providing')
@ -1154,22 +1152,13 @@ class ProviderOf(object):
... binder.bind(int, to=provide_int) ... binder.bind(int, to=provide_int)
>>> >>>
>>> injector = Injector(configure) >>> injector = Injector(configure)
>>> provider = injector.get(ProviderOf(int)) >>> provider = injector.get(ProviderOf[int])
>>> type(provider)
<class 'injector.BoundProvider'>
>>> value = provider.get() >>> value = provider.get()
providing providing
>>> value >>> value
123 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): def __init__(self, injector, interface):
self._injector = injector self._injector = injector
self._interface = interface self._interface = interface

View File

@ -23,7 +23,7 @@ from injector import (
inject, singleton, threadlocal, UnsatisfiedRequirement, inject, singleton, threadlocal, UnsatisfiedRequirement,
CircularDependency, Module, provides, Key, SingletonScope, CircularDependency, Module, provides, Key, SingletonScope,
ScopeDecorator, with_injector, AssistedBuilder, BindingKey, 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 pass
injector = Injector(configure) injector = Injector(configure)
builder = injector.get(AssistedBuilder(A)) builder = injector.get(AssistedBuilder[A])
with pytest.raises(ZeroDivisionError): with pytest.raises(ZeroDivisionError):
builder.build() builder.build()
@ -383,7 +383,7 @@ def test_dependency_cycle_can_be_worked_broken_by_assisted_building():
self.i = i self.i = i
class B(object): class B(object):
@inject(a_builder=AssistedBuilder(A)) @inject(a_builder=AssistedBuilder[A])
def __init__(self, a_builder): def __init__(self, a_builder):
self.a = a_builder.build(i=self) self.a = a_builder.build(i=self)
@ -820,14 +820,14 @@ class NeedsAssistance(object):
def test_assisted_builder_works_when_got_directly_from_injector(): def test_assisted_builder_works_when_got_directly_from_injector():
injector = Injector() injector = Injector()
builder = injector.get(AssistedBuilder(NeedsAssistance)) builder = injector.get(AssistedBuilder[NeedsAssistance])
obj = builder.build(b=123) obj = builder.build(b=123)
assert ((obj.a, obj.b) == (str(), 123)) assert ((obj.a, obj.b) == (str(), 123))
def test_assisted_builder_works_when_injected(): def test_assisted_builder_works_when_injected():
class X(object): class X(object):
@inject(builder=AssistedBuilder(NeedsAssistance)) @inject(builder=AssistedBuilder[NeedsAssistance])
def __init__(self, builder): def __init__(self, builder):
self.obj = builder.build(b=234) self.obj = builder.build(b=234)
@ -843,7 +843,7 @@ def test_assisted_builder_uses_bindings():
binder.bind(Interface, to=NeedsAssistance) binder.bind(Interface, to=NeedsAssistance)
injector = Injector(configure) injector = Injector(configure)
builder = injector.get(AssistedBuilder(Interface)) builder = injector.get(AssistedBuilder[Interface])
x = builder.build(b=333) x = builder.build(b=333)
assert ((type(x), x.b) == (NeedsAssistance, 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) binder.bind(X, to=lambda: 1 / 0)
injector = Injector(configure) injector = Injector(configure)
builder = injector.get(AssistedBuilder(cls=X)) builder = injector.get(ClassAssistedBuilder[X])
builder.build() builder.build()
def test_assisted_builder_injection_is_safe_to_use_with_multiple_injectors(): def test_assisted_builder_injection_is_safe_to_use_with_multiple_injectors():
class X(object): class X(object):
@inject(builder=AssistedBuilder(NeedsAssistance)) @inject(builder=AssistedBuilder[NeedsAssistance])
def y(self, builder): def y(self, builder):
return builder return builder
i1, i2 = Injector(), Injector() i1, i2 = Injector(), Injector()
b1 = i1.get(X).y() b1 = i1.get(X).y()
b2 = i2.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(): 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 # 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() assert gen_key() == gen_key()
@ -1018,7 +1018,7 @@ def test_providerof():
assert counter[0] == 0 assert counter[0] == 0
provider = injector.get(ProviderOf(str)) provider = injector.get(ProviderOf[str])
assert counter[0] == 0 assert counter[0] == 0
assert provider.get() == 'content' assert provider.get() == 'content'
@ -1030,7 +1030,7 @@ def test_providerof():
def test_providerof_cannot_be_bound(): def test_providerof_cannot_be_bound():
def configure(binder): def configure(binder):
binder.bind(ProviderOf(int), to=InstanceProvider(None)) binder.bind(ProviderOf[int], to=InstanceProvider(None))
with pytest.raises(Exception): with pytest.raises(Exception):
Injector(configure) Injector(configure)
@ -1046,7 +1046,7 @@ def test_providerof_is_safe_to_use_with_multiple_injectors():
injector1 = Injector(configure1) injector1 = Injector(configure1)
injector2 = Injector(configure2) injector2 = Injector(configure2)
provider_of = ProviderOf(int) provider_of = ProviderOf[int]
provider1 = injector1.get(provider_of) provider1 = injector1.get(provider_of)
provider2 = injector2.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) # raise UnsatisfiedRequirement(cls, key)
# UnsatisfiedRequirement: unsatisfied requirement on # UnsatisfiedRequirement: unsatisfied requirement on
# <injector.ProviderOf object at 0x10ff01550> # <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 # 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(): def test_binding_an_instance_regression():

View File

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