Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Injector Change Log

- Dropped support for Python 3.8 and 3.9. Python 3.10+ is now required.
- Use the SPDX license identifier `BSD-3-Clause` in package metadata.
- Skip annotation resolution for parameters excluded with ``@noninjectable``.

0.24.0
------
Expand Down
14 changes: 9 additions & 5 deletions injector/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1232,7 +1232,7 @@ def get_bindings(callable: Callable) -> Dict[str, type]:
"""
look_for_explicit_bindings = False
if not hasattr(callable, '__bindings__'):
type_hints = get_type_hints(callable, include_extras=True)
type_hints = get_type_hints(cast(Callable, _NoReturnAnnotationProxy(callable)), include_extras=True)
has_injectable_parameters = any(
_is_specialization(v, Annotated) and _inject_marker in getattr(v, "__metadata__", ())
for v in type_hints.values()
Expand Down Expand Up @@ -1268,7 +1268,12 @@ def __getattribute__(self, name: str) -> Any:
callable = object.__getattribute__(self, 'callable')
if name == '__annotations__':
annotations = callable.__annotations__
return {name: value for (name, value) in annotations.items() if name != 'return'}
noninjectables: Set[str] = getattr(callable, '__noninjectables__', set())
return {
name: value
for (name, value) in annotations.items()
if name != 'return' and name not in noninjectables
}
return getattr(callable, name)


Expand Down Expand Up @@ -1297,9 +1302,8 @@ def _recreate_annotated_origin(annotated_type: Any) -> Any:
try:
# Return types don't matter for the purpose of dependency injection so instead of
# obtaining type hints of the callable directly let's wrap it in _NoReturnAnnotationProxy.
# The proxy removes the return type annotation (if present) from the annotations so that
# get_type_hints() works even if the return type is a forward reference that can't be
# resolved.
# The proxy removes the return type and explicitly noninjectable parameters so that
# get_type_hints() does not try to resolve forward references we will never inject.
bindings = get_type_hints(cast(Callable, _NoReturnAnnotationProxy(callable)), include_extras=True)
except NameError as e:
raise _BindingNotYetAvailable(e)
Expand Down
77 changes: 77 additions & 0 deletions injector_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1892,6 +1892,83 @@ def function2(a: int, b: str) -> None:
assert get_bindings(function1) == {'a': int} == get_bindings(function2)


@pytest.mark.parametrize('inject_first', [False, True])
@pytest.mark.parametrize('body_type', ['UnresolvableBody', dict[str, Optional['RecursiveDict']]])
def test_noninjectable_annotations_need_not_resolve(inject_first, body_type) -> None:
def function(value: int, body: body_type):
return value, body

if inject_first:
function = noninjectable('body')(inject(function))
else:
function = inject(noninjectable('body')(function))

injector = Injector()
injector.binder.bind(int, to=42)
body = {'value': None}

assert get_bindings(function) == {'value': int}
assert injector.call_with_injection(function, kwargs={'body': body}) == (42, body)
with pytest.raises(CallError):
injector.call_with_injection(function)


def test_noninjectable_keyword_only_annotations_need_not_resolve() -> None:
@inject
@noninjectable('first')
@noninjectable('second')
def function(value: int, *, first: 'UnresolvableFirst', second: 'UnresolvableSecond'):
return value, first, second

assert get_bindings(function) == {'value': int}
assert Injector().call_with_injection(function, kwargs={'first': 'a', 'second': 'b'}) == (0, 'a', 'b')


def test_noninjectable_constructor_annotations_need_not_resolve() -> None:
class Service:
@inject
@noninjectable('body')
def __init__(self, value: int, body: 'UnresolvableBody'):
self.value = value
self.body = body

service = Injector().create_object(Service, additional_kwargs={'body': 'provided'})
assert service.value == 0
assert service.body == 'provided'


def test_noninjectable_does_not_hide_unresolved_injected_annotations() -> None:
from injector import _BindingNotYetAvailable

@inject
@noninjectable('body')
def function(value: 'UnresolvableDependency', body: 'UnresolvableBody'):
pass

with pytest.raises(_BindingNotYetAvailable, match='UnresolvableDependency'):
get_bindings(function)


def test_noninjectable_annotations_need_not_resolve_with_explicit_inject() -> None:
@noninjectable('body')
def function(value: Inject[int], body: 'UnresolvableBody'):
return value, body

injector = Injector()
injector.binder.bind(int, to=42)
assert get_bindings(function) == {'value': int}
assert injector.call_with_injection(function, kwargs={'body': 'provided'}) == (42, 'provided')


def test_noninjectable_annotations_need_not_resolve_without_injected_parameters() -> None:
@noninjectable('body')
def function(body: 'UnresolvableBody'):
return body

assert get_bindings(function) == {}
assert Injector().call_with_injection(function, kwargs={'body': 'provided'}) == 'provided'


def test_gets_bindings_with_inject_annotation() -> None:
def function(a: Inject[int], b: str) -> None:
pass
Expand Down