From cda04a3ec2c67a661511c3ab46374460dcef25b9 Mon Sep 17 00:00:00 2001 From: Charles Swartz Date: Thu, 21 Sep 2023 22:48:27 -0400 Subject: [PATCH 1/8] Add uniqueness checker for bindings --- injector/__init__.py | 31 ++++++++++++++++++++++++++++--- injector_test.py | 10 ++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/injector/__init__.py b/injector/__init__.py index 4136f8f..2102247 100644 --- a/injector/__init__.py +++ b/injector/__init__.py @@ -23,6 +23,7 @@ import types from abc import ABCMeta, abstractmethod from collections import namedtuple +from collections import UserDict from typing import ( Any, Callable, @@ -240,6 +241,10 @@ class UnknownProvider(Error): """Tried to bind to a type whose provider couldn't be determined.""" +class NonUniqueBinding(Error): + """Tried to bind to a type that already has a binding when disallowed.""" + + class UnknownArgument(Error): """Tried to mark an unknown argument as noninjectable.""" @@ -389,6 +394,14 @@ class ImplicitBinding(Binding): _InstallableModuleType = Union[Callable[['Binder'], None], 'Module', Type['Module']] +class UniqueBindings(UserDict[type, Binding]): + """A dictionary that raises an exception when trying to add duplicate bindings.""" + def __setitem__(self, key: type, value: Binding) -> None: + if key in self.data: + raise NonUniqueBinding(key.__name__) + super().__setitem__(key, value) + + class Binder: """Bind interfaces to implementations. @@ -400,17 +413,22 @@ class Binder: @private def __init__( - self, injector: 'Injector', auto_bind: bool = True, parent: Optional['Binder'] = None + self, + injector: 'Injector', + auto_bind: bool = True, + parent: Optional['Binder'] = None, + unique: bool = False, ) -> None: """Create a new Binder. :param injector: Injector we are binding for. :param auto_bind: Whether to automatically bind missing types. :param parent: Parent binder. + :parm unique: Whether to allow multiple bindings for the same type. """ self.injector = injector self._auto_bind = auto_bind - self._bindings = {} + self._bindings = cast(Dict[type, Binding], UniqueBindings()) if unique else {} self.parent = parent def bind( @@ -881,6 +899,7 @@ class Injector: :param auto_bind: Whether to automatically bind missing types. :param parent: Parent injector. + :unique: Whether to allow multiple bindings for the same type. .. versionadded:: 0.7.5 ``use_annotations`` parameter @@ -897,6 +916,7 @@ def __init__( modules: Union[_InstallableModuleType, Iterable[_InstallableModuleType], None] = None, auto_bind: bool = True, parent: Optional['Injector'] = None, + unique: bool = False, ) -> None: # Stack of keys currently being injected. Used to detect circular # dependencies. @@ -905,7 +925,12 @@ def __init__( self.parent = parent # Binder - self.binder = Binder(self, auto_bind=auto_bind, parent=parent.binder if parent is not None else None) + self.binder = Binder( + self, + auto_bind=auto_bind, + parent=parent.binder if parent is not None else None, + unique=unique, + ) if not modules: modules = [] diff --git a/injector_test.py b/injector_test.py index 10087f2..326c6d7 100644 --- a/injector_test.py +++ b/injector_test.py @@ -28,6 +28,7 @@ Inject, Injector, NoInject, + NonUniqueBinding, Scope, InstanceProvider, ClassProvider, @@ -1583,6 +1584,15 @@ def test_binder_has_implicit_binding_for_implicitly_bound_type(): assert not injector.binder.has_explicit_binding_for(int) +def test_binder_with_uniqueness_checking_raises_error(): + def configure(binder): + binder.bind(int, to=123) + binder.bind(int, to=456) + + with pytest.raises(NonUniqueBinding): + _ = Injector([configure], unique=True) + + def test_get_bindings(): def function1(a: int) -> None: pass From 212d38ce48fbc3688d8655d68742c5f3503cdcf4 Mon Sep 17 00:00:00 2001 From: Charles Swartz Date: Wed, 4 Oct 2023 21:40:59 -0400 Subject: [PATCH 2/8] Reformat via black --- injector/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/injector/__init__.py b/injector/__init__.py index 2102247..a4945dd 100644 --- a/injector/__init__.py +++ b/injector/__init__.py @@ -396,6 +396,7 @@ class ImplicitBinding(Binding): class UniqueBindings(UserDict[type, Binding]): """A dictionary that raises an exception when trying to add duplicate bindings.""" + def __setitem__(self, key: type, value: Binding) -> None: if key in self.data: raise NonUniqueBinding(key.__name__) From 28522caf962d76e8d92ca6fc64ae1d0110a0a014 Mon Sep 17 00:00:00 2001 From: Charles Swartz Date: Wed, 4 Oct 2023 21:41:46 -0400 Subject: [PATCH 3/8] Remove generics from UserDict (for python 3.8) --- injector/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/injector/__init__.py b/injector/__init__.py index a4945dd..87d329a 100644 --- a/injector/__init__.py +++ b/injector/__init__.py @@ -394,7 +394,7 @@ class ImplicitBinding(Binding): _InstallableModuleType = Union[Callable[['Binder'], None], 'Module', Type['Module']] -class UniqueBindings(UserDict[type, Binding]): +class UniqueBindings(UserDict): """A dictionary that raises an exception when trying to add duplicate bindings.""" def __setitem__(self, key: type, value: Binding) -> None: From 3cbe7765b6bf01d2fa54c915986b1030f701ee09 Mon Sep 17 00:00:00 2001 From: Charles Swartz Date: Wed, 4 Oct 2023 21:59:47 -0400 Subject: [PATCH 4/8] Improve message for unique binding exception --- injector/__init__.py | 9 ++++++++- injector_test.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/injector/__init__.py b/injector/__init__.py index 87d329a..750ba52 100644 --- a/injector/__init__.py +++ b/injector/__init__.py @@ -244,6 +244,13 @@ class UnknownProvider(Error): class NonUniqueBinding(Error): """Tried to bind to a type that already has a binding when disallowed.""" + def __init__(self, interface: type) -> None: + super().__init__(interface) + self.interface = interface + + def __str__(self) -> str: + return "Binding for '%s' already exists" % _describe(self.interface) + class UnknownArgument(Error): """Tried to mark an unknown argument as noninjectable.""" @@ -399,7 +406,7 @@ class UniqueBindings(UserDict): def __setitem__(self, key: type, value: Binding) -> None: if key in self.data: - raise NonUniqueBinding(key.__name__) + raise NonUniqueBinding(key) super().__setitem__(key, value) diff --git a/injector_test.py b/injector_test.py index 326c6d7..92ebb34 100644 --- a/injector_test.py +++ b/injector_test.py @@ -1589,7 +1589,7 @@ def configure(binder): binder.bind(int, to=123) binder.bind(int, to=456) - with pytest.raises(NonUniqueBinding): + with pytest.raises(NonUniqueBinding, match="Binding for 'int' already exists"): _ = Injector([configure], unique=True) From 8e2fffc260df5b6e65a6382bd83248dbea7bff46 Mon Sep 17 00:00:00 2001 From: Charles Swartz Date: Wed, 4 Oct 2023 22:16:42 -0400 Subject: [PATCH 5/8] Add tests for child injectors and unique bindings --- injector_test.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/injector_test.py b/injector_test.py index 92ebb34..190fd51 100644 --- a/injector_test.py +++ b/injector_test.py @@ -63,12 +63,12 @@ def __init__(self, b: EmptyClass): self.b = b -def prepare_nested_injectors(): +def prepare_nested_injectors(unique=False): def configure(binder): binder.bind(str, to='asd') parent = Injector(configure) - child = parent.create_child_injector() + child = parent.create_child_injector(unique=unique) return parent, child @@ -1584,7 +1584,7 @@ def test_binder_has_implicit_binding_for_implicitly_bound_type(): assert not injector.binder.has_explicit_binding_for(int) -def test_binder_with_uniqueness_checking_raises_error(): +def test_injector_with_uniqueness_checking_raises_error(): def configure(binder): binder.bind(int, to=123) binder.bind(int, to=456) @@ -1593,6 +1593,21 @@ def configure(binder): _ = Injector([configure], unique=True) +def test_child_injector_with_uniqueness_checking_overrides_parent_bindings(): + parent, child = prepare_nested_injectors(unique=True) + child.binder.bind(str, to='qwe') + + assert (parent.get(str), child.get(str)) == ('asd', 'qwe') + + +def test_child_injector_with_uniqueness_checking_raises_error(): + _, child = prepare_nested_injectors(unique=True) + child.binder.bind(str, to='qwe') + + with pytest.raises(NonUniqueBinding, match="Binding for 'str' already exists"): + child.binder.bind(str, to='zxc') + + def test_get_bindings(): def function1(a: int) -> None: pass From cc8cf6f11d26f48626f5b8e2a31aff1683cce069 Mon Sep 17 00:00:00 2001 From: Charles Swartz Date: Thu, 5 Oct 2023 20:40:59 -0400 Subject: [PATCH 6/8] Allow child injectors to inherit uniqueness checks --- injector/__init__.py | 12 +++++++++++- injector_test.py | 16 +++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/injector/__init__.py b/injector/__init__.py index 750ba52..00e7174 100644 --- a/injector/__init__.py +++ b/injector/__init__.py @@ -436,8 +436,16 @@ def __init__( """ self.injector = injector self._auto_bind = auto_bind - self._bindings = cast(Dict[type, Binding], UniqueBindings()) if unique else {} self.parent = parent + self._unique = unique + if self._unique: + self._bindings = cast(Dict[type, Binding], UniqueBindings()) + else: + self._bindings = {} + + @property + def unique(self) -> bool: + return self._unique def bind( self, @@ -1011,6 +1019,8 @@ def run(self): def create_child_injector(self, *args: Any, **kwargs: Any) -> 'Injector': kwargs['parent'] = self + if 'unique' not in kwargs: + kwargs['unique'] = self.binder.unique return Injector(*args, **kwargs) def create_object(self, cls: Type[T], additional_kwargs: Any = None) -> T: diff --git a/injector_test.py b/injector_test.py index 190fd51..0c8a798 100644 --- a/injector_test.py +++ b/injector_test.py @@ -67,7 +67,7 @@ def prepare_nested_injectors(unique=False): def configure(binder): binder.bind(str, to='asd') - parent = Injector(configure) + parent = Injector(configure, unique=unique) child = parent.create_child_injector(unique=unique) return parent, child @@ -1608,6 +1608,20 @@ def test_child_injector_with_uniqueness_checking_raises_error(): child.binder.bind(str, to='zxc') +def test_child_injector_inherits_parent_uniqueness_checking(): + + def configure(binder): + binder.bind(str, to='asd') + + parent = Injector(configure, unique=True) + child = parent.create_child_injector() # no unique=True here + + child.binder.bind(str, to='qwe') + + with pytest.raises(NonUniqueBinding, match="Binding for 'str' already exists"): + child.binder.bind(str, to='qwe') + + def test_get_bindings(): def function1(a: int) -> None: pass From 9e59d1573dc61c429f0c03e8275d76b5c4b17e05 Mon Sep 17 00:00:00 2001 From: Charles Swartz Date: Thu, 5 Oct 2023 20:49:54 -0400 Subject: [PATCH 7/8] Further improve non unique binding message --- injector/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/injector/__init__.py b/injector/__init__.py index 00e7174..6a39b28 100644 --- a/injector/__init__.py +++ b/injector/__init__.py @@ -249,7 +249,8 @@ def __init__(self, interface: type) -> None: self.interface = interface def __str__(self) -> str: - return "Binding for '%s' already exists" % _describe(self.interface) + exists = "Binding for '%s' already exists" % _describe(self.interface) + return "%s for Binder/Injector with unique=True" % exists class UnknownArgument(Error): From 57849373250bc481bbb54fcdf49c7aa1b3068d17 Mon Sep 17 00:00:00 2001 From: Charles Swartz Date: Thu, 5 Oct 2023 20:50:23 -0400 Subject: [PATCH 8/8] Reformat via black --- injector_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/injector_test.py b/injector_test.py index 0c8a798..921d612 100644 --- a/injector_test.py +++ b/injector_test.py @@ -1609,7 +1609,6 @@ def test_child_injector_with_uniqueness_checking_raises_error(): def test_child_injector_inherits_parent_uniqueness_checking(): - def configure(binder): binder.bind(str, to='asd')