From 4facb2759a6dd5f828bb252abfc063cec6a60f97 Mon Sep 17 00:00:00 2001 From: boyfeiyu <53444713+boyfeiyu@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:43:19 +0800 Subject: [PATCH] Fix implicit ProviderOf lookup in child injectors --- CHANGES | 2 + injector/__init__.py | 11 ++- injector_test.py | 214 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 3794d42..7814689 100644 --- a/CHANGES +++ b/CHANGES @@ -6,6 +6,8 @@ Injector Change Log - Dropped support for Python 3.8 and 3.9. Python 3.10+ is now required. - Use the SPDX license identifier `BSD-3-Clause` in package metadata. +- Fixed implicit ProviderOf wrappers using an ancestor Injector after a parent lookup, + so child binding overrides also apply to deferred lookups. 0.24.0 ------ diff --git a/injector/__init__.py b/injector/__init__.py index 402c47c..deb8c90 100644 --- a/injector/__init__.py +++ b/injector/__init__.py @@ -711,7 +711,16 @@ 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 or is_assisted_builder) + binding, binder = self._get_binding(interface, only_this_binder=is_scope or is_assisted_builder) + if ( + binder is not self + and isinstance(binding, ImplicitBinding) + and _is_specialization(interface, ProviderOf) + ): + # Implicit ProviderOf wrappers must capture the requesting Injector. + # Explicit bindings still follow the usual inheritance rules. + raise KeyError + return binding, binder except (KeyError, UnsatisfiedRequirement): if is_scope: scope = interface diff --git a/injector_test.py b/injector_test.py index 917f34e..5ba94c3 100644 --- a/injector_test.py +++ b/injector_test.py @@ -1365,6 +1365,220 @@ def configure2(binder): assert provider2.get() == 2 +class ProviderOfTestScope(SingletonScope): + pass + + +@pytest.mark.parametrize('auto_bind', [True, False]) +@pytest.mark.parametrize('parent_first', [False, True]) +def test_providerof_child_override_is_independent_of_lookup_order(auto_bind, parent_first): + parent = Injector(lambda binder: binder.bind(str, to='parent'), auto_bind=auto_bind) + child = parent.create_child_injector(lambda binder: binder.bind(str, to='child'), auto_bind=auto_bind) + if parent_first: + assert parent.get(ProviderOf[str]).get() == 'parent' + + assert child.get(str) == child.get(ProviderOf[str]).get() == 'child' + assert parent.get(ProviderOf[str]).get() == 'parent' + + +@pytest.mark.parametrize('parent_has_target', [True, False]) +def test_providerof_cached_before_child_creation(parent_has_target): + parent = Injector(auto_bind=False) + if parent_has_target: + parent.binder.bind(str, to='parent') + parent_provider = parent.get(ProviderOf[str]) + child = parent.create_child_injector(lambda binder: binder.bind(str, to='child'), auto_bind=False) + + assert child.get(ProviderOf[str]).get() == 'child' + if parent_has_target: + assert parent_provider.get() == 'parent' + else: + with pytest.raises(UnsatisfiedRequirement): + parent_provider.get() + + +@pytest.mark.parametrize('auto_bind', [True, False]) +def test_providerof_constructor_injection_uses_child(auto_bind): + class Consumer: + @inject + def __init__(self, deferred: ProviderOf[str]): + self.deferred = deferred + + parent = Injector(auto_bind=auto_bind) + parent.binder.bind(Consumer) + parent.binder.bind(str, to='parent') + assert parent.get(Consumer).deferred.get() == 'parent' + child = parent.create_child_injector(lambda binder: binder.bind(str, to='child'), auto_bind=auto_bind) + + assert child.get(Consumer).deferred.get() == 'child' + + +def test_providerof_siblings_grandchildren_and_unrelated_injectors_are_independent(): + parent = Injector(lambda binder: binder.bind(str, to='parent')) + parent_provider = parent.get(ProviderOf[str]) + first = parent.create_child_injector(lambda binder: binder.bind(str, to='first')) + second = parent.create_child_injector(lambda binder: binder.bind(str, to='second')) + first_provider = first.get(ProviderOf[str]) + second_provider = second.get(ProviderOf[str]) + grandchild = first.create_child_injector(lambda binder: binder.bind(str, to='grandchild')) + unrelated = Injector(lambda binder: binder.bind(str, to='unrelated')) + + assert grandchild.get(ProviderOf[str]).get() == 'grandchild' + assert unrelated.get(ProviderOf[str]).get() == 'unrelated' + assert second_provider.get() == 'second' + assert first_provider.get() == 'first' + assert parent_provider.get() == 'parent' + + +def test_providerof_child_inherits_target_and_keeps_deferred_lookup_local(): + creations = [] + + def provide_target(): + target = EmptyClass() + creations.append(target) + return target + + parent = Injector(lambda binder: binder.bind(EmptyClass, to=provide_target)) + parent_provider = parent.get(ProviderOf[EmptyClass]) + child = parent.create_child_injector() + child_provider = child.get(ProviderOf[EmptyClass]) + assert child.get(ProviderOf[EmptyClass]) is child_provider + assert not creations + + first = child_provider.get() + second = child_provider.get() + assert creations == [first, second] + assert first is not second + + replacement = EmptyClass() + child.binder.bind(EmptyClass, to=InstanceProvider(replacement)) + assert child_provider.get() is replacement + assert child.get(ProviderOf[EmptyClass]) is child_provider + assert parent_provider.get() is creations[-1] + assert len(creations) == 3 + assert creations[-1] is not replacement + + +@pytest.mark.parametrize('scope', [None, singleton, ProviderOfTestScope]) +@pytest.mark.parametrize('auto_bind', [True, False]) +def test_providerof_explicit_special_binding_is_inherited(scope, auto_bind): + parent = Injector(auto_bind=auto_bind) + parent.binder.bind(str, to='parent') + # Explicit specialization bindings accept a scope, but no target provider. + parent.binder.bind(ProviderOf[str], scope=scope) + child = parent.create_child_injector(lambda binder: binder.bind(str, to='child'), auto_bind=auto_bind) + + assert parent.binder.has_explicit_binding_for(ProviderOf[str]) + assert child.get(str) == 'child' + assert child.get(ProviderOf[str]) is parent.get(ProviderOf[str]) + assert child.get(ProviderOf[str]).get() == 'parent' + + +def test_providerof_explicit_middle_binding_overrides_implicit_ancestor(): + parent = Injector(lambda binder: binder.bind(str, to='parent')) + parent.get(ProviderOf[str]) + child = parent.create_child_injector(lambda binder: binder.bind(str, to='child')) + child.binder.bind(ProviderOf[str]) + grandchild = child.create_child_injector(lambda binder: binder.bind(str, to='grandchild')) + + assert grandchild.get(ProviderOf[str]) is child.get(ProviderOf[str]) + assert grandchild.get(ProviderOf[str]).get() == 'child' + + +@pytest.mark.parametrize('wrap_in_instance_provider', [True, False]) +def test_providerof_supplied_object_is_inherited_as_an_ordinary_binding(wrap_in_instance_provider): + original = Injector(lambda binder: binder.bind(str, to='original')) + supplied = ProviderOf(original, str) + target = InstanceProvider(supplied) if wrap_in_instance_provider else supplied + parent = Injector(auto_bind=False) + # The unspecialized class is an ordinary interface and accepts an instance. + parent.binder.bind(ProviderOf, to=target) + child = parent.create_child_injector(lambda binder: binder.bind(str, to='child'), auto_bind=False) + + assert child.get(ProviderOf) is supplied + assert child.get(ProviderOf).get() == 'original' + + +@pytest.mark.parametrize('wrap_in_instance_provider', [True, False]) +def test_providerof_specialization_still_rejects_supplied_objects(wrap_in_instance_provider): + injector = Injector() + supplied = ProviderOf(injector, str) + target = InstanceProvider(supplied) if wrap_in_instance_provider else supplied + + with pytest.raises(Exception, match='ProviderOf cannot be bound to anything'): + injector.binder.bind(ProviderOf[str], to=target) + + +def test_providerof_child_propagates_target_exception(): + error = ValueError('child target failed') + + def fail(): + raise error + + parent = Injector(lambda binder: binder.bind(str, to='parent')) + parent_provider = parent.get(ProviderOf[str]) + child = parent.create_child_injector(lambda binder: binder.bind(str, to=fail)) + child_provider = child.get(ProviderOf[str]) + + with pytest.raises(ValueError) as caught: + child_provider.get() + assert caught.value is error + assert parent_provider.get() == 'parent' + child.binder.bind(str, to='rebound') + assert child_provider.get() == 'rebound' + + +@pytest.mark.parametrize('scope', [singleton, ProviderOfTestScope]) +def test_providerof_child_preserves_explicit_target_scopes(scope): + parent = Injector(lambda binder: binder.bind(EmptyClass, scope=scope)) + parent_provider = parent.get(ProviderOf[EmptyClass]) + child = parent.create_child_injector() + child_provider = child.get(ProviderOf[EmptyClass]) + + assert child_provider.get() is parent_provider.get() + assert child_provider.get() is child.get(EmptyClass) + child.binder.bind(EmptyClass, scope=scope) + assert child_provider.get() is child.get(EmptyClass) + assert child_provider.get() is not parent_provider.get() + + +@pytest.mark.parametrize('parent_first', [True, False]) +def test_providerof_child_preserves_decorated_singleton_allocation(parent_first): + @singleton + class Shared: + @inject + def __init__(self, deferred: ProviderOf[str]): + self.deferred = deferred + + parent = Injector(lambda binder: binder.bind(str, to='parent')) + if parent_first: + parent.get(ProviderOf[Shared]).get() + child = parent.create_child_injector(lambda binder: binder.bind(str, to='child')) + child_shared = child.get(ProviderOf[Shared]).get() + + assert child_shared is parent.get(Shared) + assert child_shared.deferred.get() == 'parent' + assert child.get(ProviderOf[str]).get() == 'child' + + +@pytest.mark.parametrize('builder_type', [AssistedBuilder, ClassAssistedBuilder]) +@pytest.mark.parametrize('auto_bind', [True, False]) +def test_providerof_change_preserves_child_assisted_builders(builder_type, auto_bind): + class Target: + @inject + def __init__(self, value: str): + self.value = value + + parent = Injector(auto_bind=auto_bind) + parent.binder.bind(Target) + parent.binder.bind(str, to='parent') + parent_builder = parent.get(builder_type[Target]) + child = parent.create_child_injector(lambda binder: binder.bind(str, to='child'), auto_bind=auto_bind) + + assert child.get(builder_type[Target]).build().value == 'child' + assert parent_builder.build().value == 'parent' + + def test_special_interfaces_work_with_auto_bind_disabled(): class InjectMe: pass