From 2b14948aa7614450fcbfe0dbc44dd8caf472ef3d Mon Sep 17 00:00:00 2001 From: Jakub Stasiak Date: Tue, 18 Oct 2016 13:11:27 +0200 Subject: [PATCH] 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 --- .travis.yml | 3 +- CHANGES | 14 +++++++++ README.md | 2 +- docs/terminology.rst | 17 ++++++----- injector.py | 67 ++++++++++++++++++-------------------------- injector_test.py | 30 ++++++++++---------- setup.py | 1 + 7 files changed, 68 insertions(+), 66 deletions(-) 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=[