Repository: reagento/dishka
Pull request #321 (2024-12-03 19:34:19)

Add parameters in the right order


Patch:
diff --git a/src/dishka/integrations/base.py b/src/dishka/integrations/base.py
index 9b9cfed..4430477 100644
--- a/src/dishka/integrations/base.py
+++ b/src/dishka/integrations/base.py
@@ -2,6 +2,7 @@ from collections.abc import Awaitable, Callable, Sequence
 from inspect import (
     Parameter,
     Signature,
+    _ParameterKind,
     isasyncgenfunction,
     isgeneratorfunction,
     signature,
@@ -102,14 +103,6 @@ def wrap_injection(
             continue
         dependencies[name] = dep
 
-    if not dependencies:
-        try:
-            func.__dishka_injected__ = True  # type: ignore[attr-defined]
-        except AttributeError:
-            pass
-        else:
-            return func
-
     if remove_depends:
         new_annotations = {
             name: hint
@@ -123,11 +116,11 @@ def wrap_injection(
         ]
     else:
         new_annotations = hints.copy()
-        new_params = list(func_signature.parameters.values())
+        new_params = list(func_signature.parameters.copy().values())
 
     auto_injected_func: Callable[P, T | Awaitable[T]]
     if additional_params:
-        new_params.extend(additional_params)
+        new_params = _add_params(new_params, additional_params)
         for param in additional_params:
             new_annotations[param.name] = param.annotation
 
@@ -230,3 +223,47 @@ def _sync_injection_wrapper(
             return func(*args, **kwargs, **solved)
 
     return auto_injected_func
+
+
+def _add_params(
+    params: Sequence[Parameter],
+    additional_params: Sequence[Parameter],
+):
+    params_kind_dict: dict[_ParameterKind, list[Parameter]] = {}
+
+    for param in params:
+        params_kind_dict.setdefault(param.kind, []).append(param)
+
+    for param in additional_params:
+        params_kind_dict.setdefault(param.kind, []).append(param)
+
+
+    var_positional = params_kind_dict.get(Parameter.VAR_POSITIONAL, [])
+    if len(var_positional) > 1:
+        param_names = (param.name for param in var_positional)
+        var_positional_names = ", *".join(param_names)
+        base_msg = "more than one variadic positional parameter: *"
+        msg = base_msg + var_positional_names
+        raise ValueError(msg)
+
+    var_keyword = params_kind_dict.get(Parameter.VAR_KEYWORD, [])
+    if len(var_keyword) > 1:
+        var_keyword_names = ", **".join(param.name for param in var_keyword)
+        msg = "more than one variadic keyword parameter: " + var_keyword_names
+        raise ValueError(msg)
+
+    positional_only = params_kind_dict.get(Parameter.POSITIONAL_ONLY, [])
+    positional_or_keyword = params_kind_dict.get(
+        Parameter.POSITIONAL_OR_KEYWORD,
+        [],
+    )
+    keyword_only = params_kind_dict.get(Parameter.KEYWORD_ONLY, [])
+
+    result_params = []
+    result_params.extend(positional_only)
+    result_params.extend(positional_or_keyword)
+    result_params.extend(var_positional)
+    result_params.extend(keyword_only)
+    result_params.extend(var_keyword)
+
+    return result_params
diff --git a/tox.ini b/tox.ini
index 1d0b69c..cfaa0bb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,6 +4,7 @@ requires =
 env_list =
     unit,
     real_world_example,
+    integrations-base,
     fastapi-{0096,0109},
     aiohttp-393,
     flask-302,
@@ -25,6 +26,7 @@ use_develop = true
 deps =
     pytest
     pytest-cov
+    integrations-base: -r requirements/test.txt
     aiohttp-393: -r requirements/aiohttp-393.txt
     aiohttp-latest: -r requirements/aiohttp-latest.txt
     fastapi-latest: -r requirements/fastapi-latest.txt
@@ -57,6 +59,7 @@ deps =
     click-latest: -r requirements/click-latest.txt
 
 commands =
+    integrations-base: pytest --cov=dishka --cov-append --cov-report=term-missing -v tests/integrations/base
     aiohttp: pytest --cov=dishka --cov-append --cov-report=term-missing -v tests/integrations/aiohttp
     fastapi: pytest --cov=dishka --cov-append --cov-report=term-missing -v tests/integrations/fastapi
     aiogram: pytest --cov=dishka --cov-append --cov-report=term-missing -v tests/integrations/aiogram


Test patch:
diff --git a/tests/integrations/base/__init__.py b/tests/integrations/base/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/integrations/base/test_add_params.py b/tests/integrations/base/test_add_params.py
new file mode 100644
index 0000000..d07ee3e
--- /dev/null
+++ b/tests/integrations/base/test_add_params.py
@@ -0,0 +1,78 @@
+from inspect import Parameter, Signature, signature
+
+import pytest
+
+from dishka.integrations.base import _add_params
+
+
+def func(
+    pos_only,
+    /,
+    pos_keyword,
+    *,
+    keyword_only,
+) -> None: ...
+
+
+def func_expected(
+    pos_only,
+    add_pos_only,
+    /,
+    pos_keyword,
+    add_pos_keyword,
+    *add_var_pos,
+    keyword_only,
+    add_keyword_only,
+    **add_var_keyword,
+) -> None: ...
+
+
+def func_with_args_kwargs(*args, **kwargs): ...
+
+
+def test_add_all_params():
+    additional_params = [
+        Parameter("add_pos_only", Parameter.POSITIONAL_ONLY),
+        Parameter("add_pos_keyword", Parameter.POSITIONAL_OR_KEYWORD),
+        Parameter("add_var_pos", Parameter.VAR_POSITIONAL),
+        Parameter("add_keyword_only", Parameter.KEYWORD_ONLY),
+        Parameter("add_var_keyword", Parameter.VAR_KEYWORD),
+    ]
+    func_signature = signature(func)
+    func_params = list(func_signature.parameters.values())
+
+    result_params = _add_params(func_params, additional_params)
+    new_signature = Signature(
+        parameters=result_params,
+        return_annotation=func_signature.return_annotation,
+    )
+
+    assert new_signature == signature(func_expected)
+
+
+def test_fail_add_second_args():
+    additional_params = [
+        Parameter("add_var_pos", Parameter.VAR_POSITIONAL),
+    ]
+
+    func_signature = signature(func_with_args_kwargs)
+    func_params = list(func_signature.parameters.values())
+
+    with pytest.raises(
+        ValueError, match="more than one variadic positional parameter",
+    ):
+        _add_params(func_params, additional_params)
+
+
+def test_fail_add_second_kwargs():
+    additional_params = [
+        Parameter("add_var_keyword", Parameter.VAR_KEYWORD),
+    ]
+
+    func_signature = signature(func_with_args_kwargs)
+    func_params = list(func_signature.parameters.values())
+
+    with pytest.raises(
+        ValueError, match="more than one variadic keyword parameter",
+    ):
+        _add_params(func_params, additional_params)
diff --git a/tests/integrations/taskiq/test_taskiq.py b/tests/integrations/taskiq/test_taskiq.py
index 8c70794..d8e6e67 100644
--- a/tests/integrations/taskiq/test_taskiq.py
+++ b/tests/integrations/taskiq/test_taskiq.py
@@ -17,6 +17,14 @@ async def return_int_task(data: FromDishka[int]) -> int:
     return data
 
 
+@inject
+async def task_with_kwargs(
+    _: FromDishka[int],
+    **kwargs: str,
+) -> dict[str, str]:
+    return kwargs
+
+
 @asynccontextmanager
 async def create_broker() -> AsyncIterator[AsyncBroker]:
     in_memory_broker = InMemoryBroker().with_result_backend(
@@ -41,3 +49,15 @@ async def test_return_int_task() -> None:
         kiq = await task.kiq()
         result = await kiq.wait_result()
         assert result.return_value == hash("dishka")
+
+
+@pytest.mark.asyncio
+async def test_task_with_kwargs() -> None:
+    async with create_broker() as broker:
+        task = broker.task(task_with_kwargs)
+        kwargs = {"key": "value"}
+
+        kiq = await task.kiq(**kwargs)
+        result = await kiq.wait_result()
+
+        assert result.return_value == kwargs