Repository: pyodide/pytest-pyodide (language: python)

Issue:
ENH run_in_pyodide type annotations, defaults, posonly, and kwonly args
It still doesn't support *args or **kwargs arguments though.

- [x] Add tests
- [ ] Update changelog


Discussion / hints:
Thanks for keep improving `run_in_pyodide`! For the part about modifying the AST, it's hard to understand if it's correct just by looking at the code... so I'll review it again when the tests are added.

Okay I added tests. Of course it didn't work very well before I added the tests.

I reorganized things a bit to reduce the reliance on class attributes when it can be avoided and make things a bit easier to read. In particular, the method `_generate_pyodide_ast` used to do a confusing mixture of ast tree walking and other stuff. I replaced it with the function `_locate_funcdef` which has a more precise purpose.

Unfortunately this made the diff a bit big.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 63c141d..d1ea5c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,12 @@
 ## Unreleased
 
-- Added support in `@run_in_pyodide` positional only and keyword only arguments,
-  type annotations, and argument default values.
+- Added support in `@run_in_pyodide` for arbitrary function definitions,
+  including positional only and keyword only arguments, varargs and kwargs, type
+  annotations, and argument default values.
+
   [#116](https://github.com/pyodide/pytest-pyodide/pull/116)
+  [#119](https://github.com/pyodide/pytest-pyodide/pull/119)
+
 
 ## [0.54.0] - 2023-11/04
 


Test patch:
diff --git a/pytest_pyodide/decorator.py b/pytest_pyodide/decorator.py
index cb5f995..12765c2 100644
--- a/pytest_pyodide/decorator.py
+++ b/pytest_pyodide/decorator.py
@@ -114,7 +114,15 @@ def _decode(result: str, selenium: SeleniumType) -> Any:
 
 
 def all_args(funcdef: MaybeAsyncFuncDef) -> list[ast.arg]:
-    return funcdef.args.posonlyargs + funcdef.args.args + funcdef.args.kwonlyargs
+    vararg = [funcdef.args.vararg] if funcdef.args.vararg else []
+    kwarg = [funcdef.args.kwarg] if funcdef.args.kwarg else []
+    return (
+        funcdef.args.posonlyargs
+        + funcdef.args.args
+        + vararg
+        + funcdef.args.kwonlyargs
+        + kwarg
+    )
 
 
 def prepare_inner_funcdef(funcdef: MaybeAsyncFuncDef) -> MaybeAsyncFuncDef:
@@ -124,13 +132,13 @@ def prepare_inner_funcdef(funcdef: MaybeAsyncFuncDef) -> MaybeAsyncFuncDef:
     for arg in all_args(funcdef):
         arg.annotation = None
     funcdef.returns = None
-    # For the inner funcdef, we turn all kwonly args into positional args.
-    # We don't want to deal with kwonly args, it's easier to pass everything by
-    # position. Positional only args cause us no trouble so we leave them alone.
     args = funcdef.args
+    args.args = all_args(funcdef)
     args.defaults = []
-    args.args.extend(args.kwonlyargs)
+    args.posonlyargs = []
     args.kwonlyargs = []
+    args.vararg = None
+    args.kwarg = None
     funcdef.args.kw_defaults = []
     return funcdef
 
diff --git a/tests/test_decorator.py b/tests/test_decorator.py
index f4dc114..fddacd8 100644
--- a/tests/test_decorator.py
+++ b/tests/test_decorator.py
@@ -354,3 +354,55 @@ def fn_with_defaults(selenium, a=a, b=b, *, c=c, d=d):
         return [a, b, c, d]
 
     assert fn_with_defaults(selenium) == [a, b, c, d]
+
+
+def test_vararg(selenium):
+    @run_in_pyodide
+    def fn_with_vararg(selenium, a, *args: Callable):
+        assert a == 5
+        assert args == (6, 7, 8)
+
+    fn_with_vararg(selenium, 5, 6, 7, 8)
+
+
+def test_kwarg(selenium):
+    @run_in_pyodide
+    def fn_with_kwarg(selenium, a, **kwargs: Callable):
+        assert a == 5
+        assert kwargs == dict(b=6, c=7, d=8)
+
+    fn_with_kwarg(selenium, a=5, b=6, c=7, d=8)
+
+
+def test_complex_args(selenium):
+    def fn_with_complex_args(selenium, a, /, b, *args, c, d=Callable, **kwargs):
+        from collections.abc import Callable
+
+        assert a == 5
+        assert b == 6
+        assert c == 7
+        assert args == tuple()
+        assert d == Callable
+        assert kwargs == dict(a=8, q=11)
+
+    wrapped = run_in_pyodide(fn_with_complex_args)
+    import inspect
+
+    assert inspect.signature(wrapped) == inspect.signature(fn_with_complex_args)
+
+    wrapped(selenium, 5, b=6, a=8, c=7, q=11)
+
+    @run_in_pyodide
+    def fn_with_complex_args2(selenium, a, /, b, *args, c, d=Callable, **kwargs):
+        from collections.abc import Callable
+
+        assert a == 5
+        assert b == 6
+        assert c == 7
+        assert args == (7, 8, 9)
+        assert d == Callable
+        assert kwargs == dict(a=8, q=11)
+
+    fn_with_complex_args2(selenium, 5, 6, 7, 8, 9, a=8, c=7, q=11)
+    with pytest.raises(TypeError, match="multiple values"):
+        fn_with_complex_args2(selenium, 5, 6, 7, 8, 9, b=8, c=7, q=11)