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 2023-12-08 22:31:57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-injector (Old) and /work/SRC/openSUSE:Factory/.python-injector.new.25432 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-injector" Fri Dec 8 22:31:57 2023 rev:10 rq:1131714 version:0.21.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-injector/python-injector.changes 2022-09-29 18:14:53.899436112 +0200 +++ /work/SRC/openSUSE:Factory/.python-injector.new.25432/python-injector.changes 2023-12-08 22:32:13.136365269 +0100 @@ -1,0 +2,12 @@ +Thu Dec 7 22:09:22 UTC 2023 - Dirk Müller <dmuel...@suse.com> + +- update to 0.21.0: + * Improved the documentation, thanks to jonathanmach and Jakub + Wilk + * Fixed a thread-safety regression + * Improved the type annotations, thanks to David Pärsson + * Fixed singleton scope behavior with parent/child injectors, + thanks to David Pärsson + * Stopped using a deprecated test function, thanks to ljnsn + +------------------------------------------------------------------- Old: ---- 0.20.1.tar.gz New: ---- 0.21.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-injector.spec ++++++ --- /var/tmp/diff_new_pack.c9WQQK/_old 2023-12-08 22:32:13.720386757 +0100 +++ /var/tmp/diff_new_pack.c9WQQK/_new 2023-12-08 22:32:13.720386757 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-injector # -# Copyright (c) 2022 SUSE LLC +# Copyright (c) 2023 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,7 +19,7 @@ %define skip_python2 1 %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-injector -Version: 0.20.1 +Version: 0.21.0 Release: 0 Summary: Python dependency injection framework, inspired by Guice License: BSD-3-Clause ++++++ 0.20.1.tar.gz -> 0.21.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.20.1/.github/workflows/ci.yml new/injector-0.21.0/.github/workflows/ci.yml --- old/injector-0.20.1/.github/workflows/ci.yml 2022-08-17 01:03:10.000000000 +0200 +++ new/injector-0.21.0/.github/workflows/ci.yml 2023-07-27 02:44:06.000000000 +0200 @@ -8,7 +8,7 @@ strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9, "3.10", "pypy3.7", "pypy3.8", "pypy3.9"] + python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "pypy3.7", "pypy3.8", "pypy3.9", "pypy3.10"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} @@ -17,7 +17,7 @@ python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - pip install --upgrade -r requirements.txt -r requirements-dev.txt + pip install --upgrade -r requirements-dev.txt pip install . - name: Run tests run: | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.20.1/.gitignore new/injector-0.21.0/.gitignore --- old/injector-0.20.1/.gitignore 2022-08-17 01:03:10.000000000 +0200 +++ new/injector-0.21.0/.gitignore 2023-07-27 02:44:06.000000000 +0200 @@ -1,3 +1,8 @@ +/.* + +!/.gitignore +!/.github + .cache/ __pycache__/ docs/_build/ @@ -9,4 +14,3 @@ coverage.xml /dist/ /injector.egg-info/ -/.coverage diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.20.1/CHANGES new/injector-0.21.0/CHANGES --- old/injector-0.20.1/CHANGES 2022-08-17 01:03:10.000000000 +0200 +++ new/injector-0.21.0/CHANGES 2023-07-27 02:44:06.000000000 +0200 @@ -1,6 +1,15 @@ Injector Change Log =================== +0.21.0 +------ + +- Improved the documentation, thanks to jonathanmach and Jakub Wilk +- Fixed a thread-safety regression +- Improved the type annotations, thanks to David Pärsson +- Fixed singleton scope behavior with parent/child injectors, thanks to David Pärsson +- Stopped using a deprecated test function, thanks to ljnsn + 0.20.1 ------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.20.1/MANIFEST.in new/injector-0.21.0/MANIFEST.in --- old/injector-0.20.1/MANIFEST.in 2022-08-17 01:03:10.000000000 +0200 +++ new/injector-0.21.0/MANIFEST.in 2023-07-27 02:44:06.000000000 +0200 @@ -1,5 +1,6 @@ include *.py include *.toml +include requirements-dev.in include *.txt include CHANGES include COPYING diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.20.1/README.md new/injector-0.21.0/README.md --- old/injector-0.20.1/README.md 2022-08-17 01:03:10.000000000 +0200 +++ new/injector-0.21.0/README.md 2023-07-27 02:44:06.000000000 +0200 @@ -7,7 +7,7 @@ Introduction ------------ -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 nature, 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>`. +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 nature, 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 modules. If you're not sure what dependency injection is or you'd like to learn more about it see: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.20.1/docs/index.rst new/injector-0.21.0/docs/index.rst --- old/injector-0.20.1/docs/index.rst 2022-08-17 01:03:10.000000000 +0200 +++ new/injector-0.21.0/docs/index.rst 2023-07-27 02:44:06.000000000 +0200 @@ -50,6 +50,8 @@ a different configuration and each with different objects in different scopes. Code like this won't work for this very reason:: + # This will NOT work: + class MyClass: @inject def __init__(self, t: SomeType): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.20.1/docs/scopes.rst new/injector-0.21.0/docs/scopes.rst --- old/injector-0.20.1/docs/scopes.rst 2022-08-17 01:03:10.000000000 +0200 +++ new/injector-0.21.0/docs/scopes.rst 2023-07-27 02:44:06.000000000 +0200 @@ -24,6 +24,8 @@ def provide_thing(self) -> Thing: return Thing() +If using hierarchies of injectors, classes decorated with `@singleton` will be created by and bound to the parent/ancestor injector closest to the root that can provide all of its dependencies. + Implementing new Scopes ``````````````````````` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.20.1/injector/__init__.py new/injector-0.21.0/injector/__init__.py --- old/injector-0.20.1/injector/__init__.py 2022-08-17 01:03:10.000000000 +0200 +++ new/injector-0.21.0/injector/__init__.py 2023-07-27 02:44:06.000000000 +0200 @@ -61,7 +61,7 @@ __author__ = 'Alec Thomas <a...@swapoff.org>' -__version__ = '0.20.1' +__version__ = '0.21.0' __version_tag__ = '' log = logging.getLogger('injector') @@ -254,7 +254,7 @@ raise NotImplementedError # pragma: no cover -class ClassProvider(Provider): +class ClassProvider(Provider, Generic[T]): """Provides instances from a given class, created using an Injector.""" def __init__(self, cls: Type[T]) -> None: @@ -264,7 +264,7 @@ return injector.create_object(self._cls) -class CallableProvider(Provider): +class CallableProvider(Provider, Generic[T]): """Provides something using a callable. The callable is called every time new value is requested from the provider. @@ -305,7 +305,7 @@ return '%s(%r)' % (type(self).__name__, self._callable) -class InstanceProvider(Provider): +class InstanceProvider(Provider, Generic[T]): """Provide a specific instance. :: @@ -379,6 +379,13 @@ return _get_origin(_punch_through_alias(self.interface)) in {dict, list} +@private +class ImplicitBinding(Binding): + """A binding that was created implicitly by auto-binding.""" + + pass + + _InstallableModuleType = Union[Callable[['Binder'], None], 'Module', Type['Module']] @@ -392,7 +399,9 @@ _bindings: Dict[type, Binding] @private - def __init__(self, injector: 'Injector', auto_bind: bool = True, parent: 'Binder' = None) -> None: + def __init__( + self, injector: 'Injector', auto_bind: bool = True, parent: Optional['Binder'] = None + ) -> None: """Create a new Binder. :param injector: Injector we are binding for. @@ -460,7 +469,7 @@ self, interface: Type[List[T]], to: Union[List[T], Callable[..., List[T]], Provider[List[T]]], - scope: Union[Type['Scope'], 'ScopeDecorator'] = None, + scope: Union[Type['Scope'], 'ScopeDecorator', None] = None, ) -> None: # pragma: no cover pass @@ -469,12 +478,12 @@ self, interface: Type[Dict[K, V]], to: Union[Dict[K, V], Callable[..., Dict[K, V]], Provider[Dict[K, V]]], - scope: Union[Type['Scope'], 'ScopeDecorator'] = None, + scope: Union[Type['Scope'], 'ScopeDecorator', None] = None, ) -> None: # pragma: no cover pass def multibind( - self, interface: type, to: Any, scope: Union['ScopeDecorator', Type['Scope']] = None + self, interface: type, to: Any, scope: Union['ScopeDecorator', Type['Scope'], None] = None ) -> None: """Creates or extends a multi-binding. @@ -555,7 +564,7 @@ instance(self) def create_binding( - self, interface: type, to: Any = None, scope: Union['ScopeDecorator', Type['Scope']] = None + self, interface: type, to: Any = None, scope: Union['ScopeDecorator', Type['Scope'], None] = None ) -> Binding: provider = self.provider_for(interface, to) scope = scope or getattr(to or interface, '__scope__', NoScope) @@ -643,12 +652,18 @@ # 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(interface): - binding = self.create_binding(interface) + binding = ImplicitBinding(*self.create_binding(interface)) self._bindings[interface] = binding return binding, self raise UnsatisfiedRequirement(None, interface) + def has_binding_for(self, interface: type) -> bool: + return interface in self._bindings + + def has_explicit_binding_for(self, interface: type) -> bool: + return self.has_binding_for(interface) and not isinstance(self._bindings[interface], ImplicitBinding) + def _is_special_interface(self, interface: type) -> bool: # "Special" interfaces are ones that you cannot bind yourself but # you can request them (for example you cannot bind ProviderOf(SomeClass) @@ -782,10 +797,25 @@ try: return self._context[key] except KeyError: - provider = InstanceProvider(provider.get(self.injector)) + instance = self._get_instance(key, provider, self.injector) + provider = InstanceProvider(instance) self._context[key] = provider return provider + def _get_instance(self, key: Type[T], provider: Provider[T], injector: 'Injector') -> T: + if injector.parent and not injector.binder.has_explicit_binding_for(key): + try: + return self._get_instance_from_parent(key, provider, injector.parent) + except (CallError, UnsatisfiedRequirement): + pass + return provider.get(injector) + + def _get_instance_from_parent(self, key: Type[T], provider: Provider[T], parent: 'Injector') -> T: + singleton_scope_binding, _ = parent.binder.get_binding(type(self)) + singleton_scope = singleton_scope_binding.provider.get(parent) + provider = singleton_scope.get(key, provider) + return provider.get(parent) + singleton = ScopeDecorator(SingletonScope) @@ -829,7 +859,7 @@ % (function.__name__, type(self), e) ) from e return_type = annotations['return'] - binding = function.__func__.__binding__ = Binding( + binding = cast(Any, function.__func__).__binding__ = Binding( interface=return_type, provider=binding.provider, scope=binding.scope ) bind_method = binder.multibind if binding.is_multibinding() else binder.bind @@ -864,9 +894,9 @@ def __init__( self, - modules: Union[_InstallableModuleType, Iterable[_InstallableModuleType]] = None, + modules: Union[_InstallableModuleType, Iterable[_InstallableModuleType], None] = None, auto_bind: bool = True, - parent: 'Injector' = None, + parent: Optional['Injector'] = None, ) -> None: # Stack of keys currently being injected. Used to detect circular # dependencies. @@ -896,7 +926,8 @@ def _log_prefix(self) -> str: return '>' * (len(self._stack) + 1) + ' ' - def get(self, interface: Type[T], scope: Union[ScopeDecorator, Type[Scope]] = None) -> T: + @synchronized(lock) + def get(self, interface: Type[T], scope: Union[ScopeDecorator, Type[Scope], None] = None) -> T: """Get an instance of the given interface. .. note:: @@ -940,7 +971,8 @@ log.debug( '%sInjector.get(%r, scope=%r) using %r', self._log_prefix, interface, scope, binding.provider ) - result = scope_instance.get(interface, binding.provider).get(self) + provider_instance = scope_instance.get(interface, binding.provider) + result = provider_instance.get(self) log.debug('%s -> %r', self._log_prefix, result) return result diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.20.1/injector_test.py new/injector-0.21.0/injector_test.py --- old/injector-0.20.1/injector_test.py 2022-08-17 01:03:10.000000000 +0200 +++ new/injector-0.21.0/injector_test.py 2023-07-27 02:44:06.000000000 +0200 @@ -294,6 +294,141 @@ a1 = injector1.get(A) a2 = injector1.get(A) assert a1.b is a2.b + assert a1 is not a2 + + +def test_injecting_an_auto_bound_decorated_singleton_class(): + class A: + @inject + def __init__(self, b: SingletonB): + self.b = b + + injector1 = Injector() + a1 = injector1.get(A) + a2 = injector1.get(A) + assert a1.b is a2.b + assert a1 is not a2 + + +def test_a_decorated_singleton_is_shared_between_parent_and_child_injectors_when_parent_creates_it_first(): + parent_injector = Injector() + + child_injector = parent_injector.create_child_injector() + + assert parent_injector.get(SingletonB) is child_injector.get(SingletonB) + + +def test_a_decorated_singleton_is_shared_between_parent_and_child_injectors_when_child_creates_it_first(): + parent_injector = Injector() + + child_injector = parent_injector.create_child_injector() + + assert child_injector.get(SingletonB) is parent_injector.get(SingletonB) + + +# Test for https://github.com/python-injector/injector/issues/207 +def test_a_decorated_singleton_is_shared_among_child_injectors(): + parent_injector = Injector() + + child_injector_1 = parent_injector.create_child_injector() + child_injector_2 = parent_injector.create_child_injector() + + assert child_injector_1.get(SingletonB) is child_injector_2.get(SingletonB) + + +def test_a_decorated_singleton_should_not_override_explicit_binds(): + parent_injector = Injector() + + child_injector = parent_injector.create_child_injector() + grand_child_injector = child_injector.create_child_injector() + + bound_singleton = SingletonB() + child_injector.binder.bind(SingletonB, to=bound_singleton) + + assert parent_injector.get(SingletonB) is not bound_singleton + assert child_injector.get(SingletonB) is bound_singleton + assert grand_child_injector.get(SingletonB) is bound_singleton + + +def test_binding_a_singleton_to_a_child_injector_does_not_affect_the_parent_injector(): + parent_injector = Injector() + + child_injector = parent_injector.create_child_injector() + child_injector.binder.bind(EmptyClass, scope=singleton) + + assert child_injector.get(EmptyClass) is child_injector.get(EmptyClass) + assert child_injector.get(EmptyClass) is not parent_injector.get(EmptyClass) + assert parent_injector.get(EmptyClass) is not parent_injector.get(EmptyClass) + + +def test_a_decorated_singleton_should_not_override_a_child_provider(): + parent_injector = Injector() + + provided_instance = SingletonB() + + class MyModule(Module): + @provider + def provide_name(self) -> SingletonB: + return provided_instance + + child_injector = parent_injector.create_child_injector([MyModule]) + + assert child_injector.get(SingletonB) is provided_instance + assert parent_injector.get(SingletonB) is not provided_instance + assert parent_injector.get(SingletonB) is parent_injector.get(SingletonB) + + +# Test for https://github.com/python-injector/injector/issues/207 +def test_a_decorated_singleton_is_created_as_close_to_the_root_where_dependencies_fulfilled(): + class NonInjectableD: + @inject + def __init__(self, required) -> None: + self.required = required + + @singleton + class SingletonC: + @inject + def __init__(self, d: NonInjectableD): + self.d = d + + parent_injector = Injector() + + child_injector_1 = parent_injector.create_child_injector() + + child_injector_2 = parent_injector.create_child_injector() + child_injector_2_1 = child_injector_2.create_child_injector() + + provided_d = NonInjectableD(required=True) + child_injector_2.binder.bind(NonInjectableD, to=provided_d) + + assert child_injector_2_1.get(SingletonC) is child_injector_2.get(SingletonC) + assert child_injector_2.get(SingletonC).d is provided_d + + with pytest.raises(CallError): + parent_injector.get(SingletonC) + + with pytest.raises(CallError): + child_injector_1.get(SingletonC) + + +def test_a_bound_decorated_singleton_is_created_as_close_to_the_root_where_it_exists_when_auto_bind_is_disabled(): + parent_injector = Injector(auto_bind=False) + + child_injector_1 = parent_injector.create_child_injector(auto_bind=False) + + child_injector_2 = parent_injector.create_child_injector(auto_bind=False) + child_injector_2_1 = child_injector_2.create_child_injector(auto_bind=False) + + child_injector_2.binder.bind(SingletonB) + + assert child_injector_2_1.get(SingletonB) is child_injector_2_1.get(SingletonB) + assert child_injector_2_1.get(SingletonB) is child_injector_2.get(SingletonB) + + with pytest.raises(UnsatisfiedRequirement): + parent_injector.get(SingletonB) + + with pytest.raises(UnsatisfiedRequirement): + child_injector_1.get(SingletonB) def test_threadlocal(): @@ -627,7 +762,6 @@ def test_custom_scope(): - injector = Injector([RequestModule()], auto_bind=False) with pytest.raises(UnsatisfiedRequirement): @@ -819,7 +953,7 @@ class TestThreadSafety: - def setup(self): + def setup_method(self): self.event = threading.Event() def configure(binder): @@ -1433,6 +1567,30 @@ assert injector.get(Data).name == 'data' +def test_binder_does_not_have_a_binding_for_an_unbound_type(): + injector = Injector() + assert not injector.binder.has_binding_for(int) + assert not injector.binder.has_explicit_binding_for(int) + + +def test_binder_has_binding_for_explicitly_bound_type(): + def configure(binder): + binder.bind(int, to=123) + + injector = Injector([configure]) + assert injector.binder.has_binding_for(int) + assert injector.binder.has_explicit_binding_for(int) + + +def test_binder_has_implicit_binding_for_implicitly_bound_type(): + injector = Injector() + + injector.get(int) + + assert injector.binder.has_binding_for(int) + assert not injector.binder.has_explicit_binding_for(int) + + def test_get_bindings(): def function1(a: int) -> None: pass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.20.1/requirements-dev.in new/injector-0.21.0/requirements-dev.in --- old/injector-0.20.1/requirements-dev.in 1970-01-01 01:00:00.000000000 +0100 +++ new/injector-0.21.0/requirements-dev.in 2023-07-27 02:44:06.000000000 +0200 @@ -0,0 +1,14 @@ +# Our direct dependencies used in development/CI. +# +# We generate requirements-dev.txt from this file by running +# +# pip install -r requirements-dev.in && pip freeze > requirements-dev.txt +# +# and then modifying the file manually to restrict black and mypy to CPython + +pytest +pytest-cov>=2.5.1 +mypy;implementation_name=="cpython" +black;implementation_name=="cpython" +check-manifest +typing_extensions>=3.7.4;python_version<"3.9" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/injector-0.20.1/requirements-dev.txt new/injector-0.21.0/requirements-dev.txt --- old/injector-0.20.1/requirements-dev.txt 2022-08-17 01:03:10.000000000 +0200 +++ new/injector-0.21.0/requirements-dev.txt 2023-07-27 02:44:06.000000000 +0200 @@ -1,6 +1,18 @@ -pytest -pytest-cov>=2.5.1 -dataclasses;python_version<"3.7" -mypy;implementation_name=="cpython" -black;implementation_name=="cpython" -check-manifest +black==23.3.0;implementation_name=="cpython" +build==0.10.0 +check-manifest==0.49 +click==8.1.3 +coverage==7.2.7 +exceptiongroup==1.1.1 +iniconfig==2.0.0 +mypy==1.4.1;implementation_name=="cpython" +mypy-extensions==1.0.0 +packaging==23.1 +pathspec==0.11.1 +platformdirs==3.8.0 +pluggy==1.2.0 +pyproject_hooks==1.0.0 +pytest==7.4.0 +pytest-cov==4.1.0 +tomli==2.0.1 +typing_extensions==4.7.0