Skip to content

Commit

Permalink
Add uniqueness checker for bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
cswartzvi committed Sep 22, 2023
1 parent 395e7e8 commit cda04a3
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 3 deletions.
31 changes: 28 additions & 3 deletions injector/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import types
from abc import ABCMeta, abstractmethod
from collections import namedtuple
from collections import UserDict
from typing import (
Any,
Callable,
Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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.
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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 = []
Expand Down
10 changes: 10 additions & 0 deletions injector_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
Inject,
Injector,
NoInject,
NonUniqueBinding,
Scope,
InstanceProvider,
ClassProvider,
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit cda04a3

Please sign in to comment.