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:
parent
33bc8f336b
commit
2b14948aa7
|
@ -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
14
CHANGES
|
@ -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
|
||||||
------
|
------
|
||||||
|
|
|
@ -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
|
||||||
---------------
|
---------------
|
||||||
|
|
|
@ -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>`_
|
||||||
|
|
67
injector.py
67
injector.py
|
@ -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
|
||||||
|
|
|
@ -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():
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -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=[
|
||||||
|
|
Loading…
Reference in New Issue