Skip to content

Commit

Permalink
Add apis to discard extra arguments when calling Python functions
Browse files Browse the repository at this point in the history
  • Loading branch information
hoodmane committed Jan 19, 2024
1 parent 61f4318 commit 76292da
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 1 deletion.
34 changes: 33 additions & 1 deletion src/core/pyproxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2347,7 +2347,7 @@ export class PyCallableMethods {
return Module.callPyObject(_getPtr(this), jsargs);
}
/**
* Call the function with key word arguments. The last argument must be an
* Call the function with keyword arguments. The last argument must be an
* object with the keyword arguments.
*/
callKwargs(...jsargs: any) {
Expand All @@ -2366,6 +2366,38 @@ export class PyCallableMethods {
return Module.callPyObjectKwargs(_getPtr(this), jsargs, kwargs);
}

/**
* Call the function in a "relaxed" manner. Any extra arguments will be
* ignored. This matches the behavior of JavaScript functions more accurately.
*
* Any extra arguments will be ignored. This matches the behavior of
* JavaScript functions more accurately. Missing arguments are **NOT** filled
* with `None`. If too few arguments are passed, this will still raise a
* TypeError.
*
* This uses :py:func:`pyodide.code.relaxed_call`.
*/
callRelaxed(...jsargs: any) {
return API.pyodide_code.relaxed_call(this, ...jsargs);
}

/**
* Call the function with keyword arguments in a "relaxed" manner. The last
* argument must be an object with the keyword arguments. Any extra arguments
* will be ignored. This matches the behavior of JavaScript functions more
* accurately.
*
* Missing arguments are **NOT** filled with `None`. If too few arguments are
* passed, this will still raise a TypeError. Also, if the same argument is
* passed as both a keyword argument and a positional argument, it will raise
* an error.
*
* This uses :py:func:`pyodide.code.relaxed_call`.
*/
callKwargsRelaxed(...jsargs: any) {
return API.pyodide_code.relaxed_call.callKwargs(this, ...jsargs);
}

/**
* Call the function with stack switching enabled. Functions called this way
* can use
Expand Down
47 changes: 47 additions & 0 deletions src/py/pyodide/code.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from functools import lru_cache
from typing import Any

from _pyodide._base import (
Expand Down Expand Up @@ -25,11 +26,57 @@ def run_js(code: str, /) -> Any:
return eval_(code)


@lru_cache
def _relaxed_call_sig(func):
from inspect import Parameter, signature

try:
sig = signature(func)
except (TypeError, ValueError):
return None
new_params = list(sig.parameters.values())
idx: int | None = -1
for idx, param in enumerate(new_params):
if param.kind in (Parameter.KEYWORD_ONLY, Parameter.VAR_KEYWORD):
break
if param.kind == Parameter.VAR_POSITIONAL:
idx = None
break
else:
idx += 1
if idx is not None:
new_params.insert(idx, Parameter("__var_positional", Parameter.VAR_POSITIONAL))

for param in new_params:
if param.kind == Parameter.KEYWORD_ONLY:
break
else:
new_params.append(Parameter("__var_keyword", Parameter.VAR_KEYWORD))
new_sig = sig.replace(parameters=new_params)
return new_sig


def relaxed_call(func, *args, **kwargs):
"""Call the function ignoring extra arguments
If extra positional or keyword arguments are provided they will be
discarded.
"""
sig = _relaxed_call_sig(func)
if sig is None:
func(*args, **kwargs)
bound = sig.bind(*args, **kwargs)
bound.arguments.pop("__var_positional", None)
bound.arguments.pop("__var_keyword", None)
return func(*bound.args, **bound.kwargs)


__all__ = [
"CodeRunner",
"eval_code",
"eval_code_async",
"find_imports",
"should_quiet",
"run_js",
"relaxed_call",
]

0 comments on commit 76292da

Please sign in to comment.