Skip to content

Commit 3229e0d

Browse files
authored
Merge pull request #290 from python-injector/multibind-scope-tweaks
Parent bindings should not affect multibind bindings
2 parents 1fb1c90 + bd8f001 commit 3229e0d

File tree

2 files changed

+153
-23
lines changed

2 files changed

+153
-23
lines changed

injector/__init__.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -359,16 +359,7 @@ def append(self, provider: Provider[T], scope: Type['Scope']) -> None:
359359

360360
def get_scoped_providers(self, injector: 'Injector') -> Generator[Provider[T], None, None]:
361361
for binding in self._multi_bindings:
362-
if (
363-
isinstance(binding.provider, ClassProvider)
364-
and binding.scope is NoScope
365-
and self._binder.parent
366-
and self._binder.parent.has_explicit_binding_for(binding.provider._cls)
367-
):
368-
parent_binding, _ = self._binder.parent.get_binding(binding.provider._cls)
369-
scope_binding, _ = self._binder.parent.get_binding(parent_binding.scope)
370-
else:
371-
scope_binding, _ = self._binder.get_binding(binding.scope)
362+
scope_binding, _ = self._binder.get_binding(binding.scope)
372363
scope_instance: Scope = scope_binding.provider.get(injector)
373364
provider_instance = scope_instance.get(binding.interface, binding.provider)
374365
yield provider_instance

injector_test.py

Lines changed: 152 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ def __init__(self, b: EmptyClass):
6969
self.b = b
7070

7171

72+
City = NewType('City', str)
73+
Animal = Annotated[str, 'Animal']
74+
75+
7276
def prepare_nested_injectors():
7377
def configure(binder):
7478
binder.bind(str, to='asd')
@@ -578,6 +582,27 @@ def provide_name(self) -> str:
578582
assert injector.get(str) == 'Bob'
579583

580584

585+
def test_module_provider_keeps_annotated_types_and_new_types_separate() -> None:
586+
class MyModule(Module):
587+
@provider
588+
def provide_name(self) -> str:
589+
return 'Bob'
590+
591+
@provider
592+
def provide_city(self) -> City:
593+
return City('Stockholm')
594+
595+
@provider
596+
def provide_animal(self) -> Animal:
597+
return 'Dog'
598+
599+
module = MyModule()
600+
injector = Injector(module)
601+
assert injector.get(str) == 'Bob'
602+
assert injector.get(City) == City('Stockholm')
603+
assert injector.get(Animal) == 'Dog'
604+
605+
581606
def test_module_class_gets_instantiated():
582607
name = 'Meg'
583608

@@ -609,7 +634,10 @@ def provide_description(self, age: int, weight: float) -> str:
609634

610635

611636
Names = NewType('Names', List[str])
612-
Passwords = NewType('Ages', Dict[str, str])
637+
Passwords = NewType('Passwords', Dict[str, str])
638+
639+
Animals = Annotated[List[str], 'Animals']
640+
AnimalFoods = Annotated[Dict[str, str], 'AnimalFoods']
613641

614642

615643
def test_multibind():
@@ -623,6 +651,12 @@ def configure(binder):
623651
# To see that NewTypes are treated distinctly
624652
binder.multibind(Names, to=['Bob'])
625653
binder.multibind(Passwords, to={'Bob': 'password1'})
654+
# To see that Annotated collections are treated distinctly
655+
binder.multibind(Animals, to=['Dog'])
656+
binder.multibind(AnimalFoods, to={'Dog': 'meat'})
657+
# To see that collections of Annotated types are treated distinctly
658+
binder.multibind(List[City], to=[City('Stockholm')])
659+
binder.multibind(Dict[str, City], to={'Sweden': City('Stockholm')})
626660

627661
# Then @multiprovider-decorated Module methods
628662
class CustomModule(Module):
@@ -644,11 +678,27 @@ def provide_str_to_int_mapping(self) -> Dict[str, int]:
644678

645679
@multiprovider
646680
def provide_names(self) -> Names:
647-
return ['Alice', 'Clarice']
681+
return Names(['Alice', 'Clarice'])
648682

649683
@multiprovider
650684
def provide_passwords(self) -> Passwords:
651-
return {'Alice': 'aojrioeg3', 'Clarice': 'clarice30'}
685+
return Passwords({'Alice': 'aojrioeg3', 'Clarice': 'clarice30'})
686+
687+
@multiprovider
688+
def provide_animals(self) -> Animals:
689+
return ['Cat', 'Fish']
690+
691+
@multiprovider
692+
def provide_animal_foods(self) -> AnimalFoods:
693+
return {'Cat': 'milk', 'Fish': 'flakes'}
694+
695+
@multiprovider
696+
def provide_cities(self) -> List[City]:
697+
return [City('New York'), City('Tokyo')]
698+
699+
@multiprovider
700+
def provide_city_mapping(self) -> Dict[str, City]:
701+
return {'USA': City('New York'), 'Japan': City('Tokyo')}
652702

