From 86c13a58ba6f201753ea7f0ea6f6d2b1f3d9ef99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20P=C3=A4rsson?= Date: Thu, 2 Jul 2026 15:12:57 +0200 Subject: [PATCH 1/9] Test `injector.get()` with singleton scope --- injector_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/injector_test.py b/injector_test.py index 917f34e..b3a2040 100644 --- a/injector_test.py +++ b/injector_test.py @@ -1049,6 +1049,15 @@ def test_custom_scope(): injector.get(Handler) +def test_get_accepts_a_scope_decorator_and_applies_that_scope(): + class A: + pass + + injector = Injector() + assert injector.get(A) is not injector.get(A) + assert injector.get(A, scope=singleton) is injector.get(A, scope=singleton) + + def test_binder_install(): class ModuleA(Module): def configure(self, binder): From 5f88b350e1178317839d8d74ddcf2416546d65f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20P=C3=A4rsson?= Date: Thu, 2 Jul 2026 15:13:29 +0200 Subject: [PATCH 2/9] Test non-type provider error --- injector_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/injector_test.py b/injector_test.py index b3a2040..c9da0f4 100644 --- a/injector_test.py +++ b/injector_test.py @@ -43,6 +43,7 @@ ScopeDecorator, SingletonScope, UnknownArgument, + UnknownProvider, UnsatisfiedRequirement, get_bindings, inject, @@ -1121,6 +1122,12 @@ def test_binder_provider_for_type_with_metaclass(): assert isinstance(binder.provider_for(A, None).get(injector), A) +def test_binder_provider_for_raises_unknown_provider_for_undeterminable_binding(): + binder = Injector().binder + with pytest.raises(UnknownProvider): + binder.provider_for('not-a-type', to='a string value') + + class ClassA: def __init__(self, parameter): pass From 07cf241415c95bfb6146ee4544a8e92f7ad886e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20P=C3=A4rsson?= Date: Thu, 2 Jul 2026 15:14:09 +0200 Subject: [PATCH 3/9] Test unresolvable forward-reference --- injector_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/injector_test.py b/injector_test.py index c9da0f4..677e28a 100644 --- a/injector_test.py +++ b/injector_test.py @@ -1634,6 +1634,16 @@ def __init__(self, message: str) -> None: del X +def test_provider_with_unresolvable_forward_reference_return_type_raises_name_error(): + class CustomModule(Module): + @provider + def provide_x(self) -> 'ReferenceThatCannotBeResolved': + return object() + + with pytest.raises(NameError): + Injector(CustomModule) + + def test_more_useful_exception_is_raised_when_parameters_type_is_any(): @inject def fun(a: Any) -> None: From 1c6dbcb4339544584ebd1289d66bfe552bae602b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20P=C3=A4rsson?= Date: Thu, 2 Jul 2026 15:14:39 +0200 Subject: [PATCH 4/9] Test `CallError` for missing argument to `__new__` --- injector_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/injector_test.py b/injector_test.py index 677e28a..29b4cf8 100644 --- a/injector_test.py +++ b/injector_test.py @@ -1666,6 +1666,15 @@ def fun(a: Any) -> None: injector.call_with_injection(fun) +def test_create_object_wraps_new_typeerror_in_call_error(): + class ClassWhoseNewRequiresAnArgument: + def __new__(cls, required_argument): + return super().__new__(cls) + + with pytest.raises(CallError): + Injector().create_object(ClassWhoseNewRequiresAnArgument) + + def test_optionals_are_ignored_for_now(): @inject def fun(s: str = None): From 1110ac71a3bff4a8ae628e10e775bff48b8b9331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20P=C3=A4rsson?= Date: Thu, 2 Jul 2026 15:15:24 +0200 Subject: [PATCH 5/9] Test unsatisfied requirement message formatting --- injector_test.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/injector_test.py b/injector_test.py index 29b4cf8..ae9f2df 100644 --- a/injector_test.py +++ b/injector_test.py @@ -1675,6 +1675,32 @@ def __new__(cls, required_argument): Injector().create_object(ClassWhoseNewRequiresAnArgument) +def test_unsatisfied_requirement_message_names_owning_module_for_a_function(): + class Unbound: + pass + + @inject + def function(dependency: Unbound) -> None: + pass + + injector = Injector(auto_bind=False) + with pytest.raises(UnsatisfiedRequirement) as exc_info: + injector.call_with_injection(function) + + assert str(exc_info.value) == '%s has an unsatisfied requirement on Unbound' % __name__ + + +def test_unsatisfied_requirement_message_describes_a_tuple_interface(): + class A: + pass + + injector = Injector(auto_bind=False) + with pytest.raises(UnsatisfiedRequirement) as exc_info: + injector.get((A,)) + + assert str(exc_info.value) == 'unsatisfied requirement on [A]' + + def test_optionals_are_ignored_for_now(): @inject def fun(s: str = None): From 3d8353155494be1714921c6b41762098c498a012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20P=C3=A4rsson?= Date: Thu, 2 Jul 2026 15:15:59 +0200 Subject: [PATCH 6/9] Test `Union` with `NoInject` It's not an intended thing, but still. --- injector_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/injector_test.py b/injector_test.py index ae9f2df..020e130 100644 --- a/injector_test.py +++ b/injector_test.py @@ -2087,6 +2087,14 @@ def function(a: Inject[Inject[int]]) -> None: assert get_bindings(function) == {'a': int} +def test_get_bindings_excludes_union_with_a_noinject_member() -> None: + @inject + def function_with_noinject_nested_in_union(a: Union[NoInject[int], str]) -> None: + pass + + assert get_bindings(function_with_noinject_nested_in_union) == {} + + # Tests https://github.com/alecthomas/injector/issues/202 def test_get_bindings_for_pep_604(): @inject From f5472f1cf2353efd08fd1a969dbd9424f2bcc4cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20P=C3=A4rsson?= Date: Thu, 2 Jul 2026 15:17:44 +0200 Subject: [PATCH 7/9] Ignore test coverage for logging configuration --- injector/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/injector/__init__.py b/injector/__init__.py index 55f7ab2..702ec39 100644 --- a/injector/__init__.py +++ b/injector/__init__.py @@ -53,7 +53,7 @@ log = logging.getLogger('injector') log.addHandler(logging.NullHandler()) -if log.level == logging.NOTSET: +if log.level == logging.NOTSET: # pragma: no cover log.setLevel(logging.WARN) T = TypeVar('T') From a2074e3478edeb8db8703fd79d4827157618cbd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20P=C3=A4rsson?= Date: Thu, 2 Jul 2026 15:18:09 +0200 Subject: [PATCH 8/9] Ignore test coverage for legacy type conversions --- injector/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/injector/__init__.py b/injector/__init__.py index 702ec39..596c2e5 100644 --- a/injector/__init__.py +++ b/injector/__init__.py @@ -782,9 +782,9 @@ def _punch_through_alias(type_: Any) -> type: def _get_origin(type_: type) -> Optional[type]: origin = getattr(type_, '__origin__', None) # Older typing behaves differently there and stores Dict and List as origin, we need to be flexible. - if origin is List: + if origin is List: # pragma: no cover return list - elif origin is Dict: + elif origin is Dict: # pragma: no cover return dict return origin From 8ee1652f9dccb4d151f30852a15c3cd330e65f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20P=C3=A4rsson?= Date: Thu, 2 Jul 2026 15:18:21 +0200 Subject: [PATCH 9/9] Require 100% test coverage --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index cf7ece3..7cedea3 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] -addopts = -v --tb=native --doctest-glob=*.md --doctest-modules --cov-report term --cov-report html --cov-report xml --cov=injector --cov-branch --cov-fail-under=90 +addopts = -v --tb=native --doctest-glob=*.md --doctest-modules --cov-report term --cov-report html --cov-report xml --cov=injector --cov-branch --cov-fail-under=100 norecursedirs = __pycache__ *venv* .git build