Hello community, here is the log from the commit of package python-injector for openSUSE:Factory checked in at 2019-12-11 12:15:00 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-injector (Old) and /work/SRC/openSUSE:Factory/.python-injector.new.4691 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-injector" Wed Dec 11 12:15:00 2019 rev:4 rq:755754 version:0.18.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-injector/python-injector.changes 2019-07-26 12:39:44.813929745 +0200 +++ /work/SRC/openSUSE:Factory/.python-injector.new.4691/python-injector.changes 2019-12-11 12:15:19.276508220 +0100 @@ -1,0 +2,7 @@ +Wed Dec 11 08:48:54 UTC 2019 - Tomáš Chvátal <tchva...@suse.com> + +- Update to 0.18.1: + * Various minor fixes and support for new python +- Depend on full python interpreter for sqlite module + +------------------------------------------------------------------- Old: ---- 0.17.0.tar.gz New: ---- 0.18.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-injector.spec ++++++ --- /var/tmp/diff_new_pack.wX86H5/_old 2019-12-11 12:15:19.792508074 +0100 +++ /var/tmp/diff_new_pack.wX86H5/_new 2019-12-11 12:15:19.792508074 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-injector # -# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2019 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,18 +19,20 @@ %define skip_python2 1 %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-injector -Version: 0.17.0 +Version: 0.18.1 Release: 0 Summary: Python dependency injection framework, inspired by Guice License: BSD-3-Clause -Group: Development/Languages/Python URL: https://github.com/alecthomas/injector Source: https://github.com/alecthomas/injector/archive/%{version}.tar.gz -BuildRequires: %{python_module pytest-cov} BuildRequires: %{python_module pytest} BuildRequires: %{python_module setuptools} +BuildRequires: %{python_module typing_extensions >= 3.7.4} +BuildRequires: %{pythons} BuildRequires: fdupes BuildRequires: python-rpm-macros +Requires: python +Requires: python-typing_extensions >= 3.7.4 BuildArch: noarch %python_subpackages @@ -51,6 +53,7 @@ %prep %setup -q -n injector-%{version} +rm pytest.ini %build %python_build ++++++ 0.17.0.tar.gz -> 0.18.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.17.0/.travis.yml new/injector-0.18.1/.travis.yml --- old/injector-0.17.0/.travis.yml 2019-06-15 16:23:44.000000000 +0200 +++ new/injector-0.18.1/.travis.yml 2019-12-10 02:25:28.000000000 +0100 @@ -4,15 +4,16 @@ python: - "3.5" - "3.6" + - "3.7" + - "3.8" - "nightly" - - "pypy3.5-5.8.0" + - "pypy3.5" + - "pypy3" matrix: allow_failures: - python: "nightly" - include: - - { python: "3.7", dist: xenial, sudo: true } install: - - pip install --upgrade coveralls pytest "pytest-cov>=2.5.1" dataclasses + - pip install --upgrade coveralls pytest "pytest-cov>=2.5.1" dataclasses typing_extensions # mypy can't be installed on pypy - if [[ "${TRAVIS_PYTHON_VERSION}" != "pypy"* ]] ; then pip install mypy ; fi # Black is Python 3.6+-only diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.17.0/CHANGES new/injector-0.18.1/CHANGES --- old/injector-0.17.0/CHANGES 2019-06-15 16:23:44.000000000 +0200 +++ new/injector-0.18.1/CHANGES 2019-12-10 02:25:28.000000000 +0100 @@ -1,6 +1,27 @@ Injector Change Log =================== +0.18.1 +------ + +- Fixed UnsatisfiedRequirement instantiation (trying to get its string representation would fail) +- Fixed injecting a subclass of a generic type on Python versions older than 3.7.0 +- Fixed regression that caused BoundKey injection failure + +0.18.0 +------ + +- Added new public :func:`get_bindings <injector.get_bindings>` function to see what parameters will be injected + into a function +- Added new generic types using a draft implementation of `PEP 593 <https://www.python.org/dev/peps/pep-0593/>`_: + :data:`Inject <injector.Inject>` and :data:`NoInject <injector.NoInject>`. Those serve as additional ways to + declare (non)injectable parameters while :func:`inject <injector.inject>` won't go away any time soon + :func:`noninjectable <injector.noninjectable>` may be removed once `NoInject` is cofirmed to work. + +Backwards incompatible: + +- Removed previously deprecated `Key`, `BindingKey`, `SequenceKey` and `MappingKey` pseudo-types + 0.17.0 ------ @@ -43,7 +64,7 @@ - Removed previously deprecated constructs: with_injector, Injector.install_into, Binder.bind_scope - Dependencies are no longer injected into Module.configure and raw module functions (previously deprecated) -– Removed unofficial support for injecting into parent class constructors +- Removed unofficial support for injecting into parent class constructors 0.15.0 ------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.17.0/README.md new/injector-0.18.1/README.md --- old/injector-0.17.0/README.md 2019-06-15 16:23:44.000000000 +0200 +++ new/injector-0.18.1/README.md 2019-12-10 02:25:28.000000000 +0100 @@ -7,11 +7,50 @@ Introduction ------------ -Dependency injection as a formal pattern is less useful in Python than in other languages, primarily due to its support for keyword arguments, the ease with which objects can be mocked, and its dynamic nature. +While dependency injection is easy to do in Python due to its support for keyword arguments, the ease with which objects can be mocked and its dynamic natura, a framework for assisting in this process can remove a lot of boiler-plate from larger applications. That's where Injector can help. It automatically and transitively provides dependencies for you. As an added benefit, Injector encourages nicely compartmentalised code through the use of :ref:`modules <module>`. -That said, a framework for assisting in this process can remove a lot of boiler-plate from larger applications. That's where Injector can help. It automatically and transitively provides keyword arguments with their values. As an added benefit, Injector encourages nicely compartmentalised code through the use of `Module` s. +If you're not sure what dependency injection is or you'd like to learn more about it see: -While being inspired by Guice, it does not slavishly replicate its API. Providing a Pythonic API trumps faithfulness. +* [The Clean Code Talks - Don't Look For Things! (a talk by Miško Hevery)]( + https://www.youtube.com/watch?v=RlfLCWKxHJ0) +* [Inversion of Control Containers and the Dependency Injection pattern (an article by Martin Fowler)]( + https://martinfowler.com/articles/injection.html) + +The core values of Injector are: + +* Simplicity - while being inspired by Guice, Injector does not slavishly replicate its API. + Providing a Pythonic API trumps faithfulness. Additionally some features are ommitted + because supporting them would be cumbersome and introduce a little bit too much "magic" + (member injection, method injection). + + Connected to this, Injector tries to be as nonintrusive as possible. For example while you may + declare a class' constructor to expect some injectable parameters, the class' constructor + remains a standard constructor – you may instaniate the class just the same manually, if you want. + +* No global state – you can have as many [Injector](https://injector.readthedocs.io/en/latest/api.html#injector.Injector) + instances as you like, each with a different configuration and each with different objects in different + scopes. Code like this won't work for this very reason: + + ```python + class MyClass: + @inject + def __init__(t: SomeType): + # ... + + MyClass() + ``` + + This is simply because there's no global `Injector` to use. You need to be explicit and use + [Injector.get](https://injector.readthedocs.io/en/latest/api.html#injector.Injector.get), + [Injector.create_object](https://injector.readthedocs.io/en/latest/api.html#injector.Injector.create_object) + or inject `MyClass` into the place that needs it. + +* Cooperation with static type checking infrastructure – the API provides as much static type safety + as possible and only breaks it where there's no other option. For example the + [Injector.get](https://injector.readthedocs.io/en/latest/api.html#injector.Injector.get) method + is typed such that `injector.get(SomeType)` is statically declared to return an instance of + `SomeType`, therefore making it possible for tools such as [mypy](https://github.com/python/mypy) to + type-check correctly the code using it. ### How to get Injector? @@ -77,7 +116,7 @@ ```python ->>> from injector import Module, Key, provider, Injector, inject, singleton +>>> from injector import Module, provider, Injector, inject, singleton ``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.17.0/conftest.py new/injector-0.18.1/conftest.py --- old/injector-0.17.0/conftest.py 2019-06-15 16:23:44.000000000 +0200 +++ new/injector-0.18.1/conftest.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,7 +0,0 @@ -import os.path - -test_sources = ['injector', 'injector_test.py', 'README.md'] - - -def pytest_ignore_collect(path, config): - return not os.path.basename(str(path)) in test_sources diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.17.0/docs/index.rst new/injector-0.18.1/docs/index.rst --- old/injector-0.17.0/docs/index.rst 2019-06-15 16:23:44.000000000 +0200 +++ new/injector-0.18.1/docs/index.rst 2019-12-10 02:25:28.000000000 +0100 @@ -20,16 +20,61 @@ pip install injector +Injector works with CPython 3.5+ and PyPy 3 implementing Python 3.5+. + Introduction ------------ -Dependency injection as a formal pattern is less useful in Python than in other languages, primarily due to its support for keyword arguments, the ease with which objects can be mocked, and its dynamic nature. +While dependency injection is easy to do in Python due to its support for keyword arguments, the ease with which objects can be mocked and its dynamic natura, a framework for assisting in this process can remove a lot of boiler-plate from larger applications. That's where Injector can help. It automatically and transitively provides dependencies for you. As an added benefit, Injector encourages nicely compartmentalised code through the use of :ref:`modules <module>`. + +If you're not sure what dependency injection is or you'd like to learn more about it see: + +* `The Clean Code Talks - Don't Look For Things! (a talk by Miško Hevery) + <https://www.youtube.com/watch?v=RlfLCWKxHJ0>`_ +* `Inversion of Control Containers and the Dependency Injection pattern (an article by Martin Fowler) + <https://martinfowler.com/articles/injection.html>`_ + +The core values of Injector are: + +* Simplicity - while being inspired by Guice, Injector does not slavishly replicate its API. + Providing a Pythonic API trumps faithfulness. Additionally some features are ommitted + because supporting them would be cumbersome and introduce a little bit too much "magic" + (member injection, method injection). + + Connected to this, Injector tries to be as nonintrusive as possible. For example while you may + declare a class' constructor to expect some injectable parameters, the class' constructor + remains a standard constructor – you may instaniate the class just the same manually, if you want. + +* No global state – you can have as many :class:`Injector` instances as you like, each with + a different configuration and each with different objects in different scopes. Code like this + won't work for this very reason:: + + class MyClass: + @inject + def __init__(t: SomeType): + # ... + + MyClass() + + This is simply because there's no global :class:`Injector` to use. You need to be explicit and use + :meth:`Injector.get <injector.Injector.get>`, + :meth:`Injector.create_object <injector.Injector.create_object>` or inject `MyClass` into the place + that needs it. + +* Cooperation with static type checking infrastructure – the API provides as much static type safety + as possible and only breaks it where there's no other option. For example the + :meth:`Injector.get <injector.Injector.get>` method is typed such that `injector.get(SomeType)` + is statically declared to return an instance of `SomeType`, therefore making it possible for tools + such as `mypy <https://github.com/python/mypy>`_ to type-check correctly the code using it. -That said, a framework for assisting in this process can remove a lot of boiler-plate from larger applications. That's where Injector can help. It automatically and transitively provides keyword arguments with their values. As an added benefit, Injector encourages nicely compartmentalised code through the use of :ref:`modules <module>`. +Quick start +----------- -While being inspired by Guice, it does not slavishly replicate its API. Providing a Pythonic API trumps faithfulness. +See `the project's README <https://github.com/alecthomas/injector/blob/master/README.md>`_ for an +example of Injector use. -Contents: +Contents +-------- .. toctree:: :maxdepth: 1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.17.0/injector/__init__.py new/injector-0.18.1/injector/__init__.py --- old/injector-0.17.0/injector/__init__.py 2019-06-15 16:23:44.000000000 +0200 +++ new/injector-0.18.1/injector/__init__.py 2019-12-10 02:25:28.000000000 +0100 @@ -21,29 +21,36 @@ import sys import threading import types -import warnings from abc import ABCMeta, abstractmethod from collections import namedtuple -from typing import ( - Any, - Callable, - cast, - Dict, - Generic, - get_type_hints, - List, - overload, - Tuple, - Type, - TypeVar, - Union, -) +from typing import Any, Callable, cast, Dict, Generic, List, Optional, overload, Tuple, Type, TypeVar, Union + +HAVE_ANNOTATED = sys.version_info >= (3, 7, 0) + +if HAVE_ANNOTATED: + # Ignoring errors here as typing_extensions stub doesn't know about those things yet + from typing_extensions import _AnnotatedAlias, Annotated, get_type_hints # type: ignore +else: + + class Annotated: # type: ignore + pass + + from typing import get_type_hints as _get_type_hints + + def get_type_hints( + obj: Callable[..., Any], + globalns: Optional[Dict[str, Any]] = None, + localns: Optional[Dict[str, Any]] = None, + include_extras: bool = False, + ) -> Dict[str, Any]: + return _get_type_hints(obj, globalns, localns) + TYPING353 = hasattr(Union[str, int], '__origin__') __author__ = 'Alec Thomas <a...@swapoff.org>' -__version__ = '0.17.0' +__version__ = '0.18.1' __version_tag__ = '' log = logging.getLogger('injector') @@ -80,6 +87,91 @@ lock = threading.RLock() +_inject_marker = object() +_noinject_marker = object() + +if HAVE_ANNOTATED: + InjectT = TypeVar('InjectT') + Inject = Annotated[InjectT, _inject_marker] + """An experimental way to declare injectable dependencies utilizing a `PEP 593`_ implementation + in `typing_extensions`. + + Those two declarations are equivalent:: + + @inject + def fun(t: SomeType) -> None: + pass + + def fun(t: Inject[SomeType]) -> None: + pass + + The advantage over using :func:`inject` is that if you have some noninjectable parameters + it may be easier to spot what are they. Those two are equivalent:: + + @inject + @noninjectable('s') + def fun(t: SomeType, s: SomeOtherType) -> None: + pass + + def fun(t: Inject[SomeType], s: SomeOtherType) -> None: + pass + + .. seealso:: + + Function :func:`get_bindings` + A way to inspect how various injection declarations interact with each other. + + .. versionadded:: 0.18.0 + .. note:: Requires Python 3.7+. + .. note:: + + If you're using mypy you need the version 0.750 or newer to fully type-check code using this + construct. + + .. _PEP 593: https://www.python.org/dev/peps/pep-0593/ + .. _typing_extensions: https://pypi.org/project/typing-extensions/ + """ + + NoInject = Annotated[InjectT, _noinject_marker] + """An experimental way to declare noninjectable dependencies utilizing a `PEP 593`_ implementation + in `typing_extensions`. + + Since :func:`inject` declares all function's parameters to be injectable there needs to be a way + to opt out of it. This has been provided by :func:`noninjectable` but `noninjectable` suffers from + two issues: + + * You need to repeat the parameter name + * The declaration may be relatively distance in space from the actual parameter declaration, thus + hindering readability + + `NoInject` solves both of those concerns, for example (those two declarations are equivalent):: + + @inject + @noninjectable('b') + def fun(a: TypeA, b: TypeB) -> None: + pass + + @inject + def fun(a: TypeA, b: NoInject[TypeB]) -> None: + pass + + .. seealso:: + + Function :func:`get_bindings` + A way to inspect how various injection declarations interact with each other. + + .. versionadded:: 0.18.0 + .. note:: Requires Python 3.7+. + .. note:: + + If you're using mypy you need the version 0.750 or newer to fully type-check code using this + construct. + + .. _PEP 593: https://www.python.org/dev/peps/pep-0593/ + .. _typing_extensions: https://pypi.org/project/typing-extensions/ + """ + + def reraise(original: Exception, exception: Exception, maximum_frames: int = 1) -> None: prev_cls, prev, tb = sys.exc_info() frames = inspect.getinnerframes(cast(types.TracebackType, tb)) @@ -170,16 +262,19 @@ :: - >>> key = Key('key') + >>> class MyClass: + ... def __init__(self, value: int) -> None: + ... self.value = value + ... >>> def factory(): ... print('providing') - ... return [] + ... return MyClass(42) ... >>> def configure(binder): - ... binder.bind(key, to=CallableProvider(factory)) + ... binder.bind(MyClass, to=CallableProvider(factory)) ... >>> injector = Injector(configure) - >>> injector.get(key) is injector.get(key) + >>> injector.get(MyClass) is injector.get(MyClass) providing providing False @@ -200,15 +295,17 @@ :: - >>> my_list = Key('my_list') + >>> class MyType: + ... def __init__(self): + ... self.contents = [] >>> def configure(binder): - ... binder.bind(my_list, to=InstanceProvider([])) + ... binder.bind(MyType, to=InstanceProvider(MyType())) ... >>> injector = Injector(configure) - >>> injector.get(my_list) is injector.get(my_list) + >>> injector.get(MyType) is injector.get(MyType) True - >>> injector.get(my_list).append('x') - >>> injector.get(my_list) + >>> injector.get(MyType).contents.append('x') + >>> injector.get(MyType).contents ['x'] """ @@ -257,37 +354,6 @@ return map -@private -class BindingKey(tuple): - """A key mapping to a Binding.""" - - @classmethod - def create(cls, what: Any) -> 'BindingKey': - if isinstance(what, list): - if len(what) != 1: - raise Error('list bindings must have a single interface ' 'element') - warnings.warn( - 'Multibinding using the %s form is deprecated, use typing.List instead.' % (what,), - RuntimeWarning, - stacklevel=3, - ) - what = (list, BindingKey.create(what[0])) - elif isinstance(what, dict): - if len(what) != 1: - raise Error('dictionary bindings must have a single interface ' 'key and value') - warnings.warn( - 'Multibinding using the %s form is deprecated, use typing.Dict instead.' % (what,), - RuntimeWarning, - stacklevel=3, - ) - what = (dict, BindingKey.create(list(what.items())[0])) - return tuple.__new__(cls, (what,)) - - @property - def interface(self): - return self[0] - - _BindingBase = namedtuple('_BindingBase', 'interface provider scope') @@ -307,7 +373,7 @@ """ @private - def __init__(self, injector, auto_bind=True, parent=None): + def __init__(self, injector: 'Injector', auto_bind: bool = True, parent: 'Binder' = None) -> None: """Create a new Binder. :param injector: Injector we are binding for. @@ -316,10 +382,15 @@ """ self.injector = injector self._auto_bind = auto_bind - self._bindings = {} + self._bindings = {} # type: Dict[type, Binding] self.parent = parent - def bind(self, interface, to=None, scope=None): + def bind( + self, + interface: Type[T], + to: Union[None, T, Callable[..., T], Provider[T]] = None, + scope: Union[None, Type['Scope'], 'ScopeDecorator'] = None, + ) -> None: """Bind an interface to an implementation. `typing.List` and `typing.Dict` instances are reserved for multibindings and trying to bind them @@ -327,28 +398,16 @@ binder.bind(List[str], to=['hello', 'there']) # Error - :param interface: Interface or :func:`Key` to bind. + :param interface: Type to bind. :param to: Instance or class to bind to, or an explicit :class:`Provider` subclass. :param scope: Optional :class:`Scope` in which to bind. """ - if type(interface) is type and issubclass(interface, (BaseMappingKey, BaseSequenceKey)): - return self.multibind(interface, to, scope=scope) if _get_origin(_punch_through_alias(interface)) in {dict, list}: raise Error( 'Type %s is reserved for multibindings. Use multibind instead of bind.' % (interface,) ) - key = BindingKey.create(interface) - self._bindings[key] = self.create_binding(interface, to, scope) - - @overload - def multibind( - self, - interface: Union['BaseSequenceKey', 'BaseMappingKey'], - to: Any, - scope: Union[Type['Scope'], 'ScopeDecorator'] = None, - ) -> None: - pass + self._bindings[interface] = self.create_binding(interface, to, scope) @overload def multibind( @@ -386,13 +445,12 @@ Deprecated support for `MappingKey`, `SequenceKey` and single-item lists and dictionaries as interfaces. - :param interface: :func:`MappingKey`, :func:`SequenceKey` or typing.Dict or typing.List instance to bind to. + :param interface: typing.Dict or typing.List instance to bind to. :param to: Instance, class to bind to, or an explicit :class:`Provider` subclass. Must provide a list or a dictionary, depending on the interface. :param scope: Optional Scope in which to bind. """ - key = BindingKey.create(interface) - if key not in self._bindings: + if interface not in self._bindings: if ( isinstance(interface, dict) or isinstance(interface, type) @@ -403,14 +461,14 @@ else: provider = MultiBindProvider() binding = self.create_binding(interface, provider, scope) - self._bindings[key] = binding + self._bindings[interface] = binding else: - binding = self._bindings[key] + binding = self._bindings[interface] provider = binding.provider assert isinstance(provider, ListOfProviders) - provider.append(self.provider_for(key.interface, to)) + provider.append(self.provider_for(interface, to)) - def install(self, module): + def install(self, module: Union[Callable[['Binder'], None], 'Module', Type['Module']]) -> None: """Install a module into this binder. In this context the module is one of the following: @@ -441,8 +499,8 @@ binder.install(MyModule) """ - if type(module) is type and issubclass(module, Module): - instance = module() + if type(module) is type and issubclass(cast(type, module), Module): + instance = cast(type, module)() else: instance = module instance(self) @@ -482,10 +540,14 @@ return ClassProvider(to) elif isinstance(interface, BoundKey): - def proxy(**kwargs): + def proxy(injector: Injector): + binder = injector.binder + kwarg_providers = { + name: binder.provider_for(None, provider) for (name, provider) in interface.kwargs.items() + } + kwargs = {name: provider.get(injector) for (name, provider) in kwarg_providers.items()} return interface.interface(**kwargs) - proxy.__annotations__ = interface.kwargs.copy() return CallableProvider(inject(proxy)) elif _is_specialization(interface, AssistedBuilder): (target,) = interface.__args__ @@ -517,23 +579,23 @@ raise KeyError - def get_binding(self, key): - is_scope = isinstance(key.interface, type) and issubclass(key.interface, Scope) + def get_binding(self, interface): + is_scope = isinstance(interface, type) and issubclass(interface, Scope) try: - return self._get_binding(key, only_this_binder=is_scope) + return self._get_binding(interface, only_this_binder=is_scope) except (KeyError, UnsatisfiedRequirement): if is_scope: - scope = key.interface + scope = interface self.bind(scope, to=scope(self.injector)) - return self._get_binding(key) + return self._get_binding(interface) # The special interface is added here so that requesting a special # interface with auto_bind disabled works - if self._auto_bind or self._is_special_interface(key.interface): - binding = self.create_binding(key.interface) - self._bindings[key] = binding + if self._auto_bind or self._is_special_interface(interface): + binding = self.create_binding(interface) + self._bindings[interface] = binding return binding, self - raise UnsatisfiedRequirement(key) + raise UnsatisfiedRequirement(None, interface) def _is_special_interface(self, interface): # "Special" interfaces are ones that you cannot bind yourself but @@ -549,6 +611,13 @@ # issubclass(SomeGeneric[X], SomeGeneric) so we need some other way to # determine whether a particular object is a generic class with type parameters # provided. Fortunately there seems to be __origin__ attribute that's useful here. + + # We need to special-case Annotated as its __origin__ behaves differently than + # other typing generic classes. See https://github.com/python/typing/pull/635 + # for some details. + if HAVE_ANNOTATED and generic_class is Annotated and isinstance(cls, _AnnotatedAlias): + return True + if not hasattr(cls, '__origin__'): return False origin = cls.__origin__ @@ -631,9 +700,6 @@ class NoScope(Scope): """An unscoped provider.""" - def __init__(self, injector=None): - super(NoScope, self).__init__(injector) - def get(self, unused_key, provider): return provider @@ -786,20 +852,18 @@ :param scope: Class of the Scope in which to resolve. :returns: An implementation of interface. """ - key = BindingKey.create(interface) - binding, binder = self.binder.get_binding(key) + binding, binder = self.binder.get_binding(interface) scope = scope or binding.scope if isinstance(scope, ScopeDecorator): scope = scope.scope # Fetch the corresponding Scope instance from the Binder. - scope_key = BindingKey.create(scope) - scope_binding, _ = binder.get_binding(scope_key) + scope_binding, _ = binder.get_binding(scope) scope_instance = scope_binding.provider.get(self) log.debug( '%sInjector.get(%r, scope=%r) using %r', self._log_prefix, interface, scope, binding.provider ) - result = scope_instance.get(key, binding.provider).get(self) + result = scope_instance.get(interface, binding.provider).get(self) log.debug('%s -> %r', self._log_prefix, result) return result @@ -823,19 +887,7 @@ init = cls.__init__ self.call_with_injection(init, self_=instance, kwargs=additional_kwargs) except TypeError as e: - # The reason why getattr() fallback is used here is that - # __init__.__func__ apparently doesn't exist for Key-type objects - reraise( - e, - CallError( - instance, - getattr(instance.__init__, '__func__', instance.__init__), - (), - additional_kwargs, - e, - self._stack, - ), - ) + reraise(e, CallError(instance, instance.__init__.__func__, (), additional_kwargs, e, self._stack)) return instance def call_with_injection(self, callable, self_=None, args=(), kwargs={}): @@ -851,16 +903,7 @@ :return: Value returned by callable. """ - def _get_callable_bindings(callable): - if not hasattr(callable, '__bindings__'): - return {} - - if callable.__bindings__ == 'deferred': - read_and_store_bindings(callable, _infer_injected_bindings(callable)) - return callable.__bindings__ - - bindings = _get_callable_bindings(callable) - noninjectables = getattr(callable, '__noninjectables__', set()) + bindings = get_bindings(callable) signature = inspect.signature(callable) full_args = args if self_ is not None: @@ -868,9 +911,7 @@ bound_arguments = signature.bind_partial(*full_args) needed = dict( - (k, v) - for (k, v) in bindings.items() - if k not in kwargs and k not in noninjectables and k not in bound_arguments.arguments + (k, v) for (k, v) in bindings.items() if k not in kwargs and k not in bound_arguments.arguments ) dependencies = self.args_to_inject( @@ -915,9 +956,9 @@ self._stack += (key,) try: - for arg, key in bindings.items(): + for arg, interface in bindings.items(): try: - instance = self.get(key.interface) + instance = self.get(interface) except UnsatisfiedRequirement as e: if not e.args[0]: e = UnsatisfiedRequirement(owner_key, e.args[1]) @@ -929,14 +970,102 @@ return dependencies +def get_bindings(callable: Callable) -> Dict[str, type]: + """Get bindings of injectable parameters from a callable. + + If the callable is not decorated with :func:`inject` and does not have any of its + parameters declared as injectable using :data:`Inject` an empty dictionary will + be returned. Otherwise the returned dictionary will contain a mapping + between parameter names and their types with the exception of parameters + excluded from dependency injection (either with :func:`noninjectable`, :data:`NoInject` + or only explicit injection with :data:`Inject` being used). For example:: + + >>> def function1(a: int) -> None: + ... pass + ... + >>> get_bindings(function1) + {} + + >>> @inject + ... def function2(a: int) -> None: + ... pass + ... + >>> get_bindings(function2) + {'a': <class 'int'>} + + >>> @inject + ... @noninjectable('b') + ... def function3(a: int, b: str) -> None: + ... pass + ... + >>> get_bindings(function3) + {'a': <class 'int'>} + + >>> import sys, pytest + >>> if sys.version_info < (3, 7, 0): + ... pytest.skip('Python 3.7.0 required for sufficient Annotated support') + + >>> # The simple case of no @inject but injection requested with Inject[...] + >>> def function4(a: Inject[int], b: str) -> None: + ... pass + ... + >>> get_bindings(function4) + {'a': <class 'int'>} + + >>> # Using @inject with Inject is redundant but it should not break anything + >>> @inject + ... def function5(a: Inject[int], b: str) -> None: + ... pass + ... + >>> get_bindings(function5) + {'a': <class 'int'>, 'b': <class 'str'>} + + >>> # We need to be able to exclude a parameter from injection with NoInject + >>> @inject + ... def function6(a: int, b: NoInject[str]) -> None: + ... pass + ... + >>> get_bindings(function6) + {'a': <class 'int'>} + + >>> # The presence of NoInject should not trigger anything on its own + >>> def function7(a: int, b: NoInject[str]) -> None: + ... pass + ... + >>> get_bindings(function7) + {} + + This function is used internally so by calling it you can learn what exactly + Injector is going to try to provide to a callable. + """ + look_for_explicit_bindings = False + if not hasattr(callable, '__bindings__'): + type_hints = get_type_hints(callable, include_extras=True) + has_injectable_parameters = any( + _is_specialization(v, Annotated) and _inject_marker in v.__metadata__ for v in type_hints.values() + ) + + if not has_injectable_parameters: + return {} + else: + look_for_explicit_bindings = True + + if look_for_explicit_bindings or cast(Any, callable).__bindings__ == 'deferred': + read_and_store_bindings( + callable, _infer_injected_bindings(callable, only_explicit_bindings=look_for_explicit_bindings) + ) + noninjectables = getattr(callable, '__noninjectables__', set()) + return {k: v for k, v in cast(Any, callable).__bindings__.items() if k not in noninjectables} + + class _BindingNotYetAvailable(Exception): pass -def _infer_injected_bindings(callable): +def _infer_injected_bindings(callable, only_explicit_bindings: bool): spec = inspect.getfullargspec(callable) try: - bindings = get_type_hints(callable) + bindings = get_type_hints(callable, include_extras=True) except NameError as e: raise _BindingNotYetAvailable(e) @@ -952,10 +1081,22 @@ # variadic arguments aren't supported at the moment (this may change # in the future if someone has a good idea how to utilize them) - bindings.pop(spec.varargs, None) - bindings.pop(spec.varkw, None) + if spec.varargs: + bindings.pop(spec.varargs, None) + if spec.varkw: + bindings.pop(spec.varkw, None) for k, v in list(bindings.items()): + if _is_specialization(v, Annotated): + v, metadata = v.__origin__, v.__metadata__ + bindings[k] = v + else: + metadata = tuple() + + if only_explicit_bindings and _inject_marker not in metadata or _noinject_marker in metadata: + del bindings[k] + break + if _is_specialization(v, Union): # We don't treat Optional parameters in any special way at the moment. if TYPING353: @@ -963,7 +1104,9 @@ else: union_members = v.__union_params__ new_members = tuple(set(union_members) - {type(None)}) - new_union = Union[new_members] + # mypy stared complaining about this line for some reason: + # error: Variable "new_members" is not valid as a type + new_union = Union[new_members] # type: ignore # mypy complains about this construct: # error: The type alias is invalid in runtime context # See: https://github.com/python/mypy/issues/5354 @@ -1038,40 +1181,36 @@ eg. - >>> Sizes = Key('sizes') - >>> Names = Key('names') - >>> >>> class A: ... @inject - ... def __init__(self, number: int, name: str, sizes: Sizes): - ... print([number, name, sizes]) + ... def __init__(self, number: int, name: str): + ... print([number, name]) ... >>> def configure(binder): ... binder.bind(A) ... binder.bind(int, to=123) ... binder.bind(str, to='Bob') - ... binder.bind(Sizes, to=[1, 2, 3]) Use the Injector to get a new instance of A: >>> a = Injector(configure).get(A) - [123, 'Bob', [1, 2, 3]] + [123, 'Bob'] - As a convenience one can decorate a class itself: + As a convenience one can decorate a class itself:: - >>> @inject - ... class B: - ... def __init__(self, dependency: Dependency): - ... self.dependency = dependency + @inject + class B: + def __init__(self, dependency: Dependency): + self.dependency = dependency This is equivalent to decorating its constructor. In particular this provides integration with `dataclasses <https://docs.python.org/3/library/dataclasses.html>`_ (the order of decorator - application is important here): + application is important here):: - >>> @inject - ... @dataclass - ... class C: - ... dependency: Dependency + @inject + @dataclass + class C: + dependency: Dependency .. note:: @@ -1082,6 +1221,14 @@ Third party libraries may, however, provide support for injecting dependencies into non-constructor methods or free functions in one form or another. + .. seealso:: + + Generic type :data:`Inject` + A more explicit way to declare parameters as injectable. + + Function :func:`get_bindings` + A way to inspect how various injection declarations interact with each other. + .. versionchanged:: 0.16.2 (Re)added support for decorating classes with @inject. @@ -1091,7 +1238,7 @@ else: function = constructor_or_class try: - bindings = _infer_injected_bindings(function) + bindings = _infer_injected_bindings(function, only_explicit_bindings=False) read_and_store_bindings(function, bindings) except _BindingNotYetAvailable: function.__bindings__ = 'deferred' @@ -1120,6 +1267,15 @@ each other and the order in which a function is decorated with :func:`inject` and :func:`noninjectable` doesn't matter. + + .. seealso:: + + Generic type :data:`NoInject` + A nicer way to declare parameters as noninjectable. + + Function :func:`get_bindings` + A way to inspect how various injection declarations interact with each other. + """ def decorator(function): @@ -1138,8 +1294,6 @@ @private def read_and_store_bindings(f, bindings): - for key, value in bindings.items(): - bindings[key] = BindingKey.create(value) function_bindings = getattr(f, '__bindings__', None) or {} if function_bindings == 'deferred': function_bindings = {} @@ -1150,75 +1304,6 @@ f.__bindings__ = merged_bindings -@private -class BaseKey: - """Base type for binding keys.""" - - def __init__(self) -> None: - raise Exception( - 'Instantiation of %s prohibited - it is derived from BaseKey ' - 'so most likely you should bind it to something.' % (self.__class__,) - ) - - -def Key(name: str) -> BaseKey: - """Create a new type key. - - .. versionchanged:: 0.17.0 - Deprecated, use `typing.NewType` with a real type or subclass a real type instead. - - >>> Age = Key('Age') - >>> def configure(binder): - ... binder.bind(Age, to=90) - >>> Injector(configure).get(Age) - 90 - """ - warnings.warn('Key is deprecated, use a real type instead', RuntimeWarning, stacklevel=3) - return cast(BaseKey, type(name, (BaseKey,), {})) - - -@private -class BaseMappingKey(dict): - """Base type for mapping binding keys.""" - - def __init__(self) -> None: - raise Exception( - 'Instantiation of %s prohibited - it is derived from BaseMappingKey ' - 'so most likely you should bind it to something.' % (self.__class__,) - ) - - -def MappingKey(name: str) -> BaseMappingKey: - """As for Key, but declares a multibind mapping. - - .. versionchanged:: 0.17.0 - Deprecated, use `typing.Dict` instance instead. - """ - warnings.warn('SequenceKey is deprecated, use typing.Dict instead', RuntimeWarning, stacklevel=3) - return cast(BaseMappingKey, type(name, (BaseMappingKey,), {})) - - -@private -class BaseSequenceKey(list): - """Base type for mapping sequence keys.""" - - def __init__(self) -> None: - raise Exception( - 'Instantiation of %s prohibited - it is derived from BaseSequenceKey ' - 'so most likely you should bind it to something.' % (self.__class__,) - ) - - -def SequenceKey(name: str) -> BaseSequenceKey: - """As for Key, but declares a multibind sequence. - - .. versionchanged:: 0.17.0 - Deprecated, use `typing.List` instance instead. - """ - warnings.warn('SequenceKey is deprecated, use typing.List instead', RuntimeWarning, stacklevel=3) - return cast(BaseSequenceKey, type(name, (BaseSequenceKey,), {})) - - class BoundKey(tuple): """A BoundKey provides a key to a type with pre-injected arguments. @@ -1252,9 +1337,8 @@ self._target = target def build(self, **kwargs: Any) -> T: - key = BindingKey.create(self._target) binder = self._injector.binder - binding, _ = binder.get_binding(key) + binding, _ = binder.get_binding(self._target) provider = binding.provider if not isinstance(provider, ClassProvider): raise Error( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.17.0/injector_test.py new/injector-0.18.1/injector_test.py --- old/injector-0.17.0/injector_test.py 2019-06-15 16:23:44.000000000 +0200 +++ new/injector-0.18.1/injector_test.py 2019-12-10 02:25:28.000000000 +0100 @@ -29,6 +29,7 @@ Scope, InstanceProvider, ClassProvider, + get_bindings, inject, multiprovider, noninjectable, @@ -37,20 +38,20 @@ UnsatisfiedRequirement, CircularDependency, Module, - Key, SingletonScope, ScopeDecorator, AssistedBuilder, - BindingKey, - SequenceKey, - MappingKey, provider, ProviderOf, ClassAssistedBuilder, Error, UnknownArgument, + HAVE_ANNOTATED, ) +if HAVE_ANNOTATED: + from injector import Inject, NoInject + def prepare_basic_injection(): class B: @@ -97,20 +98,20 @@ def test_child_injector_rebinds_arguments_for_parent_scope(): - I = Key("interface") - Cls = Key("test_class") + class Cls: + val = "" - class A: + class A(Cls): @inject - def __init__(self, val: I): + def __init__(self, val: str): self.val = val def configure_parent(binder): binder.bind(Cls, to=A) - binder.bind(I, to="Parent") + binder.bind(str, to="Parent") def configure_child(binder): - binder.bind(I, to="Child") + binder.bind(str, to="Child") parent = Injector(configure_parent) assert parent.get(Cls).val == "Parent" @@ -128,16 +129,6 @@ assert parent.get(A) is child.get(A) -def test_key_cannot_be_instantiated(): - Interface = Key('Interface') - - with pytest.raises(Exception): - Interface() - - with pytest.raises(Exception): - Injector().get(Interface) - - def test_get_default_injected_instances(): A, B = prepare_basic_injection() @@ -472,40 +463,6 @@ assert injector.get(str) == name -def test_bind_using_key(): - Name = Key('name') - Age = Key('age') - - class MyModule(Module): - @provider - def provider_name(self) -> Name: - return 'Bob' - - def configure(self, binder): - binder.bind(Age, to=25) - - injector = Injector(MyModule()) - assert injector.get(Age) == 25 - assert injector.get(Name) == 'Bob' - - -def test_inject_using_key(): - Name = Key('name') - Description = Key('description') - - class MyModule(Module): - @provider - def provide_name(self) -> Name: - return 'Bob' - - @provider - @inject - def provide_description(self, name: Name) -> Description: - return '%s is cool!' % name - - assert Injector(MyModule()).get(Description) == 'Bob is cool!' - - def test_inject_and_provide_coexist_happily(): class MyModule(Module): @provider @@ -525,18 +482,6 @@ assert Injector(MyModule()).get(str) == 'Bob is 25 and weighs 50.0kg' -def test_multibind_old(): - Names = Key('names') - - def configure_one(binder): - binder.multibind(Names, to=['Bob']) - - def configure_two(binder): - binder.multibind(Names, to=['Tom']) - - assert Injector([configure_one, configure_two]).get(Names) == ['Bob', 'Tom'] - - def test_multibind(): Names = NewType('Names', List[str]) Passwords = NewType('Ages', Dict[str, str]) @@ -634,21 +579,6 @@ binder.bind(Passwords, to={}) -def test_provider_sequence_decorator(): - Names = SequenceKey('names') - - class MyModule(Module): - @provider - def bob(self) -> Names: - return ['Bob'] - - @provider - def tom(self) -> Names: - return ['Tom'] - - assert Injector(MyModule()).get(Names) == ['Bob', 'Tom'] - - def test_auto_bind(): class A: pass @@ -723,36 +653,6 @@ injector.get(Handler) -def test_bind_interface_of_list_of_types(): - def configure(binder): - binder.multibind([int], to=[1, 2, 3]) - binder.multibind([int], to=[4, 5, 6]) - - injector = Injector(configure) - assert injector.get([int]) == [1, 2, 3, 4, 5, 6] - - -def test_provider_mapping(): - - StrInt = MappingKey('StrInt') - - def configure(binder): - binder.multibind(StrInt, to={'one': 1}) - binder.multibind(StrInt, to={'two': 2}) - - class MyModule(Module): - @provider - def provide_numbers(self) -> StrInt: - return {'three': 3} - - @provider - def provide_more_numbers(self) -> StrInt: - return {'four': 4} - - injector = Injector([configure, MyModule()]) - assert injector.get(StrInt) == {'one': 1, 'two': 2, 'three': 3, 'four': 4} - - def test_binder_install(): class ModuleA(Module): def configure(self, binder): @@ -874,7 +774,8 @@ def test_assisted_builder_uses_bindings(): - Interface = Key('Interface') + class Interface: + b = 0 def configure(binder): binder.bind(Interface, to=NeedsAssistance) @@ -910,12 +811,6 @@ 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.create(AssistedBuilder[NeedsAssistance]) - assert gen_key() == gen_key() - - class TestThreadSafety: def setup(self): self.event = threading.Event() @@ -990,8 +885,8 @@ def test_callable_provider_injection(): - Name = Key("Name") - Message = Key("Message") + Name = NewType("Name", str) + Message = NewType("Message", str) @inject def create_message(name: Name): @@ -1420,17 +1315,17 @@ # The test taken from Alec Thomas' pull request: https://github.com/alecthomas/injector/pull/73 def test_child_scope(): - TestKey = Key('TestKey') - TestKey2 = Key('TestKey2') + TestKey = NewType('TestKey', str) + TestKey2 = NewType('TestKey2', str) def parent_module(binder): - binder.bind(TestKey, to=object, scope=singleton) + binder.bind(TestKey, to='in parent', scope=singleton) def first_child_module(binder): - binder.bind(TestKey2, to=object, scope=singleton) + binder.bind(TestKey2, to='in first child', scope=singleton) def second_child_module(binder): - binder.bind(TestKey2, to='marker', scope=singleton) + binder.bind(TestKey2, to='in second child', scope=singleton) injector = Injector(modules=[parent_module]) first_child_injector = injector.create_child_injector(modules=[first_child_module]) @@ -1527,3 +1422,50 @@ injector = Injector([configure]) assert injector.get(Data).name == 'data' + + +def test_get_bindings(): + def function1(a: int) -> None: + pass + + assert get_bindings(function1) == {} + + @inject + def function2(a: int) -> None: + pass + + assert get_bindings(function2) == {'a': int} + + @inject + @noninjectable('b') + def function3(a: int, b: str) -> None: + pass + + assert get_bindings(function3) == {'a': int} + + if HAVE_ANNOTATED: + # The simple case of no @inject but injection requested with Inject[...] + def function4(a: Inject[int], b: str) -> None: + pass + + assert get_bindings(function4) == {'a': int} + + # Using @inject with Inject is redundant but it should not break anything + @inject + def function5(a: Inject[int], b: str) -> None: + pass + + assert get_bindings(function5) == {'a': int, 'b': str} + + # We need to be able to exclude a parameter from injection with NoInject + @inject + def function6(a: int, b: NoInject[str]) -> None: + pass + + assert get_bindings(function6) == {'a': int} + + # The presence of NoInject should not trigger anything on its own + def function7(a: int, b: NoInject[str]) -> None: + pass + + assert get_bindings(function7) == {} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.17.0/pytest.ini new/injector-0.18.1/pytest.ini --- old/injector-0.17.0/pytest.ini 2019-06-15 16:23:44.000000000 +0200 +++ new/injector-0.18.1/pytest.ini 2019-12-10 02:25:28.000000000 +0100 @@ -1,4 +1,3 @@ [pytest] -python_files = injector_test.py injector_test_py3.py addopts = -v --tb=native --doctest-glob=*.md --doctest-modules --cov-report term --cov-report html --cov-report xml --cov=injector --cov-branch norecursedirs = __pycache__ *venv* .git build diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.17.0/setup.cfg new/injector-0.18.1/setup.cfg --- old/injector-0.17.0/setup.cfg 2019-06-15 16:23:44.000000000 +0200 +++ new/injector-0.18.1/setup.cfg 2019-12-10 02:25:28.000000000 +0100 @@ -1,5 +1,2 @@ -[pytest] -addopts = -rsxX -q --doctest-modules --doctest-glob=README.md injector injector_test.py README.md - [wheel] universal = True diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.17.0/setup.py new/injector-0.18.1/setup.py --- old/injector-0.17.0/setup.py 2019-06-15 16:23:44.000000000 +0200 +++ new/injector-0.18.1/setup.py 2019-12-10 02:25:28.000000000 +0100 @@ -69,4 +69,5 @@ 'IoC', 'Inversion of Control container', ], + install_requires=['typing_extensions>=3.7.4'], )