diff --git a/docs/project/changelog.md b/docs/project/changelog.md index 0ba58fc4dfd..0707e3b3040 100644 --- a/docs/project/changelog.md +++ b/docs/project/changelog.md @@ -39,6 +39,9 @@ myst: it can be raised in Python. {pr}`3868` +- {{ Fix }} `from jsmodule import *` now works. + {pr}`3903` + - {{ Enhancement }} When a `JsProxy` of an array is passed to Python builtin functions that use the `PySequence_*` APIs, it now works as expected. Also `jsarray * n` repeats the array `n` times and `jsarray + iterable` returns a diff --git a/src/js/pyodide.ts b/src/js/pyodide.ts index 9a779beb0de..1af3be8e7a0 100644 --- a/src/js/pyodide.ts +++ b/src/js/pyodide.ts @@ -88,7 +88,20 @@ function finalizeBootstrap(API: any, config: ConfigType) { // Set up key Javascript modules. let importhook = API._pyodide._importhook; - importhook.register_js_finder(); + function jsFinderHook(o: object) { + if ("__all__" in o) { + return; + } + Object.defineProperty(o, "__all__", { + get: () => + pyodide.toPy( + Object.getOwnPropertyNames(o).filter((name) => name !== "__all__"), + ), + enumerable: false, + configurable: true, + }); + } + importhook.register_js_finder.callKwargs({ hook: jsFinderHook }); importhook.register_js_module("js", config.jsglobals); let pyodide = API.makePublicAPI(); diff --git a/src/py/_pyodide/_importhook.py b/src/py/_pyodide/_importhook.py index 9e9e7c67d20..63da3f044ac 100644 --- a/src/py/_pyodide/_importhook.py +++ b/src/py/_pyodide/_importhook.py @@ -1,5 +1,5 @@ import sys -from collections.abc import Sequence +from collections.abc import Callable, Sequence from importlib.abc import Loader, MetaPathFinder from importlib.machinery import ModuleSpec from importlib.util import spec_from_loader @@ -12,6 +12,7 @@ class JsFinder(MetaPathFinder): def __init__(self) -> None: self.jsproxies: dict[str, Any] = {} + self.hook: Callable[[JsProxy], None] = lambda _: None def find_spec( self, @@ -69,6 +70,7 @@ def register_js_module(self, name: str, jsproxy: Any) -> None: raise TypeError( f"Argument 'jsproxy' must be a JsProxy, not {type(jsproxy).__name__!r}" ) + self.hook(jsproxy) self.jsproxies[name] = jsproxy def unregister_js_module(self, name: str) -> None: @@ -114,7 +116,7 @@ def is_package(self, fullname: str) -> bool: unregister_js_module = jsfinder.unregister_js_module -def register_js_finder() -> None: +def register_js_finder(*, hook: Callable[[JsProxy], None]) -> None: """A bootstrap function, called near the end of Pyodide initialization. It is called in ``loadPyodide`` in ``pyodide.js`` once ``_pyodide_core`` is ready @@ -129,7 +131,7 @@ def register_js_finder() -> None: for importer in sys.meta_path: if isinstance(importer, JsFinder): raise RuntimeError("JsFinder already registered") - + jsfinder.hook = hook sys.meta_path.append(jsfinder) diff --git a/src/tests/test_jsproxy.py b/src/tests/test_jsproxy.py index e6b48ffe818..4193f1649be 100644 --- a/src/tests/test_jsproxy.py +++ b/src/tests/test_jsproxy.py @@ -663,6 +663,56 @@ def test_unregister_jsmodule_error(selenium): ) +@pytest.mark.skip_refcount_check +@pytest.mark.skip_pyproxy_check +@run_in_pyodide +def test_jsmod_import_star1(selenium): + import sys + from typing import Any + + from pyodide.code import run_js + + run_js("pyodide.registerJsModule('xx', {a: 2, b: 7, f(x){ return x + 1; }});") + g: dict[str, Any] = {} + exec("from xx import *", g) + try: + assert "a" in g + assert "b" in g + assert "f" in g + assert "__all__" not in g + assert g["a"] == 2 + assert g["b"] == 7 + assert g["f"](9) == 10 + finally: + sys.modules.pop("xx", None) + run_js("pyodide.unregisterJsModule('xx');") + + +@pytest.mark.skip_refcount_check +@pytest.mark.skip_pyproxy_check +@run_in_pyodide +def test_jsmod_import_star2(selenium): + import sys + from typing import Any + + from pyodide.code import run_js + + run_js( + "pyodide.registerJsModule('xx', {a: 2, b: 7, f(x){ return x + 1; }, __all__ : pyodide.toPy(['a'])});" + ) + g: dict[str, Any] = {} + exec("from xx import *", g) + try: + assert "a" in g + assert "b" not in g + assert "f" not in g + assert "__all__" not in g + assert g["a"] == 2 + finally: + sys.modules.pop("xx", None) + run_js("pyodide.unregisterJsModule('xx');") + + @pytest.mark.skip_refcount_check @pytest.mark.skip_pyproxy_check def test_nested_import(selenium_standalone):