diff --git a/.travis.yml b/.travis.yml index 13732a4..05af009 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/CHANGES b/CHANGES index 62530a5..9ecb824 100644 --- a/CHANGES +++ b/CHANGES @@ -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 ------ diff --git a/README.md b/README.md index 775e562..107755d 100644 --- a/README.md +++ b/README.md @@ -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 --------------- diff --git a/docs/terminology.rst b/docs/terminology.rst index bf075da..3ae4cbb 100644 --- a/docs/terminology.rst +++ b/docs/terminology.rst @@ -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 `_ diff --git a/injector.py b/injector.py index de2e7dc..c3fdcb7 100644 --- a/injector.py +++ b/injector.py @@ -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) - + >>> 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 diff --git a/injector_test.py b/injector_test.py index 2bd05d8..eabfd5d 100644 --- a/injector_test.py +++ b/injector_test.py @@ -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.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(): diff --git a/setup.py b/setup.py index 08cc59c..8211143 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,7 @@ setup( author_email='alec@swapoff.org', install_requires=[ 'setuptools >= 0.6b1', + 'typing; python_version < "3.5"', ], cmdclass={'test': PyTest}, keywords=[