653703
injector = Injector([configure, CustomModule])
654704
assert injector.get(List[str]) == ['not a name', 'not a name either']
@@ -657,6 +707,10 @@ def provide_passwords(self) -> Passwords:
657707
assert injector.get(Dict[str, int]) == {'weight': 12, 'height': 33}
658708
assert injector.get(Names) == ['Bob', 'Alice', 'Clarice']
659709
assert injector.get(Passwords) == {'Bob': 'password1', 'Alice': 'aojrioeg3', 'Clarice': 'clarice30'}
710+
assert injector.get(Animals) == ['Dog', 'Cat', 'Fish']
711+
assert injector.get(AnimalFoods) == {'Dog': 'meat', 'Cat': 'milk', 'Fish': 'flakes'}
712+
assert injector.get(List[City]) == ['Stockholm', 'New York', 'Tokyo']
713+
assert injector.get(Dict[str, City]) == {'Sweden': 'Stockholm', 'USA': 'New York', 'Japan': 'Tokyo'}
660714

661715

662716
class Plugin(abc.ABC):
@@ -679,7 +733,7 @@ class PluginD(Plugin):
679733
pass
680734

681735

682-
def test__multibind_list_of_plugins():
736+
def test_multibind_list_of_plugins():
683737
def configure(binder: Binder):
684738
binder.multibind(List[Plugin], to=PluginA)
685739
binder.multibind(List[Plugin], to=[PluginB, PluginC()])
@@ -694,7 +748,7 @@ def configure(binder: Binder):
694748
assert isinstance(plugins[3], PluginD)
695749

696750

697-
def test__multibind_dict_of_plugins():
751+
def test_multibind_dict_of_plugins():
698752
def configure(binder: Binder):
699753
binder.multibind(Dict[str, Plugin], to={'a': PluginA})
700754
binder.multibind(Dict[str, Plugin], to={'b': PluginB, 'c': PluginC()})
@@ -709,7 +763,7 @@ def configure(binder: Binder):
709763
assert isinstance(plugins['d'], PluginD)
710764

711765

712-
def test__multibinding_to_non_generic_type_raises_error():
766+
def test_multibinding_to_non_generic_type_raises_error():
713767
def configure_list(binder: Binder):
714768
binder.multibind(List, to=[1])
715769

@@ -723,22 +777,49 @@ def configure_dict(binder: Binder):
723777
Injector([configure_dict])
724778

725779

726-
def test_multibind_types_respect_the_bound_type_scope() -> None:
780+
def test_multibind_types_are_not_affected_by_the_bound_type_scope() -> None:
727781
def configure(binder: Binder) -> None:
728782
binder.bind(PluginA, to=PluginA, scope=singleton)
729783
binder.multibind(List[Plugin], to=PluginA)
730784

731785
injector = Injector([configure])
732786
first_list = injector.get(List[Plugin])
733787
second_list = injector.get(List[Plugin])
734-
child_injector = injector.create_child_injector()
735-
third_list = child_injector.get(List[Plugin])
736788

737-
assert first_list[0] is second_list[0]
738-
assert third_list[0] is second_list[0]
789+
assert injector.get(PluginA) is injector.get(PluginA)
790+
assert first_list[0] is not injector.get(PluginA)
791+
assert first_list[0] is not second_list[0]
792+
793+
794+
def test_multibind_types_are_not_affected_by_the_bound_type_provider() -> None:
795+
def configure(binder: Binder) -> None:
796+
binder.bind(PluginA, to=InstanceProvider(PluginA()))
797+
binder.multibind(List[Plugin], to=PluginA)
798+
799+
injector = Injector([configure])
800+
first_list = injector.get(List[Plugin])
801+
second_list = injector.get(List[Plugin])
802+
803+
assert injector.get(PluginA) is injector.get(PluginA)
804+
assert first_list[0] is not injector.get(PluginA)
805+
assert first_list[0] is not second_list[0]
739806

740807

