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-09-29 18:13:53 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-injector (Old) and /work/SRC/openSUSE:Factory/.python-injector.new.2275 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-injector" Thu Sep 29 18:13:53 2022 rev:9 rq:1006876 version:0.20.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-injector/python-injector.changes 2022-01-10 23:54:23.136845479 +0100 +++ /work/SRC/openSUSE:Factory/.python-injector.new.2275/python-injector.changes 2022-09-29 18:14:53.899436112 +0200 @@ -1,0 +2,15 @@ +Thu Sep 29 02:41:09 UTC 2022 - Yogalakshmi Arunachalam <yarunacha...@suse.com> + +- Update to 0.20.1 + - Added support for PEP 604 union types (Python 3.10+), thanks to David P??rsson + - Fixed building with pypandoc 1.8+, thanks to S??ren Fuglede J??rgensen + +- Update to 0.20.0 + - Fixed handling of Union combined with Annotated, thanks to Tobias Nilsson + - Fixed AssitedBuilder/child Injector interaction, thanks to Erik Cederberg + - Made get_bindings() and injections work even if a injectee's return type + annotation is a forward reference that can't be resolved + Backwards incompatible: + - Dropped Python 3.6 support + +------------------------------------------------------------------- Old: ---- 0.19.0.tar.gz New: ---- 0.20.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-injector.spec ++++++ --- /var/tmp/diff_new_pack.2Y5nQR/_old 2022-09-29 18:14:54.503437296 +0200 +++ /var/tmp/diff_new_pack.2Y5nQR/_new 2022-09-29 18:14:54.511437312 +0200 @@ -19,7 +19,7 @@ %define skip_python2 1 %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-injector -Version: 0.19.0 +Version: 0.20.1 Release: 0 Summary: Python dependency injection framework, inspired by Guice License: BSD-3-Clause ++++++ 0.19.0.tar.gz -> 0.20.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.19.0/.github/workflows/ci.yml new/injector-0.20.1/.github/workflows/ci.yml --- old/injector-0.19.0/.github/workflows/ci.yml 2021-12-20 23:18:57.000000000 +0100 +++ new/injector-0.20.1/.github/workflows/ci.yml 2022-08-17 01:03:10.000000000 +0200 @@ -8,11 +8,11 @@ strategy: matrix: os: [ubuntu-latest] - python-version: [3.6, 3.7, 3.8, 3.9, "3.10", pypy3] + python-version: [3.7, 3.8, 3.9, "3.10", "pypy3.7", "pypy3.8", "pypy3.9"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.19.0/CHANGES new/injector-0.20.1/CHANGES --- old/injector-0.19.0/CHANGES 2021-12-20 23:18:57.000000000 +0100 +++ new/injector-0.20.1/CHANGES 2022-08-17 01:03:10.000000000 +0200 @@ -1,6 +1,24 @@ Injector Change Log =================== +0.20.1 +------ + +- Added support for PEP 604 union types (Python 3.10+), thanks to David P??rsson +- Fixed building with pypandoc 1.8+, thanks to S??ren Fuglede J??rgensen + +0.20.0 +------ + +- Fixed handling of Union combined with Annotated, thanks to Tobias Nilsson +- Fixed AssitedBuilder/child Injector interaction, thanks to Erik Cederberg +- Made get_bindings() and injections work even if a injectee's return type + annotation is a forward reference that can't be resolved + +Backwards incompatible: + +- Dropped Python 3.6 support + 0.19.0 ------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.19.0/README.md new/injector-0.20.1/README.md --- old/injector-0.19.0/README.md 2021-12-20 23:18:57.000000000 +0100 +++ new/injector-0.20.1/README.md 2022-08-17 01:03:10.000000000 +0200 @@ -72,7 +72,7 @@ * Documentation: https://injector.readthedocs.org * Change log: https://injector.readthedocs.io/en/latest/changelog.html -Injector works with CPython 3.6+ and PyPy 3 implementing Python 3.6+. +Injector works with CPython 3.7+ and PyPy 3 implementing Python 3.7+. A Quick Example --------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.19.0/injector/__init__.py new/injector-0.20.1/injector/__init__.py --- old/injector-0.19.0/injector/__init__.py 2021-12-20 23:18:57.000000000 +0100 +++ new/injector-0.20.1/injector/__init__.py 2022-08-17 01:03:10.000000000 +0200 @@ -46,40 +46,22 @@ except ImportError: from typing_extensions import NoReturn -HAVE_ANNOTATED = sys.version_info >= (3, 7, 0) - # 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. +# 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: +else: # Ignoring errors here as typing_extensions stub doesn't know about those things yet try: from typing import _AnnotatedAlias, Annotated, get_type_hints except ImportError: from typing_extensions import _AnnotatedAlias, Annotated, get_type_hints -else: - - class Annotated: - 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) __author__ = 'Alec Thomas <a...@swapoff.org>' -__version__ = '0.19.0' +__version__ = '0.20.1' __version_tag__ = '' log = logging.getLogger('injector') @@ -119,86 +101,85 @@ _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 Python 3.9 and backported to Python 3.7+ in `typing_extensions`. +InjectT = TypeVar('InjectT') +Inject = Annotated[InjectT, _inject_marker] +"""An experimental way to declare injectable dependencies utilizing a `PEP 593`_ implementation +in Python 3.9 and backported to Python 3.7+ in `typing_extensions`. - Those two declarations are equivalent:: +Those two declarations are equivalent:: - @inject - def fun(t: SomeType) -> None: - pass + @inject + def fun(t: SomeType) -> None: + pass - def fun(t: Inject[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:: +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 + @inject + @noninjectable('s') + def fun(t: SomeType, s: SomeOtherType) -> None: + pass - def fun(t: Inject[SomeType], s: SomeOtherType) -> None: - pass + def fun(t: Inject[SomeType], s: SomeOtherType) -> None: + pass - .. seealso:: +.. seealso:: - Function :func:`get_bindings` - A way to inspect how various injection declarations interact with each other. + 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:: +.. 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. + 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/ - """ +.. _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 Python 3.9 and backported to Python 3.7+ in `typing_extensions`. +NoInject = Annotated[InjectT, _noinject_marker] +"""An experimental way to declare noninjectable dependencies utilizing a `PEP 593`_ implementation +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 - two issues: +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 +* 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):: +`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 + @noninjectable('b') + def fun(a: TypeA, b: TypeB) -> None: + pass - @inject - def fun(a: TypeA, b: NoInject[TypeB]) -> None: - pass + @inject + def fun(a: TypeA, b: NoInject[TypeB]) -> None: + pass - .. seealso:: +.. seealso:: - Function :func:`get_bindings` - A way to inspect how various injection declarations interact with each other. + 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:: +.. 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. + 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/ - """ +.. _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) -> NoReturn: @@ -651,8 +632,9 @@ def get_binding(self, interface: type) -> Tuple[Binding, 'Binder']: is_scope = isinstance(interface, type) and issubclass(interface, Scope) + is_assisted_builder = _is_specialization(interface, AssistedBuilder) try: - return self._get_binding(interface, only_this_binder=is_scope) + return self._get_binding(interface, only_this_binder=is_scope or is_assisted_builder) except (KeyError, UnsatisfiedRequirement): if is_scope: scope = interface @@ -683,7 +665,7 @@ # 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): + if generic_class is Annotated and isinstance(cls, _AnnotatedAlias): return True if not hasattr(cls, '__origin__'): @@ -1166,10 +1148,37 @@ pass +# See a comment in _infer_injected_bindings() for why this is useful. +class _NoReturnAnnotationProxy: + def __init__(self, callable: Callable) -> None: + self.callable = callable + + def __getattribute__(self, name: str) -> Any: + # get_type_hints() uses quite complex logic to determine the namespaces using which + # any forward references should be resolved. Instead of mirroring this logic here + # let's just take the easy way out and forward all attribute access to the original + # callable except for the annotations ??? we want to filter them. + callable = object.__getattribute__(self, 'callable') + if name == '__annotations__': + annotations = callable.__annotations__ + return {name: value for (name, value) in annotations.items() if name != 'return'} + return getattr(callable, name) + + def _infer_injected_bindings(callable: Callable, only_explicit_bindings: bool) -> Dict[str, type]: + def _is_new_union_type(instance: Any) -> bool: + new_union_type = getattr(types, 'UnionType', None) + return new_union_type is not None and isinstance(instance, new_union_type) + spec = inspect.getfullargspec(callable) + try: - bindings = get_type_hints(callable, include_extras=True) + # Return types don't matter for the purpose of dependency injection so instead of + # obtaining type hints of the callable directly let's wrap it in _NoReturnAnnotationProxy. + # The proxy removes the return type annotation (if present) from the annotations so that + # get_type_hints() works even if the return type is a forward reference that can't be + # resolved. + bindings = get_type_hints(cast(Callable, _NoReturnAnnotationProxy(callable)), include_extras=True) except NameError as e: raise _BindingNotYetAvailable(e) @@ -1199,7 +1208,7 @@ if only_explicit_bindings and _inject_marker not in metadata or _noinject_marker in metadata: del bindings[k] - elif _is_specialization(v, Union): + elif _is_specialization(v, Union) or _is_new_union_type(v): # We don't treat Optional parameters in any special way at the moment. union_members = v.__args__ new_members = tuple(set(union_members) - {type(None)}) @@ -1209,7 +1218,20 @@ # mypy complains about this construct: # error: The type alias is invalid in runtime context # See: https://github.com/python/mypy/issues/5354 - bindings[k] = new_union # type: ignore + union_metadata = { + metadata + for member in new_members + for metadata in getattr(member, '__metadata__', tuple()) + if _is_specialization(member, Annotated) + } + if ( + only_explicit_bindings + and _inject_marker not in union_metadata + or _noinject_marker in union_metadata + ): + del bindings[k] + else: + bindings[k] = new_union # type: ignore return bindings diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.19.0/injector_test.py new/injector-0.20.1/injector_test.py --- old/injector-0.19.0/injector_test.py 2021-12-20 23:18:57.000000000 +0100 +++ new/injector-0.20.1/injector_test.py 2022-08-17 01:03:10.000000000 +0200 @@ -11,7 +11,7 @@ """Functional tests for the "Injector" dependency injection framework.""" from contextlib import contextmanager -from typing import Any, NewType +from typing import Any, NewType, Optional, Union import abc import sys import threading @@ -25,7 +25,9 @@ from injector import ( Binder, CallError, + Inject, Injector, + NoInject, Scope, InstanceProvider, ClassProvider, @@ -46,12 +48,8 @@ ClassAssistedBuilder, Error, UnknownArgument, - HAVE_ANNOTATED, ) -if HAVE_ANNOTATED: - from injector import Inject, NoInject - class EmptyClass: pass @@ -807,6 +805,19 @@ assert (b1._injector, b2._injector) == (i1, i2) +def test_assisted_builder_injection_is_safe_to_use_with_child_injectors(): + class X: + @inject + def __init__(self, builder: AssistedBuilder[NeedsAssistance]): + self.builder = builder + + i1 = Injector() + i2 = i1.create_child_injector() + b1 = i1.get(X).builder + b2 = i2.get(X).builder + assert (b1._injector, b2._injector) == (i1, i2) + + class TestThreadSafety: def setup(self): self.event = threading.Event() @@ -1449,38 +1460,75 @@ 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: - pass + # 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} + 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 + # 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} + 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 + # 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} + 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 + # The presence of NoInject should not trigger anything on its own + def function7(a: int, b: NoInject[str]) -> None: + pass - assert get_bindings(function7) == {} + 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 + # 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) == {} + + # Default arguments to NoInject annotations should behave the same as noninjectable decorator w.r.t 'None' + @inject + @noninjectable('b') + def function9(self, a: int, b: Optional[str] = None): + pass + + @inject + def function10(self, a: int, b: NoInject[Optional[str]] = None): + # b:s type is Union[NoInject[Union[str, None]], None] + pass + + assert get_bindings(function9) == {'a': int} == get_bindings(function10) + + # If there's a return type annottion that contains an a forward reference that can't be + # resolved (for whatever reason) we don't want that to break things for us ??? return types + # don't matter for the purpose of dependency injection. + @inject + def function11(a: int) -> 'InvalidForwardReference': + pass + + assert get_bindings(function11) == {'a': int} + + +# Tests https://github.com/alecthomas/injector/issues/202 +@pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10+") +def test_get_bindings_for_pep_604(): + @inject + def function1(a: int | None) -> None: + pass + + assert get_bindings(function1) == {'a': int} + + @inject + def function1(a: int | str) -> None: + pass - assert get_bindings(function8) == {} + assert get_bindings(function1) == {'a': Union[int, str]} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.19.0/setup.py new/injector-0.20.1/setup.py --- old/injector-0.19.0/setup.py 2021-12-20 23:18:57.000000000 +0100 +++ new/injector-0.20.1/setup.py 2022-08-17 01:03:10.000000000 +0200 @@ -49,7 +49,7 @@ try: import pypandoc - long_description = pypandoc.convert('README.md', 'rst') + long_description = pypandoc.convert_file('README.md', 'rst') except ImportError: warnings.warn('Could not locate pandoc, using Markdown long_description.', ImportWarning) with open('README.md') as f: