From 76292da434af89888524033e7fcbc750f9baf643 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Fri, 19 Jan 2024 12:23:09 -0800 Subject: [PATCH] Add apis to discard extra arguments when calling Python functions --- src/core/pyproxy.ts | 34 +++++++++++++++++++++++++++++- src/py/pyodide/code.py | 47 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/core/pyproxy.ts b/src/core/pyproxy.ts index cb51cd44590..502950a79c3 100644 --- a/src/core/pyproxy.ts +++ b/src/core/pyproxy.ts @@ -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) { @@ -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 diff --git a/src/py/pyodide/code.py b/src/py/pyodide/code.py index b4496d1bf2b..ae5f0bf24aa 100644 --- a/src/py/pyodide/code.py +++ b/src/py/pyodide/code.py @@ -1,3 +1,4 @@ +from functools import lru_cache from typing import Any from _pyodide._base import ( @@ -25,6 +26,51 @@ 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", @@ -32,4 +78,5 @@ def run_js(code: str, /) -> Any: "find_imports", "should_quiet", "run_js", + "relaxed_call", ]