741-
def test_multibind_list_scopes_applies_to_the_bound_items() -> None:
808+
def test_multibind_dict_types_use_their_own_bound_providers_and_scopes() -> None:
809+
def configure(binder: Binder) -> None:
810+
binder.bind(PluginA, to=InstanceProvider(PluginA()))
811+
binder.bind(PluginB, to=PluginB, scope=singleton)
812+
binder.multibind(Dict[str, Plugin], to={'a': PluginA, 'b': PluginB})
813+
814+
injector = Injector([configure])
815+
816+
dictionary = injector.get(Dict[str, Plugin])
817+
818+
assert dictionary['a'] is not injector.get(PluginA)
819+
assert dictionary['b'] is not injector.get(PluginB)
820+
821+
822+
def test_multibind_list_scopes_apply_to_the_bound_items() -> None:
742823
def configure(binder: Binder) -> None:
743824
binder.multibind(List[Plugin], to=PluginA, scope=singleton)
744825
binder.multibind(List[Plugin], to=PluginB)
@@ -754,7 +835,64 @@ def configure(binder: Binder) -> None:
754835
assert first_list[2] is second_list[2]
755836

756837

757-
def test_multibind_dict_scopes_applies_to_the_bound_items() -> None:
838+
def test_multibind_list_scopes_apply_to_the_bound_items_not_types() -> None:
839+
def configure(binder: Binder) -> None:
840+
binder.multibind(List[Plugin], to=PluginA)
841+
binder.multibind(List[Plugin], to=[PluginA, PluginB], scope=singleton)
842+
binder.multibind(List[Plugin], to=PluginA)
843+
binder.multibind(List[Plugin], to=PluginA, scope=singleton)
844+
845+
injector = Injector([configure])
846+
first_list = injector.get(List[Plugin])
847+
second_list = injector.get(List[Plugin])
848+
849+
assert first_list is not second_list
850+
assert first_list[0] is not second_list[0]
851+
assert first_list[1] is second_list[1]
852+
assert first_list[2] is second_list[2]
853+
assert first_list[3] is not second_list[3]
854+
assert first_list[4] is second_list[4]
855+
856+
857+
def test_multibind_dict_scopes_apply_to_the_bound_items_in_the_multibound_dict() -> None:
858+
SingletonPlugins = Annotated[Plugin, "singleton"]
859+
OtherPlugins = Annotated[Plugin, "other"]
860+
861+
def configure(binder: Binder) -> None:
862+
binder.multibind(Dict[str, SingletonPlugins], to={'a': PluginA}, scope=singleton)
863+
binder.multibind(Dict[str, OtherPlugins], to={'a': PluginA})
864+
865+
injector = Injector([configure])
866+
singletons_1 = injector.get(Dict[str, SingletonPlugins])
867+
singletons_2 = injector.get(Dict[str, SingletonPlugins])
868+
others_1 = injector.get(Dict[str, OtherPlugins])
869+
others_2 = injector.get(Dict[str, OtherPlugins])
870+
871+
assert singletons_1['a'] is singletons_2['a']
872+
assert singletons_1['a'] is not others_1['a']
873+
assert others_1['a'] is not others_2['a']
874+
875+
876+
def test_multibind_list_scopes_apply_to_the_bound_items_in_the_multibound_list() -> None:
877+
SingletonPlugins = Annotated[Plugin, "singleton"]
878+
OtherPlugins = Annotated[Plugin, "other"]
879+
880+
def configure(binder: Binder) -> None:
881+
binder.multibind(List[SingletonPlugins], to=PluginA, scope=singleton)
882+
binder.multibind(List[OtherPlugins], to=PluginA)
883+
884+
injector = Injector([configure])
885+
singletons_1 = injector.get(List[SingletonPlugins])
886+
singletons_2 = injector.get(List[SingletonPlugins])
887+
others_1 = injector.get(List[OtherPlugins])
888+
others_2 = injector.get(List[OtherPlugins])
889+
890+
assert singletons_1[0] is singletons_2[0]
891+
assert singletons_1[0] is not others_1[0]
892+
assert others_1[0] is not others_2[0]
893+
894+
895+
def test_multibind_dict_scopes_apply_to_the_bound_items() -> None:
758896
def configure(binder: Binder) -> None:
759897
binder.multibind(Dict[str, Plugin], to={'a': PluginA}, scope=singleton)
760898
binder.multibind(Dict[str, Plugin], to={'b': PluginB})
@@ -773,6 +911,7 @@ def configure(binder: Binder) -> None:
773911
def test_multibind_scopes_does_not_apply_to_the_type_globally() -> None:
774912
def configure(binder: Binder) -> None:
775913
binder.multibind(List[Plugin], to=PluginA, scope=singleton)
914+
binder.multibind(Dict[str, Plugin], to={'a': PluginA}, scope=singleton)
776915

777916
injector = Injector([configure])
778917
plugins = injector.get(List[Plugin])

0 commit comments

Comments
 (0)