Skip to content
Merged
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
29 changes: 26 additions & 3 deletions tests/test_injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def _injected(value: providers.Object[int] = provider) -> providers.Object[int]:

assert _injected(provider) is provider
assert plan.direct_parameters == (
_DirectInjectionParameter(0, "value", provider, provider._get_scope_context_init_order()),
_DirectInjectionParameter("value", provider, provider._get_scope_context_init_order()),
)


Expand All @@ -110,7 +110,7 @@ def _injected(value: float = Provide()) -> float:
plan = _build_injection_plan(_injected)

assert _injected(1.0) == 1.0
assert plan.typed_parameters == (_TypedInjectionParameter(0, "value", float),)
assert plan.typed_parameters == (_TypedInjectionParameter("value", float),)


def test_build_injection_plan_stores_string_provider_separately() -> None:
Expand All @@ -121,11 +121,34 @@ def _injected(value: int = Provide["Container.provider"]) -> int:

assert _injected(1) == 1
assert len(plan.string_parameters) == 1
assert plan.string_parameters[0].argument_index == 0
assert plan.string_parameters[0].field_name == "value"
assert plan.string_parameters[0].definition._definition == "Container.provider"


def test_injection_handles_keyword_only_parameter_after_varargs() -> None:
injected_value = 42
override_value = 7
provider = providers.Object(injected_value)

@inject
def target(*values: str, dependency: int = Provide[provider]) -> int:
_ = values
return dependency

assert target("one", "two") == injected_value
assert target("one", dependency=override_value) == override_value


def test_injection_rejects_positional_only_parameter() -> None:
provider = providers.Object(42)

with pytest.raises(TypeError, match="Injected parameter 'dependency' cannot be positional-only"):

@inject
def target(dependency: int = Provide[provider], /) -> int: # pragma: no cover
return dependency


async def test_empty_injection() -> None:
@inject
async def inner(_: int) -> None:
Expand Down
15 changes: 15 additions & 0 deletions tests/test_typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import typing

from typing_extensions import assert_type

from that_depends import Provide, providers
from that_depends.experimental.providers import LazyProvider


int_provider = providers.Object(1)
lazy_provider: LazyProvider[str] = LazyProvider("tests.container.DIContainer.simple_factory")


assert_type(Provide[int_provider], int)
assert_type(Provide[lazy_provider], str)
assert_type(Provide["Container.provider"], typing.Any)
48 changes: 24 additions & 24 deletions that_depends/injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,17 @@ class ContextProviderError(Exception):


class _DirectInjectionParameter(typing.NamedTuple):
argument_index: int
field_name: str
provider: AbstractProvider[typing.Any]
scope_context_init_order: tuple[AbstractProvider[typing.Any], ...]


class _StringInjectionParameter(typing.NamedTuple):
argument_index: int
field_name: str
definition: "StringProviderDefinition"


class _TypedInjectionParameter(typing.NamedTuple):
argument_index: int
field_name: str
annotation: type[typing.Any]

Expand Down Expand Up @@ -113,14 +110,19 @@ def _build_injection_plan(func: typing.Callable[..., typing.Any]) -> _InjectionP
else:
resolved_hints = {}

