Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix from jsmod import * #3903

Merged
merged 11 commits into from Jun 12, 2023
3 changes: 3 additions & 0 deletions docs/project/changelog.md
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion src/js/pyodide.ts
Expand Up @@ -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();
Expand Down
8 changes: 5 additions & 3 deletions 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
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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)


Expand Down
50 changes: 50 additions & 0 deletions src/tests/test_jsproxy.py
Expand Up @@ -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):
Expand Down