From cda04a3ec2c67a661511c3ab46374460dcef25b9 Mon Sep 17 00:00:00 2001 From: Charles Swartz Date: Thu, 21 Sep 2023 22:48:27 -0400 Subject: [PATCH] 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