for index, (field_name, param) in enumerate(parameters):
for _index, (field_name, param) in enumerate(parameters):
default = param.default
if (
isinstance(default, (StringProviderDefinition, AbstractProvider, _Provide))
and param.kind is inspect.Parameter.POSITIONAL_ONLY
):
msg = f"Injected parameter {field_name!r} cannot be positional-only"
raise TypeError(msg)
if isinstance(default, StringProviderDefinition):
string_parameters.append(_StringInjectionParameter(index, field_name, default))
string_parameters.append(_StringInjectionParameter(field_name, default))
elif isinstance(default, AbstractProvider):
direct_parameters.append(
_DirectInjectionParameter(
index,
field_name,
default,
default._get_scope_context_init_order(), # noqa: SLF001
Expand All @@ -138,7 +140,6 @@ def _build_injection_plan(func: typing.Callable[..., typing.Any]) -> _InjectionP
raise TypeError(msg)
typed_parameters.append(
_TypedInjectionParameter(
index,
field_name,
annotation,
)
Expand Down Expand Up @@ -289,8 +290,9 @@ async def _resolve_arguments_async(
return False, kwargs

context_providers: set[AbstractProvider[typing.Any]] = set()
provided_names = plan.signature.bind_partial(*args, **kwargs).arguments
for direct_parameter in plan.direct_parameters:
if _is_argument_provided(direct_parameter.argument_index, direct_parameter.field_name, args, kwargs):
if direct_parameter.field_name in provided_names:
continue

if direct_parameter.scope_context_init_order:
Expand All @@ -303,7 +305,7 @@ async def _resolve_arguments_async(
kwargs[direct_parameter.field_name] = await direct_parameter.provider.resolve()

for string_parameter in plan.string_parameters:
if _is_argument_provided(string_parameter.argument_index, string_parameter.field_name, args, kwargs):
if string_parameter.field_name in provided_names:
continue

kwargs[string_parameter.field_name] = await _resolve_provider_with_scope_async(
Expand All @@ -314,7 +316,7 @@ async def _resolve_arguments_async(
)

for typed_parameter in plan.typed_parameters:
if _is_argument_provided(typed_parameter.argument_index, typed_parameter.field_name, args, kwargs):
if typed_parameter.field_name in provided_names:
continue

provider = _resolve_typed_provider(typed_parameter.annotation, container)
Expand All @@ -339,8 +341,9 @@ def _resolve_arguments_sync(
return False, kwargs

context_providers: set[AbstractProvider[typing.Any]] = set()
provided_names = plan.signature.bind_partial(*args, **kwargs).arguments
for direct_parameter in plan.direct_parameters:
if _is_argument_provided(direct_parameter.argument_index, direct_parameter.field_name, args, kwargs):
if direct_parameter.field_name in provided_names:
continue

if direct_parameter.scope_context_init_order:
Expand All @@ -353,7 +356,7 @@ def _resolve_arguments_sync(
kwargs[direct_parameter.field_name] = direct_parameter.provider.resolve_sync()

for string_parameter in plan.string_parameters:
if _is_argument_provided(string_parameter.argument_index, string_parameter.field_name, args, kwargs):
if string_parameter.field_name in provided_names:
continue

kwargs[string_parameter.field_name] = _resolve_provider_with_scope_sync(
Expand All @@ -364,7 +367,7 @@ def _resolve_arguments_sync(
)

for typed_parameter in plan.typed_parameters:
if _is_argument_provided(typed_parameter.argument_index, typed_parameter.field_name, args, kwargs):
if typed_parameter.field_name in provided_names:
continue

provider = _resolve_typed_provider(typed_parameter.annotation, container)
Expand All @@ -382,15 +385,6 @@ def _plan_has_injected_parameters(plan: _InjectionPlan) -> bool:
return bool(plan.direct_parameters or plan.string_parameters or plan.typed_parameters)


def _is_argument_provided(
argument_index: int,
field_name: str,
args: tuple[typing.Any, ...],
kwargs: dict[str, typing.Any],
) -> bool:
return argument_index < len(args) or field_name in kwargs


def _resolve_typed_provider(
annotation: type[typing.Any],
container: BaseContainerMeta | None,
Expand Down Expand Up @@ -585,10 +579,16 @@ def provider(self) -> AbstractProvider[typing.Any]:


class _Provide:
def __getitem__(self, provider: AbstractProvider[T] | str) -> T | typing.Any: # noqa: ANN401
@typing.overload
def __getitem__(self, provider: AbstractProvider[T]) -> T: ...

@typing.overload
def __getitem__(self, provider: str) -> typing.Any: ... # noqa: ANN401

def __getitem__(self, provider: AbstractProvider[T] | str) -> typing.Any:
if isinstance(provider, str):
return StringProviderDefinition(provider) # will be resolved later
return typing.cast(T, provider)
return provider

def __call__(self) -> typing.Any: # noqa: ANN401
"""Marker for automatic dependency injection."""
Expand Down
Loading