Repository: django/asgiref

Issue:
`AsyncToSync` incorrectly passes arguments when called function arguments to internal wrapper
If the function being wrapped uses a kwarg that has the same name as the `AsyncToSync.main_wrap` function, it causes the execution to fail with `AsyncToSync.main_wrap() got multiple values for argument '<kwarg>'`.

How to reproduce - run the following example:

```python
from asgiref.sync import async_to_sync

@async_to_sync
async def example(context):
    pass

example(context=1)
```

Stacktrace:
```
Traceback (most recent call last):
  File "example.py", line 9, in <module>
    example(context=1)
  File "<venv>/asgiref/sync.py", line 215, in __call__
    awaitable = self.main_wrap(
                ^^^^^^^^^^^^^^^
TypeError: AsyncToSync.main_wrap() got multiple values for argument 'context'
```


I have tested this on latest `main`, on 3.8.1, 3.7.2, 3.7.1, and 3.7.0 versions of asgiref.
From my investigation, the culprit is #390. I tested on the commit before that PR (fd7a63c591653146379b55e68a29883b9bf7e706) and the bug does not reproduce. So the last working version which does not exhibit the bug is 3.6.0.

Discussion:
andrewgodwin: That's a very neat way of solving it - nice! And thanks!

Patch:
diff --git a/asgiref/sync.py b/asgiref/sync.py
index 4427fc2..87ee406 100644
--- a/asgiref/sync.py
+++ b/asgiref/sync.py
@@ -217,8 +217,12 @@ class AsyncToSync(Generic[_P, _R]):
                 sys.exc_info(),
                 task_context,
                 context,
-                *args,
-                **kwargs,
+                # prepare an awaitable which can be passed as is to self.main_wrap,
+                # so that `args` and `kwargs` don't need to be
+                # destructured when passed to self.main_wrap
+                # (which is required by `ParamSpec`)
+                # as that may cause overlapping arguments
+                self.awaitable(*args, **kwargs),
             )
 
             if not (self.main_event_loop and self.main_event_loop.is_running()):
@@ -302,8 +306,7 @@ class AsyncToSync(Generic[_P, _R]):
         exc_info: "OptExcInfo",
         task_context: "Optional[List[asyncio.Task[Any]]]",
         context: List[contextvars.Context],
-        *args: _P.args,
-        **kwargs: _P.kwargs,
+        awaitable: Union[Coroutine[Any, Any, _R], Awaitable[_R]],
     ) -> None:
         """
         Wraps the awaitable with something that puts the result into the
@@ -326,9 +329,9 @@ class AsyncToSync(Generic[_P, _R]):
                 try:
                     raise exc_info[1]
                 except BaseException:
-                    result = await self.awaitable(*args, **kwargs)
+                    result = await awaitable
             else:
-                result = await self.awaitable(*args, **kwargs)
+                result = await awaitable
         except BaseException as e:
             call_result.set_exception(e)
         else:


Test patch:
diff --git a/tests/test_sync.py b/tests/test_sync.py
index a4d2413..0c67308 100644
--- a/tests/test_sync.py
+++ b/tests/test_sync.py
@@ -7,6 +7,7 @@ import time
 import warnings
 from concurrent.futures import ThreadPoolExecutor
 from functools import wraps
+from typing import Any
 from unittest import TestCase
 
 import pytest
@@ -1174,3 +1175,36 @@ async def test_inner_shield_sync_and_async_middleware_sync_task():
         assert task_complete
 
     assert task_executed
+
+
+def test_async_to_sync_overlapping_kwargs() -> None:
+    """
+    Tests that AsyncToSync correctly passes through kwargs to the wrapped function,
+    particularly in the case where the wrapped function uses same names for the parameters
+    as the wrapper.
+    """
+
+    @async_to_sync
+    async def test_function(**kwargs: Any) -> None:
+        assert kwargs
+
+    # AsyncToSync.main_wrap has a param named `context`.
+    # So we pass the same argument here to test for the error
+    # "AsyncToSync.main_wrap() got multiple values for argument '<kwarg>'"
+    test_function(context=1)
+
+
+@pytest.mark.asyncio
+async def test_sync_to_async_overlapping_kwargs() -> None:
+    """
+    Tests that SyncToAsync correctly passes through kwargs to the wrapped function,
+    particularly in the case where the wrapped function uses same names for the parameters
+    as the wrapper.
+    """
+
+    @sync_to_async
+    def test_function(**kwargs: Any) -> None:
+        assert kwargs
+
+    # SyncToAsync.__call__.loop.run_in_executor has a param named `task_context`.
+    await test_function(task_context=1)