diff --git a/tests/test_injection.py b/tests/test_injection.py index ac4f3d5..3f0e02a 100644 --- a/tests/test_injection.py +++ b/tests/test_injection.py @@ -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()), ) @@ -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: @@ -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: diff --git a/that_depends/injection.py b/that_depends/injection.py index 19bd8d7..a4a14e4 100644 --- a/that_depends/injection.py +++ b/that_depends/injection.py @@ -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] @@ -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 @@ -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, ) @@ -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: @@ -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( @@ -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) @@ -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: @@ -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( @@ -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) @@ -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,