Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-injector for openSUSE:Factory checked in at 2022-01-03 10:49:24 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-injector (Old) and /work/SRC/openSUSE:Factory/.python-injector.new.1896 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-injector" Mon Jan 3 10:49:24 2022 rev:7 rq:943322 version:0.19.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-injector/python-injector.changes 2021-03-08 15:18:22.981988154 +0100 +++ /work/SRC/openSUSE:Factory/.python-injector.new.1896/python-injector.changes 2022-01-03 10:49:55.123591918 +0100 @@ -1,0 +2,17 @@ +Fri Dec 31 13:01:20 UTC 2021 - Matej Cepl <mc...@suse.com> + +- Update to 0.19.0: + - Added the license to the source distribution, thanks to + Joshua Adelman + - Added Python 3.9 and 3.10 support, this includes fixing + Python 3.10 compatibility, thanks to Torge Matthies + - Improved the documentation, thanks to Takahiro Kojima + - Improved the source distribution so that it can be used to + build and install wheels, thanks to Janusz Skonieczny + - Added requirements files for easier development, thanks to + Greg Eremeev + - Removed Python 3.5 support + - Fixed a bug where only one of multiple NoInject annotations + was interpreted + +------------------------------------------------------------------- Old: ---- 0.18.3.tar.gz New: ---- 0.19.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-injector.spec ++++++ --- /var/tmp/diff_new_pack.jPPdmI/_old 2022-01-03 10:49:56.199592330 +0100 +++ /var/tmp/diff_new_pack.jPPdmI/_new 2022-01-03 10:49:56.203592332 +0100 @@ -19,7 +19,7 @@ %define skip_python2 1 %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-injector -Version: 0.18.3 +Version: 0.19.0 Release: 0 Summary: Python dependency injection framework, inspired by Guice License: BSD-3-Clause @@ -33,7 +33,7 @@ BuildRequires: python-rpm-macros BuildRequires: ((python3-dataclasses and python3-base < 3.7) or (python36-dataclasses and python36-base)) Requires: python -Requires: python-typing_extensions >= 3.7.4 +Requires: (python-typing_extensions if python-base < 3.8) BuildArch: noarch %python_subpackages ++++++ 0.18.3.tar.gz -> 0.19.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/.github/workflows/ci.yml new/injector-0.19.0/.github/workflows/ci.yml --- old/injector-0.18.3/.github/workflows/ci.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/injector-0.19.0/.github/workflows/ci.yml 2021-12-20 23:18:57.000000000 +0100 @@ -0,0 +1,29 @@ +name: CI + +on: [push, pull_request] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.6, 3.7, 3.8, 3.9, "3.10", pypy3] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + pip install --upgrade -r requirements.txt -r requirements-dev.txt + pip install . + - name: Run tests + run: | + py.test -vv --cov=injector --cov-branch --cov-report html --cov-report term + if which mypy; then mypy injector ; fi + if which black; then black --check . ; fi + check-manifest + - name: Report coverage to Codecov + uses: codecov/codecov-action@v1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/.gitignore new/injector-0.19.0/.gitignore --- old/injector-0.18.3/.gitignore 2020-02-03 15:35:27.000000000 +0100 +++ new/injector-0.19.0/.gitignore 2021-12-20 23:18:57.000000000 +0100 @@ -7,3 +7,6 @@ .mypy_cache/ .pytest_cache/ coverage.xml +/dist/ +/injector.egg-info/ +/.coverage diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/.travis.yml new/injector-0.19.0/.travis.yml --- old/injector-0.18.3/.travis.yml 2020-02-03 15:35:27.000000000 +0100 +++ new/injector-0.19.0/.travis.yml 1970-01-01 01:00:00.000000000 +0100 @@ -1,29 +0,0 @@ -sudo: false -language: python -cache: pip -python: - # There were significant typing-related changes in Python 3.5.4, let's make sure we test with the - # older versions as well - - "3.5.3" - - "3.5" - - "3.6" - - "3.7" - - "3.8" - - "nightly" - - "pypy3.5" - - "pypy3" -matrix: - allow_failures: - - python: "nightly" -install: - - 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 - - if [[ "${TRAVIS_PYTHON_VERSION}" != "pypy"* && "${TRAVIS_PYTHON_VERSION}" != "3.5"* ]] ; then pip install black ; fi -script: - - py.test -vv --cov=injector --cov-branch --cov-report html --cov-report term - - if [[ "${TRAVIS_PYTHON_VERSION}" != "pypy"* ]] ; then mypy injector ; fi - - if [[ "${TRAVIS_PYTHON_VERSION}" != "pypy"* && "${TRAVIS_PYTHON_VERSION}" != "3.5"* ]] ; then black --check . ; fi -after_success: - - coveralls diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/CHANGES new/injector-0.19.0/CHANGES --- old/injector-0.18.3/CHANGES 2020-02-03 15:35:27.000000000 +0100 +++ new/injector-0.19.0/CHANGES 2021-12-20 23:18:57.000000000 +0100 @@ -1,6 +1,24 @@ Injector Change Log =================== +0.19.0 +------ + +- Added the license to the source distribution, thanks to Joshua Adelman +- Added Python 3.9 and 3.10 support, this includes fixing Python 3.10 compatibility, thanks to Torge Matthies +- Improved the documentation, thanks to Takahiro Kojima +- Improved the source distribution so that it can be used to build and install wheels, thanks to Janusz Skonieczny +- Added requirements files for easier development, thanks to Greg Eremeev + +Backwards incompatible: + +- Removed Python 3.5 support + +0.18.4 +------ + +- Fixed a bug where only one of multiple NoInject annotations was interpreted + 0.18.3 ------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/MANIFEST.in new/injector-0.19.0/MANIFEST.in --- old/injector-0.18.3/MANIFEST.in 2020-02-03 15:35:27.000000000 +0100 +++ new/injector-0.19.0/MANIFEST.in 2021-12-20 23:18:57.000000000 +0100 @@ -1 +1,12 @@ +include *.py +include *.toml +include *.txt +include CHANGES +include COPYING include README.md +include mypy.ini +include pytest.ini +recursive-include docs *.html +recursive-include docs *.py +recursive-include docs *.rst +recursive-include docs Makefile diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/README.md new/injector-0.19.0/README.md --- old/injector-0.18.3/README.md 2020-02-03 15:35:27.000000000 +0100 +++ new/injector-0.19.0/README.md 2021-12-20 23:18:57.000000000 +0100 @@ -1,8 +1,8 @@ Injector - Python dependency injection framework, inspired by Guice =================================================================== -[](https://travis-ci.org/alecthomas/injector) -[](https://coveralls.io/github/alecthomas/injector?branch=master) +[](https://github.com/alecthomas/injector/actions?query=workflow%3ACI+branch%3Amaster) +[](https://codecov.io/gh/alecthomas/injector) Introduction ------------ @@ -51,21 +51,28 @@ 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. + +* The client code only knows about dependency injection to the extent it needs ????? + [`inject`](https://injector.readthedocs.io/en/latest/api.html#injector.inject), + [`Inject`](https://injector.readthedocs.io/en/latest/api.html#injector.Inject) and + [`NoInject`](https://injector.readthedocs.io/en/latest/api.html#injector.NoInject) are simple markers + that don't really do anything on their own and your code can run just fine without Injector + orchestrating things. ### How to get Injector? * GitHub (code repository, issues): https://github.com/alecthomas/injector -* PyPI (installable, stable distributions): https://pypi.python.org/pypi/injector. You can install it using pip: +* PyPI (installable, stable distributions): https://pypi.org/project/injector/. You can install it using pip: ```bash pip install injector ``` -* Documentation: http://injector.readthedocs.org -* Change log: http://injector.readthedocs.io/en/latest/changelog.html +* Documentation: https://injector.readthedocs.org +* Change log: https://injector.readthedocs.io/en/latest/changelog.html -Injector works with CPython 3.5+ and PyPy 3 implementing Python 3.5+. +Injector works with CPython 3.6+ and PyPy 3 implementing Python 3.6+. A Quick Example --------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/docs/_templates/sidebar.html new/injector-0.19.0/docs/_templates/sidebar.html --- old/injector-0.18.3/docs/_templates/sidebar.html 2020-02-03 15:35:27.000000000 +0100 +++ new/injector-0.19.0/docs/_templates/sidebar.html 2021-12-20 23:18:57.000000000 +0100 @@ -2,6 +2,6 @@ <p>Dependency Injection Framework</p> <ul> <li><a href="https://github.com/alecthomas/injector">Injector at GitHub</a></li> - <li><a href="https://pypi.python.org/pypi/injector">Injector at PyPI</a></li> - <li><a href="http://injector.readthedocs.org/en/latest/">Injector documentation</a></li> + <li><a href="https://pypi.org/project/injector/">Injector at PyPI</a></li> + <li><a href="https://injector.readthedocs.org/en/latest/">Injector documentation</a></li> </ul> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/docs/index.rst new/injector-0.19.0/docs/index.rst --- old/injector-0.18.3/docs/index.rst 2020-02-03 15:35:27.000000000 +0100 +++ new/injector-0.19.0/docs/index.rst 2021-12-20 23:18:57.000000000 +0100 @@ -6,13 +6,14 @@ Welcome to Injector's documentation! ==================================== -.. image:: https://travis-ci.org/alecthomas/injector.png?branch=master +.. image:: https://github.com/alecthomas/injector/workflows/CI/badge.svg :alt: Build status - :target: https://travis-ci.org/alecthomas/injector + :target: https://github.com/alecthomas/injector/actions?query=workflow%3ACI+branch%3Amaster -.. image:: https://coveralls.io/repos/github/alecthomas/injector/badge.svg?branch=master +.. image:: https://codecov.io/gh/alecthomas/injector/branch/master/graph/badge.svg :alt: Covergage status - :target: https://coveralls.io/github/alecthomas/injector?branch=master + :target: https://codecov.io/gh/alecthomas/injector + GitHub (code repository, issues): https://github.com/alecthomas/injector @@ -20,7 +21,7 @@ pip install injector -Injector works with CPython 3.5+ and PyPy 3 implementing Python 3.5+. +Injector works with CPython 3.6+ and PyPy 3 implementing Python 3.6+. Introduction ------------ @@ -51,7 +52,7 @@ class MyClass: @inject - def __init__(t: SomeType): + def __init__(self, t: SomeType): # ... MyClass() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/docs/practices.rst new/injector-0.19.0/docs/practices.rst --- old/injector-0.18.3/docs/practices.rst 2020-02-03 15:35:27.000000000 +0100 +++ new/injector-0.19.0/docs/practices.rst 2021-12-20 23:18:57.000000000 +0100 @@ -126,11 +126,11 @@ from threading import Thread from time import sleep - from injector import inject, Injector, Key, Module, provider + from injector import inject, Injector, Module, provider - SubA = Key('SubA') - A = Key('A') - B = Key('B') + class A: pass + class SubA(A): pass + class B: pass class BadModule(Module): @@ -146,11 +146,11 @@ sleep(1) # This never executes - return 'suba' + return SubA() @provider def provide_b(self) -> B: - return 'b' + return B() injector = Injector([BadModule]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/docs/terminology.rst new/injector-0.19.0/docs/terminology.rst --- old/injector-0.18.3/docs/terminology.rst 2020-02-03 15:35:27.000000000 +0100 +++ new/injector-0.19.0/docs/terminology.rst 2021-12-20 23:18:57.000000000 +0100 @@ -29,15 +29,6 @@ .. seealso:: :ref:`scopes` -Keys -```` - -`Key` may be used to create unique types as necessary:: - - >>> from injector import Key - >>> Name = Key('name') - >>> Description = Key('description') - Binding ``````` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/injector/__init__.py new/injector-0.19.0/injector/__init__.py --- old/injector-0.18.3/injector/__init__.py 2020-02-03 15:35:27.000000000 +0100 +++ new/injector-0.19.0/injector/__init__.py 2021-12-20 23:18:57.000000000 +0100 @@ -33,22 +33,38 @@ List, Optional, overload, + Set, Tuple, Type, TypeVar, + TYPE_CHECKING, Union, ) -from typing_extensions import NoReturn +try: + from typing import NoReturn +except ImportError: + from typing_extensions import NoReturn HAVE_ANNOTATED = sys.version_info >= (3, 7, 0) -if HAVE_ANNOTATED: +# This is a messy, type-wise, because we not only have two potentially conflicting imports here +# but we also define our own versions in the else block in case we operate on Python 3.6 +# which didn't get Annotated support in get_type_hints(). The easiest way to make mypy +# happy here is to tell it the versions from typing_extensions are canonical. Since this +# typing_extensions import is only for mypy it'll work even without typing_extensions actually +# installed so all's good. +if TYPE_CHECKING: + from typing_extensions import _AnnotatedAlias, Annotated, get_type_hints +elif 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 + try: + from typing import _AnnotatedAlias, Annotated, get_type_hints + except ImportError: + from typing_extensions import _AnnotatedAlias, Annotated, get_type_hints else: - class Annotated: # type: ignore + class Annotated: pass from typing import get_type_hints as _get_type_hints @@ -62,11 +78,8 @@ return _get_type_hints(obj, globalns, localns) -TYPING353 = hasattr(Union[str, int], '__origin__') - - __author__ = 'Alec Thomas <a...@swapoff.org>' -__version__ = '0.18.3' +__version__ = '0.19.0' __version_tag__ = '' log = logging.getLogger('injector') @@ -110,7 +123,7 @@ InjectT = TypeVar('InjectT') Inject = Annotated[InjectT, _inject_marker] """An experimental way to declare injectable dependencies utilizing a `PEP 593`_ implementation - in `typing_extensions`. + in Python 3.9 and backported to Python 3.7+ in `typing_extensions`. Those two declarations are equivalent:: @@ -150,7 +163,7 @@ NoInject = Annotated[InjectT, _noinject_marker] """An experimental way to declare noninjectable dependencies utilizing a `PEP 593`_ implementation - in `typing_extensions`. + in Python 3.9 and backported to Python 3.7+ 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 @@ -221,15 +234,9 @@ return self.args[0] instance, method, args, kwargs, original_error, stack = self.args - if hasattr(method, 'im_class'): - instance = method.__self__ - method_name = method.__func__.__name__ - else: - method_name = method.__name__ - cls = instance.__class__.__name__ if instance is not None else '' - full_method = '.'.join((cls, method_name)).strip('.') + full_method = '.'.join((cls, method.__name__)).strip('.') parameters = ', '.join( itertools.chain( @@ -305,7 +312,7 @@ providing providing False - """ + """ def __init__(self, callable: Callable[..., T]): self._callable = callable @@ -350,8 +357,10 @@ class ListOfProviders(Provider, Generic[T]): """Provide a list of instances via other Providers.""" + _providers: List[Provider[T]] + def __init__(self) -> None: - self._providers = [] # type: List[Provider[T]] + self._providers = [] def append(self, provider: Provider[T]) -> None: self._providers.append(provider) @@ -372,7 +381,7 @@ """A provider for map bindings.""" def get(self, injector: 'Injector') -> Dict[str, T]: - map = {} # type: Dict[str, T] + map: Dict[str, T] = {} for provider in self._providers: map.update(provider.get(injector)) return map @@ -399,6 +408,8 @@ to instantiate it on your own. """ + _bindings: Dict[type, Binding] + @private def __init__(self, injector: 'Injector', auto_bind: bool = True, parent: 'Binder' = None) -> None: """Create a new Binder. @@ -409,7 +420,7 @@ """ self.injector = injector self._auto_bind = auto_bind - self._bindings = {} # type: Dict[type, Binding] + self._bindings = {} self.parent = parent def bind( @@ -507,13 +518,14 @@ :param scope: Optional Scope in which to bind. """ if interface not in self._bindings: + provider: ListOfProviders if ( isinstance(interface, dict) or isinstance(interface, type) and issubclass(interface, dict) or _get_origin(_punch_through_alias(interface)) is dict ): - provider = MapBindProvider() # type: ListOfProviders + provider = MapBindProvider() else: provider = MultiBindProvider() binding = self.create_binding(interface, provider, scope) @@ -662,47 +674,45 @@ return any(_is_specialization(interface, cls) for cls in [AssistedBuilder, ProviderOf]) -if TYPING353: - - def _is_specialization(cls: type, generic_class: Any) -> bool: - # Starting with typing 3.5.3/Python 3.6 it is no longer necessarily true that - # 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 = cast(Any, cls).__origin__ - if not inspect.isclass(generic_class): - generic_class = type(generic_class) - if not inspect.isclass(origin): - origin = type(origin) - # __origin__ is generic_class is a special case to handle Union as - # Union cannot be used in issubclass() check (it raises an exception - # by design). - return origin is generic_class or issubclass(origin, generic_class) - - -else: - # To maintain compatibility we fall back to an issubclass check. - def _is_specialization(cls: type, generic_class: Any) -> bool: - return isinstance(cls, type) and cls is not Any and issubclass(cls, generic_class) +def _is_specialization(cls: type, generic_class: Any) -> bool: + # Starting with typing 3.5.3/Python 3.6 it is no longer necessarily true that + # 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 = cast(Any, cls).__origin__ + if not inspect.isclass(generic_class): + generic_class = type(generic_class) + if not inspect.isclass(origin): + origin = type(origin) + # __origin__ is generic_class is a special case to handle Union as + # Union cannot be used in issubclass() check (it raises an exception + # by design). + return origin is generic_class or issubclass(origin, generic_class) def _punch_through_alias(type_: Any) -> type: - if getattr(type_, '__qualname__', '') == 'NewType.<locals>.new_type': + if ( + sys.version_info < (3, 10) + and getattr(type_, '__qualname__', '') == 'NewType.<locals>.new_type' + or sys.version_info >= (3, 10) + and type(type_).__module__ == 'typing' + and type(type_).__name__ == 'NewType' + ): return type_.__supertype__ else: return type_ -def _get_origin(type_: type) -> type: +def _get_origin(type_: type) -> Optional[type]: origin = getattr(type_, '__origin__', None) # Older typing behaves differently there and stores Dict and List as origin, we need to be flexible. if origin is List: @@ -780,8 +790,10 @@ True """ + _context: Dict[type, Provider] + def configure(self) -> None: - self._context = {} # type: Dict[type, Provider] + self._context = {} @synchronized(lock) def get(self, key: Type[T], provider: Provider[T]) -> Provider[T]: @@ -865,6 +877,9 @@ ``use_annotations`` parameter is removed """ + _stack: Tuple[Tuple[object, Callable, Tuple[Tuple[str, type], ...]], ...] + binder: Binder + def __init__( self, modules: Union[_InstallableModuleType, Iterable[_InstallableModuleType]] = None, @@ -873,14 +888,12 @@ ) -> None: # Stack of keys currently being injected. Used to detect circular # dependencies. - self._stack = () # type: Tuple[Tuple[object, Callable, Tuple[Tuple[str, type], ...]], ...] + self._stack = () self.parent = parent # Binder - self.binder = Binder( - self, auto_bind=auto_bind, parent=parent.binder if parent is not None else None - ) # type: Binder + self.binder = Binder(self, auto_bind=auto_bind, parent=parent.binder if parent is not None else None) if not modules: modules = [] @@ -966,15 +979,13 @@ CallError(cls, getattr(cls.__new__, '__func__', cls.__new__), (), {}, e, self._stack), maximum_frames=2, ) + init = cls.__init__ try: - # On Python 3.5.3 calling call_with_injection() with object.__init__ would fail further - # down the line as get_type_hints(object.__init__) raises an exception until Python 3.5.4. - # And since object.__init__ doesn't do anything useful and can't have any injectable - # arguments we can skip calling it altogether. See GH-135 for more information. - if cls.__init__ is not object.__init__: - self.call_with_injection(cls.__init__, self_=instance, kwargs=additional_kwargs) + self.call_with_injection(init, self_=instance, kwargs=additional_kwargs) except TypeError as e: - reraise(e, CallError(instance, instance.__init__.__func__, (), additional_kwargs, e, self._stack)) + # Mypy says "Cannot access "__init__" directly" + init_function = instance.__init__.__func__ # type: ignore + reraise(e, CallError(instance, init_function, (), additional_kwargs, e, self._stack)) return instance def call_with_injection( @@ -1051,7 +1062,7 @@ try: for arg, interface in bindings.items(): try: - instance = self.get(interface) # type: Any + instance: Any = self.get(interface) except UnsatisfiedRequirement as e: if not e.owner: e = UnsatisfiedRequirement(owner_key, e.interface) @@ -1147,7 +1158,7 @@ read_and_store_bindings( callable, _infer_injected_bindings(callable, only_explicit_bindings=look_for_explicit_bindings) ) - noninjectables = getattr(callable, '__noninjectables__', set()) + noninjectables: Set[str] = getattr(callable, '__noninjectables__', set()) return {k: v for k, v in cast(Any, callable).__bindings__.items() if k not in noninjectables} @@ -1188,14 +1199,9 @@ if only_explicit_bindings and _inject_marker not in metadata or _noinject_marker in metadata: del bindings[k] - break - - if _is_specialization(v, Union): + elif _is_specialization(v, Union): # We don't treat Optional parameters in any special way at the moment. - if TYPING353: - union_members = v.__args__ - else: - union_members = v.__union_params__ + union_members = v.__args__ new_members = tuple(set(union_members) - {type(None)}) # mypy stared complaining about this line for some reason: # error: Variable "new_members" is not valid as a type @@ -1397,7 +1403,7 @@ if arg not in argspec.args and arg not in argspec.kwonlyargs: raise UnknownArgument('Unable to mark unknown argument %s ' 'as non-injectable.' % arg) - existing = getattr(function, '__noninjectables__', set()) + existing: Set[str] = getattr(function, '__noninjectables__', set()) merged = existing | set(args) cast(Any, function).__noninjectables__ = merged return function @@ -1481,19 +1487,19 @@ class ProviderOf(Generic[T]): """Can be used to get a provider of an interface, for example: - >>> def provide_int(): - ... print('providing') - ... return 123 - >>> - >>> def configure(binder): - ... binder.bind(int, to=provide_int) - >>> - >>> injector = Injector(configure) - >>> provider = injector.get(ProviderOf[int]) - >>> value = provider.get() - providing - >>> value - 123 + >>> def provide_int(): + ... print('providing') + ... return 123 + >>> + >>> def configure(binder): + ... binder.bind(int, to=provide_int) + >>> + >>> injector = Injector(configure) + >>> provider = injector.get(ProviderOf[int]) + >>> value = provider.get() + providing + >>> value + 123 """ def __init__(self, injector: Injector, interface: Type[T]): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/injector_test.py new/injector-0.19.0/injector_test.py --- old/injector-0.18.3/injector_test.py 2020-02-03 15:35:27.000000000 +0100 +++ new/injector-0.19.0/injector_test.py 2021-12-20 23:18:57.000000000 +0100 @@ -53,17 +53,15 @@ from injector import Inject, NoInject -def prepare_basic_injection(): - class B: - pass +class EmptyClass: + pass - class A: - @inject - def __init__(self, b: B): - """Construct a new A.""" - self.b = b - return A, B +class DependsOnEmptyClass: + @inject + def __init__(self, b: EmptyClass): + """Construct a new DependsOnEmptyClass.""" + self.b = b def prepare_nested_injectors(): @@ -130,11 +128,9 @@ def test_get_default_injected_instances(): - A, B = prepare_basic_injection() - def configure(binder): - binder.bind(A) - binder.bind(B) + binder.bind(DependsOnEmptyClass) + binder.bind(EmptyClass) injector = Injector(configure) assert injector.get(Injector) is injector @@ -142,15 +138,13 @@ def test_instantiate_injected_method(): - A, _ = prepare_basic_injection() - a = A('Bob') + a = DependsOnEmptyClass('Bob') assert a.b == 'Bob' def test_method_decorator_is_wrapped(): - A, _ = prepare_basic_injection() - assert A.__init__.__doc__ == 'Construct a new A.' - assert A.__init__.__name__ == '__init__' + assert DependsOnEmptyClass.__init__.__doc__ == 'Construct a new DependsOnEmptyClass.' + assert DependsOnEmptyClass.__init__.__name__ == '__init__' def test_decorator_works_for_function_with_no_args(): @@ -178,121 +172,104 @@ def test_inject_direct(): - A, B = prepare_basic_injection() - def configure(binder): - binder.bind(A) - binder.bind(B) + binder.bind(DependsOnEmptyClass) + binder.bind(EmptyClass) injector = Injector(configure) - a = injector.get(A) - assert isinstance(a, A) - assert isinstance(a.b, B) + a = injector.get(DependsOnEmptyClass) + assert isinstance(a, DependsOnEmptyClass) + assert isinstance(a.b, EmptyClass) def test_configure_multiple_modules(): - A, B = prepare_basic_injection() - def configure_a(binder): - binder.bind(A) + binder.bind(DependsOnEmptyClass) def configure_b(binder): - binder.bind(B) + binder.bind(EmptyClass) injector = Injector([configure_a, configure_b]) - a = injector.get(A) - assert isinstance(a, A) - assert isinstance(a.b, B) + a = injector.get(DependsOnEmptyClass) + assert isinstance(a, DependsOnEmptyClass) + assert isinstance(a.b, EmptyClass) def test_inject_with_missing_dependency(): - A, _ = prepare_basic_injection() - def configure(binder): - binder.bind(A) + binder.bind(DependsOnEmptyClass) injector = Injector(configure, auto_bind=False) with pytest.raises(UnsatisfiedRequirement): - injector.get(A) + injector.get(EmptyClass) def test_inject_named_interface(): - class B: - pass - class A: @inject - def __init__(self, b: B): + def __init__(self, b: EmptyClass): self.b = b def configure(binder): binder.bind(A) - binder.bind(B) + binder.bind(EmptyClass) injector = Injector(configure) a = injector.get(A) assert isinstance(a, A) - assert isinstance(a.b, B) + assert isinstance(a.b, EmptyClass) -def prepare_transitive_injection(): - class C: - pass +class TransitiveC: + pass - class B: - @inject - def __init__(self, c: C): - self.c = c - class A: - @inject - def __init__(self, b: B): - self.b = b +class TransitiveB: + @inject + def __init__(self, c: TransitiveC): + self.c = c - return A, B, C +class TransitiveA: + @inject + def __init__(self, b: TransitiveB): + self.b = b -def test_transitive_injection(): - A, B, C = prepare_transitive_injection() +def test_transitive_injection(): def configure(binder): - binder.bind(A) - binder.bind(B) - binder.bind(C) + binder.bind(TransitiveA) + binder.bind(TransitiveB) + binder.bind(TransitiveC) injector = Injector(configure) - a = injector.get(A) - assert isinstance(a, A) - assert isinstance(a.b, B) - assert isinstance(a.b.c, C) + a = injector.get(TransitiveA) + assert isinstance(a, TransitiveA) + assert isinstance(a.b, TransitiveB) + assert isinstance(a.b.c, TransitiveC) def test_transitive_injection_with_missing_dependency(): - A, B, _ = prepare_transitive_injection() - def configure(binder): - binder.bind(A) - binder.bind(B) + binder.bind(TransitiveA) + binder.bind(TransitiveB) injector = Injector(configure, auto_bind=False) with pytest.raises(UnsatisfiedRequirement): - injector.get(A) + injector.get(TransitiveA) with pytest.raises(UnsatisfiedRequirement): - injector.get(B) + injector.get(TransitiveB) def test_inject_singleton(): - class B: - pass - class A: @inject - def __init__(self, b: B): + def __init__(self, b: EmptyClass): self.b = b def configure(binder): binder.bind(A) - binder.bind(B, scope=SingletonScope) + binder.bind(EmptyClass, scope=SingletonScope) injector1 = Injector(configure) a1 = injector1.get(A) @@ -300,19 +277,20 @@ assert a1.b is a2.b -def test_inject_decorated_singleton_class(): - @singleton - class B: - pass +@singleton +class SingletonB: + pass + +def test_inject_decorated_singleton_class(): class A: @inject - def __init__(self, b: B): + def __init__(self, b: SingletonB): self.b = b def configure(binder): binder.bind(A) - binder.bind(B) + binder.bind(SingletonB) injector1 = Injector(configure) a1 = injector1.get(A) @@ -348,97 +326,105 @@ assert a2 is not a3[0] and a3[0] is not None -def test_injecting_interface_implementation(): - class Interface: - pass +class Interface2: + pass + +def test_injecting_interface_implementation(): class Implementation: pass class A: @inject - def __init__(self, i: Interface): + def __init__(self, i: Interface2): self.i = i def configure(binder): binder.bind(A) - binder.bind(Interface, to=Implementation) + binder.bind(Interface2, to=Implementation) injector = Injector(configure) a = injector.get(A) assert isinstance(a.i, Implementation) -def test_cyclic_dependencies(): - class Interface: - pass +class CyclicInterface: + pass - class A: - @inject - def __init__(self, i: Interface): - self.i = i - class B: - @inject - def __init__(self, a: A): - self.a = a +class CyclicA: + @inject + def __init__(self, i: CyclicInterface): + self.i = i + + +class CyclicB: + @inject + def __init__(self, a: CyclicA): + self.a = a + +def test_cyclic_dependencies(): def configure(binder): - binder.bind(Interface, to=B) - binder.bind(A) + binder.bind(CyclicInterface, to=CyclicB) + binder.bind(CyclicA) injector = Injector(configure) with pytest.raises(CircularDependency): - injector.get(A) + injector.get(CyclicA) -def test_dependency_cycle_can_be_worked_broken_by_assisted_building(): - class Interface: - pass +class CyclicInterface2: + pass - class A: - @inject - def __init__(self, i: Interface): - self.i = i - class B: - @inject - def __init__(self, a_builder: AssistedBuilder[A]): - self.a = a_builder.build(i=self) +class CyclicA2: + @inject + def __init__(self, i: CyclicInterface2): + self.i = i + +class CyclicB2: + @inject + def __init__(self, a_builder: AssistedBuilder[CyclicA2]): + self.a = a_builder.build(i=self) + + +def test_dependency_cycle_can_be_worked_broken_by_assisted_building(): def configure(binder): - binder.bind(Interface, to=B) - binder.bind(A) + binder.bind(CyclicInterface2, to=CyclicB2) + binder.bind(CyclicA2) injector = Injector(configure) # Previously it'd detect a circular dependency here: - # 1. Constructing A requires Interface (bound to B) - # 2. Constructing B requires assisted build of A - # 3. Constructing A triggers circular dependency check - assert isinstance(injector.get(A), A) + # 1. Constructing CyclicA2 requires CyclicInterface2 (bound to CyclicB2) + # 2. Constructing CyclicB2 requires assisted build of CyclicA2 + # 3. Constructing CyclicA2 triggers circular dependency check + assert isinstance(injector.get(CyclicA2), CyclicA2) -def test_that_injection_is_lazy(): - class Interface: - constructed = False +class Interface5: + constructed = False + + def __init__(self): + Interface5.constructed = True - def __init__(self): - Interface.constructed = True +def test_that_injection_is_lazy(): class A: @inject - def __init__(self, i: Interface): + def __init__(self, i: Interface5): self.i = i def configure(binder): - binder.bind(Interface) + binder.bind(Interface5) binder.bind(A) injector = Injector(configure) - assert not (Interface.constructed) + assert not (Interface5.constructed) injector.get(A) - assert Interface.constructed + assert Interface5.constructed def test_module_provider(): @@ -482,10 +468,11 @@ assert Injector(MyModule()).get(str) == 'Bob is 25 and weighs 50.0kg' -def test_multibind(): - Names = NewType('Names', List[str]) - Passwords = NewType('Ages', Dict[str, str]) +Names = NewType('Names', List[str]) +Passwords = NewType('Ages', Dict[str, str]) + +def test_multibind(): # First let's have some explicit multibindings def configure(binder): binder.multibind(List[str], to=['not a name']) @@ -597,45 +584,51 @@ assert isinstance(injector.get(AliasOfA), A) -def test_custom_scope(): - class RequestScope(Scope): - def configure(self): - self.context = None - - @contextmanager - def __call__(self, request): - assert self.context is None - self.context = {} - binder = self.injector.get(Binder) - binder.bind(Request, to=request, scope=RequestScope) - yield - self.context = None - - def get(self, key, provider): - if self.context is None: - raise UnsatisfiedRequirement(None, key) - try: - return self.context[key] - except KeyError: - provider = InstanceProvider(provider.get(self.injector)) - self.context[key] = provider - return provider +class Request: + pass - request = ScopeDecorator(RequestScope) - class Request: - pass +class RequestScope(Scope): + def configure(self): + self.context = None - @request - class Handler: - def __init__(self, request): - self.request = request + @contextmanager + def __call__(self, request): + assert self.context is None + self.context = {} + binder = self.injector.get(Binder) + binder.bind(Request, to=request, scope=RequestScope) + yield + self.context = None - class RequestModule(Module): - @provider - @inject - def handler(self, request: Request) -> Handler: - return Handler(request) + def get(self, key, provider): + if self.context is None: + raise UnsatisfiedRequirement(None, key) + try: + return self.context[key] + except KeyError: + provider = InstanceProvider(provider.get(self.injector)) + self.context[key] = provider + return provider + + +request = ScopeDecorator(RequestScope) + + +@request +class Handler: + def __init__(self, request): + self.request = request + + +class RequestModule(Module): + @provider + @inject + def handler(self, request: Request) -> Handler: + return Handler(request) + + +def test_custom_scope(): injector = Injector([RequestModule()], auto_bind=False) @@ -716,16 +709,18 @@ assert isinstance(binder.provider_for(A, None).get(injector), A) -def test_injecting_undecorated_class_with_missing_dependencies_raises_the_right_error(): - class ClassA: - def __init__(self, parameter): - pass +class ClassA: + def __init__(self, parameter): + pass + + +class ClassB: + @inject + def __init__(self, a: ClassA): + pass - class ClassB: - @inject - def __init__(self, a: ClassA): - pass +def test_injecting_undecorated_class_with_missing_dependencies_raises_the_right_error(): injector = Injector() try: injector.get(ClassB) @@ -773,10 +768,11 @@ assert (x.obj.a, x.obj.b) == (str(), 234) -def test_assisted_builder_uses_bindings(): - class Interface: - b = 0 +class Interface: + b = 0 + +def test_assisted_builder_uses_bindings(): def configure(binder): binder.bind(Interface, to=NeedsAssistance) @@ -884,10 +880,11 @@ injector.get(X) -def test_callable_provider_injection(): - Name = NewType("Name", str) - Message = NewType("Message", str) +Name = NewType("Name", str) +Message = NewType("Message", str) + +def test_callable_provider_injection(): @inject def create_message(name: Name): return "Hello, " + name @@ -991,47 +988,48 @@ assert injector.get(bytes) == text -def test_class_assisted_builder_of_partially_injected_class_old(): - class A: - pass +class PartialB: + @inject + def __init__(self, a: EmptyClass, b: str): + self.a = a + self.b = b - class B: - @inject - def __init__(self, a: A, b: str): - self.a = a - self.b = b +def test_class_assisted_builder_of_partially_injected_class_old(): class C: @inject - def __init__(self, a: A, builder: ClassAssistedBuilder[B]): + def __init__(self, a: EmptyClass, builder: ClassAssistedBuilder[PartialB]): self.a = a self.b = builder.build(b='C') c = Injector().get(C) assert isinstance(c, C) - assert isinstance(c.b, B) - assert isinstance(c.b.a, A) + assert isinstance(c.b, PartialB) + assert isinstance(c.b.a, EmptyClass) -def test_implicit_injection_for_python3(): - class A: - pass +class ImplicitA: + pass - class B: - @inject - def __init__(self, a: A): - self.a = a - class C: - @inject - def __init__(self, b: B): - self.b = b +class ImplicitB: + @inject + def __init__(self, a: ImplicitA): + self.a = a + + +class ImplicitC: + @inject + def __init__(self, b: ImplicitB): + self.b = b + +def test_implicit_injection_for_python3(): injector = Injector() - c = injector.get(C) - assert isinstance(c, C) - assert isinstance(c.b, B) - assert isinstance(c.b.a, A) + c = injector.get(ImplicitC) + assert isinstance(c, ImplicitC) + assert isinstance(c.b, ImplicitB) + assert isinstance(c.b.a, ImplicitA) def test_annotation_based_injection_works_in_provider_methods(): @@ -1053,21 +1051,23 @@ assert injector.get(object) is injector.get(object) -def test_assisted_building_is_supported(): - class Fetcher: - def fetch(self, user_id): - assert user_id == 333 - return {'name': 'John'} - - class Processor: - @noninjectable('provider_id') - @inject - @noninjectable('user_id') - def __init__(self, fetcher: Fetcher, user_id: int, provider_id: str): - assert provider_id == 'not injected' - data = fetcher.fetch(user_id) - self.name = data['name'] +class Fetcher: + def fetch(self, user_id): + assert user_id == 333 + return {'name': 'John'} + +class Processor: + @noninjectable('provider_id') + @inject + @noninjectable('user_id') + def __init__(self, fetcher: Fetcher, user_id: int, provider_id: str): + assert provider_id == 'not injected' + data = fetcher.fetch(user_id) + self.name = data['name'] + + +def test_assisted_building_is_supported(): def configure(binder): binder.bind(int, to=897) binder.bind(str, to='injected') @@ -1294,26 +1294,24 @@ del X -def test_class_assisted_builder_of_partially_injected_class(): - class A: - pass +class AssistedB: + @inject + def __init__(self, a: EmptyClass, b: str): + self.a = a + self.b = b - class B: - @inject - def __init__(self, a: A, b: str): - self.a = a - self.b = b +def test_class_assisted_builder_of_partially_injected_class(): class C: @inject - def __init__(self, a: A, builder: ClassAssistedBuilder[B]): + def __init__(self, a: EmptyClass, builder: ClassAssistedBuilder[AssistedB]): self.a = a self.b = builder.build(b='C') c = Injector().get(C) assert isinstance(c, C) - assert isinstance(c.b, B) - assert isinstance(c.b.a, A) + assert isinstance(c.b, AssistedB) + assert isinstance(c.b.a, EmptyClass) # The test taken from Alec Thomas' pull request: https://github.com/alecthomas/injector/pull/73 @@ -1380,18 +1378,15 @@ def test_using_an_assisted_builder_with_a_provider_raises_an_injector_error(): - class A: - pass - class MyModule(Module): @provider - def provide_a(self, builder: AssistedBuilder[A]) -> A: + def provide_a(self, builder: AssistedBuilder[EmptyClass]) -> EmptyClass: return builder.build() injector = Injector(MyModule) with pytest.raises(Error): - injector.get(A) + injector.get(EmptyClass) def test_newtype_integration_works(): @@ -1446,6 +1441,14 @@ assert get_bindings(function3) == {'a': int} + # Let's verify that the inject/noninjectable ordering doesn't matter + @noninjectable('b') + @inject + def function3b(a: int, b: str) -> None: + pass + + assert get_bindings(function3b) == {'a': int} + if HAVE_ANNOTATED: # The simple case of no @inject but injection requested with Inject[...] def function4(a: Inject[int], b: str) -> None: @@ -1472,3 +1475,12 @@ pass assert get_bindings(function7) == {} + + # There was a bug where in case of multiple NoInject-decorated parameters only the first one was + # actually made noninjectable and we tried to inject something we couldn't possibly provide + # into the second one. + @inject + def function8(a: NoInject[int], b: NoInject[int]) -> None: + pass + + assert get_bindings(function8) == {} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/requirements-dev.txt new/injector-0.19.0/requirements-dev.txt --- old/injector-0.18.3/requirements-dev.txt 1970-01-01 01:00:00.000000000 +0100 +++ new/injector-0.19.0/requirements-dev.txt 2021-12-20 23:18:57.000000000 +0100 @@ -0,0 +1,6 @@ +pytest +pytest-cov>=2.5.1 +dataclasses;python_version<"3.7" +mypy;implementation_name=="cpython" +black;implementation_name=="cpython" +check-manifest diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/requirements.txt new/injector-0.19.0/requirements.txt --- old/injector-0.18.3/requirements.txt 1970-01-01 01:00:00.000000000 +0100 +++ new/injector-0.19.0/requirements.txt 2021-12-20 23:18:57.000000000 +0100 @@ -0,0 +1 @@ +typing_extensions>=3.7.4;python_version<"3.9" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.18.3/setup.py new/injector-0.19.0/setup.py --- old/injector-0.18.3/setup.py 2020-02-03 15:35:27.000000000 +0100 +++ new/injector-0.19.0/setup.py 2021-12-20 23:18:57.000000000 +0100 @@ -6,6 +6,13 @@ warnings.filterwarnings("always", module=__name__) +def obtain_requirements(file_name): + with open(file_name) as fd_in: + for line in fd_in: + if '#' not in line: + yield line.strip() + + class PyTest(Command): user_options = [] @@ -34,6 +41,11 @@ version = read_injector_variable('__version__') version_tag = read_injector_variable('__version_tag__') + +requirements = list(obtain_requirements('requirements.txt')) +requirements_dev = list(obtain_requirements('requirements-dev.txt')) + + try: import pypandoc @@ -48,8 +60,8 @@ setup( name='injector', - url='http://github.com/alecthomas/injector', - download_url='http://pypi.python.org/pypi/injector', + url='https://github.com/alecthomas/injector', + download_url='https://pypi.org/project/injector/', version=version, options=dict(egg_info=dict(tag_build=version_tag)), description=description, @@ -61,6 +73,7 @@ author='Alec Thomas', author_email='a...@swapoff.org', cmdclass={'test': PyTest}, + extras_require={'dev': requirements_dev}, keywords=[ 'Dependency Injection', 'DI', @@ -69,5 +82,5 @@ 'IoC', 'Inversion of Control container', ], - install_requires=['typing_extensions>=3.7.4'], + install_requires=requirements, )