Skip to content

Commit d00556c

Browse files
authored
Create unbound @singleton instances in the parent injector (#216)
This change introduces consistency to how instances for unbound classes decorated with a `@singleton` are shared among parent/child injectors, when auto-binding is enabled. Classes decorated with `@singleton`, that have not been explicitly bound, are now created by and bound to the parent injector closest to the root where all dependencies are fulfilled. The behavior was like this before, but only when the parent injector had created the singleton instance (and its implicit binding) before the child injector. This allows sharing singletons between child injectors without creating them on the parent injector first.
1 parent 87826b3 commit d00556c

File tree

3 files changed

+193
-3
lines changed

3 files changed

+193
-3
lines changed

docs/scopes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ A (redundant) example showing all three methods::
2424
def provide_thing(self) -> Thing:
2525
return Thing()
2626

27+
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.
28+
2729
Implementing new Scopes
2830
```````````````````````
2931

injector/__init__.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,13 @@ def is_multibinding(self) -> bool:
379379
return _get_origin(_punch_through_alias(self.interface)) in {dict, list}
380380

381381

382+
@private
383+
class ImplicitBinding(Binding):
384+
"""A binding that was created implicitly by auto-binding."""
385+
386+
pass
387+
388+
382389
_InstallableModuleType = Union[Callable[['Binder'], None], 'Module', Type['Module']]
383390

384391

@@ -645,12 +652,18 @@ def get_binding(self, interface: type) -> Tuple[Binding, 'Binder']:
645652
# The special interface is added here so that requesting a special
646653
# interface with auto_bind disabled works
647654
if self._auto_bind or self._is_special_interface(interface):
648-
binding = self.create_binding(interface)
655+
binding = ImplicitBinding(*self.create_binding(interface))
649656
self._bindings[interface] = binding
650657
return binding, self
651658

652659
raise UnsatisfiedRequirement(None, interface)
653660

661+
def has_binding_for(self, interface: type) -> bool:
662+
return interface in self._bindings
663+
664+
def has_explicit_binding_for(self, interface: type) -> bool:
665+
return self.has_binding_for(interface) and not isinstance(self._bindings[interface], ImplicitBinding)
666+
654667
def _is_special_interface(self, interface: type) -> bool:
655668
# "Special" interfaces are ones that you cannot bind yourself but
656669
# you can request them (for example you cannot bind ProviderOf(SomeClass)
@@ -784,10 +797,25 @@ def get(self, key: Type[T], provider: Provider[T]) -> Provider[T]:
784797
try:
785798
return self._context[key]
786799
except KeyError:
787-
provider = InstanceProvider(provider.get(self.injector))
800+
instance = self._get_instance(key, provider, self.injector)
801+
provider = InstanceProvider(instance)
788802
self._context[key] = provider
789803
return provider
790804

805+
def _get_instance(self, key: Type[T], provider: Provider[T], injector: 'Injector') -> T:
806+
if injector.parent and not injector.binder.has_explicit_binding_for(key):
807+
try:
808+
return self._get_instance_from_parent(key, provider, injector.parent)
809+
except (CallError, UnsatisfiedRequirement):
810+
pass
811+
return provider.get(injector)
812+
813+
def _get_instance_from_parent(self, key: Type[T], provider: Provider[T], parent: 'Injector') -> T:
814+
singleton_scope_binding, _ = parent.binder.get_binding(type(self))
815+
singleton_scope = singleton_scope_binding.provider.get(parent)
816+
provider = singleton_scope.get(key, provider)
817+
return provider.get(parent)
818+
791819

792820
singleton = ScopeDecorator(SingletonScope)
793821

@@ -943,7 +971,8 @@ def run(self):
943971
log.debug(
944972
'%sInjector.get(%r, scope=%r) using %r', self._log_prefix, interface, scope, binding.provider
945973
)
946-
result = scope_instance.get(interface, binding.provider).get(self)
974+
provider_instance = scope_instance.get(interface, binding.provider)
975+
result = provider_instance.get(self)
947976
log.debug('%s -> %r', self._log_prefix, result)
948977
return result
949978

injector_test.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,141 @@ def configure(binder):
294294
a1 = injector1.get(A)
295295
a2 = injector1.get(A)
296296
assert a1.b is a2.b
297+
assert a1 is not a2
298+
299+
300+
def test_injecting_an_auto_bound_decorated_singleton_class():
301+
class A:
302+
@inject
303+
def __init__(self, b: SingletonB):
304+
self.b = b
305+
306+
injector1 = Injector()
307+
a1 = injector1.get(A)
308+
a2 = injector1.get(A)
309+
assert a1.b is a2.b
310+
assert a1 is not a2
311+
312+
313+
def test_a_decorated_singleton_is_shared_between_parent_and_child_injectors_when_parent_creates_it_first():
314+
parent_injector = Injector()
315+
316+
child_injector = parent_injector.create_child_injector()
317+
318+
assert parent_injector.get(SingletonB) is child_injector.get(SingletonB)
319+
320+
321+
def test_a_decorated_singleton_is_shared_between_parent_and_child_injectors_when_child_creates_it_first():
322+
parent_injector = Injector()
323+
324+
child_injector = parent_injector.create_child_injector()
325+
326+
assert child_injector.get(SingletonB) is parent_injector.get(SingletonB)
327+
328+
329+
# Test for https://github.com/python-injector/injector/issues/207
330+
def test_a_decorated_singleton_is_shared_among_child_injectors():
331+
parent_injector = Injector()
332+
333+
child_injector_1 = parent_injector.create_child_injector()
334+
child_injector_2 = parent_injector.create_child_injector()
335+
336+
assert child_injector_1.get(SingletonB) is child_injector_2.get(SingletonB)
337+
338+
339+
def test_a_decorated_singleton_should_not_override_explicit_binds():
340+
parent_injector = Injector()
341+
342+
child_injector = parent_injector.create_child_injector()
343+
grand_child_injector = child_injector.create_child_injector()
344+
345+
bound_singleton = SingletonB()
346+
child_injector.binder.bind(SingletonB, to=bound_singleton)
347+
348+
assert parent_injector.get(SingletonB) is not bound_singleton
349+
assert child_injector.get(SingletonB) is bound_singleton
350+
assert grand_child_injector.get(SingletonB) is bound_singleton
351+
352+
353+
def test_binding_a_singleton_to_a_child_injector_does_not_affect_the_parent_injector():
354+
parent_injector = Injector()
355+
356+
child_injector = parent_injector.create_child_injector()
357+
child_injector.binder.bind(EmptyClass, scope=singleton)
358+
359+
assert child_injector.get(EmptyClass) is child_injector.get(EmptyClass)
360+
assert child_injector.get(EmptyClass) is not parent_injector.get(EmptyClass)
361+
assert parent_injector.get(EmptyClass) is not parent_injector.get(EmptyClass)
362+
363+
364+
def test_a_decorated_singleton_should_not_override_a_child_provider():
365+
parent_injector = Injector()
366+
367+
provided_instance = SingletonB()
368+
369+
class MyModule(Module):
370+
@provider
371+
def provide_name(self) -> SingletonB:
372+
return provided_instance
373+
374+
child_injector = parent_injector.create_child_injector([MyModule])
375+
376+
assert child_injector.get(SingletonB) is provided_instance
377+
assert parent_injector.get(SingletonB) is not provided_instance
378+
assert parent_injector.get(SingletonB) is parent_injector.get(SingletonB)
379+
380+
381+
# Test for https://github.com/python-injector/injector/issues/207
382+
def test_a_decorated_singleton_is_created_as_close_to_the_root_where_dependencies_fulfilled():
383+
class NonInjectableD:
384+
@inject
385+
def __init__(self, required) -> None:
386+
self.required = required
387+
388+
@singleton
389+
class SingletonC:
390+
@inject
391+
def __init__(self, d: NonInjectableD):
392+
self.d = d
393+
394+
parent_injector = Injector()
395+
396+
child_injector_1 = parent_injector.create_child_injector()
397+
398+
child_injector_2 = parent_injector.create_child_injector()
399+
child_injector_2_1 = child_injector_2.create_child_injector()
400+
401+
provided_d = NonInjectableD(required=True)
402+
child_injector_2.binder.bind(NonInjectableD, to=provided_d)
403+
404+
assert child_injector_2_1.get(SingletonC) is child_injector_2.get(SingletonC)
405+
assert child_injector_2.get(SingletonC).d is provided_d
406+
407+
with pytest.raises(CallError):
408+
parent_injector.get(SingletonC)
409+
410+
with pytest.raises(CallError):
411+
child_injector_1.get(SingletonC)
412+
413+
414+
def test_a_bound_decorated_singleton_is_created_as_close_to_the_root_where_it_exists_when_auto_bind_is_disabled():
415+
parent_injector = Injector(auto_bind=False)
416+
417+
child_injector_1 = parent_injector.create_child_injector(auto_bind=False)
418+
419+
child_injector_2 = parent_injector.create_child_injector(auto_bind=False)
420+
child_injector_2_1 = child_injector_2.create_child_injector(auto_bind=False)
421+
422+
child_injector_2.binder.bind(SingletonB)
423+
424+
assert child_injector_2_1.get(SingletonB) is child_injector_2_1.get(SingletonB)
425+
assert child_injector_2_1.get(SingletonB) is child_injector_2.get(SingletonB)
426+
427+
with pytest.raises(UnsatisfiedRequirement):
428+
parent_injector.get(SingletonB)
429+
430+
with pytest.raises(UnsatisfiedRequirement):
431+
child_injector_1.get(SingletonB)
297432

298433

299434
def test_threadlocal():
@@ -1432,6 +1567,30 @@ def configure(binder):
14321567
assert injector.get(Data).name == 'data'
14331568

14341569

1570+
def test_binder_does_not_have_a_binding_for_an_unbound_type():
1571+
injector = Injector()
1572+
assert not injector.binder.has_binding_for(int)
1573+
assert not injector.binder.has_explicit_binding_for(int)
1574+
1575+
1576+
def test_binder_has_binding_for_explicitly_bound_type():
1577+
def configure(binder):
1578+
binder.bind(int, to=123)
1579+
1580+
injector = Injector([configure])
1581+
assert injector.binder.has_binding_for(int)
1582+
assert injector.binder.has_explicit_binding_for(int)
1583+
1584+
1585+
def test_binder_has_implicit_binding_for_implicitly_bound_type():
1586+
injector = Injector()
1587+
1588+
injector.get(int)
1589+
1590+
assert injector.binder.has_binding_for(int)
1591+
assert not injector.binder.has_explicit_binding_for(int)
1592+
1593+
14351594
def test_get_bindings():
14361595
def function1(a: int) -> None:
14371596
pass

0 commit comments

Comments
 (0)