Skip to content

Commit 4fe9299

Browse files
committed
An even better Pyodide experimental patch
1 parent bf92fda commit 4fe9299

File tree

10 files changed

+128
-90
lines changed

10 files changed

+128
-90
lines changed

docs/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

esm/interpreter/pyodide-patch.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
export default () => {
2+
const { prototype } = Function;
3+
const { apply, construct, defineProperty } = Reflect;
4+
5+
const notRegistered = value => value?.shared?.gcRegistered === false;
6+
7+
const patch = args => {
8+
if (known) {
9+
for (let i = 0; i < args.length; i++)
10+
args[i] = toJS(args[i]);
11+
}
12+
};
13+
14+
const toJS = current => {
15+
if (known) {
16+
switch (typeof current) {
17+
case 'object':
18+
if (current === null) break;
19+
// falls through
20+
case 'function':
21+
if (notRegistered(current[pyproxy])) {
22+
return current.toJs(options);
23+
}
24+
}
25+
}
26+
return current;
27+
};
28+
29+
const options = { dict_converter: Object.fromEntries };
30+
31+
const descriptor = {
32+
configurable: true,
33+
set(value) {
34+
delete this[pyproxy];
35+
this[pyproxy] = value;
36+
if (notRegistered(value)) {
37+
const copy = this.copy();
38+
queueMicrotask(() => {
39+
this[pyproxy] = copy[pyproxy];
40+
});
41+
}
42+
},
43+
};
44+
45+
let pyproxy, known = false;
46+
47+
defineProperty(prototype, 'apply', {
48+
value(context, args) {
49+
patch(args);
50+
return apply(this, toJS(context), args);
51+
}
52+
});
53+
54+
defineProperty(prototype, 'call', {
55+
value(context, ...args) {
56+
patch(args);
57+
return apply(this, toJS(context), args);
58+
}
59+
});
60+
61+
// minimalistic Symbol override that
62+
// won't affect performance or break expectations
63+
defineProperty(globalThis, 'Symbol', {
64+
value: new Proxy(Symbol, {
65+
apply(target, self, args) {
66+
const symbol = apply(target, self, args);
67+
if (!known && symbol.description === 'pyproxy.attrs') {
68+
known = true;
69+
pyproxy = symbol;
70+
}
71+
return symbol;
72+
},
73+
})
74+
});
75+
76+
// minimalistic Proxy override that
77+
// won't affect performance or break expectations
78+
defineProperty(globalThis, 'Proxy', {
79+
value: new Proxy(Proxy, {
80+
construct(target, args, New) {
81+
const proxy = construct(target, args, New);
82+
if (known && typeof args[0] === 'function') {
83+
defineProperty(args[0], pyproxy, descriptor);
84+
}
85+
return proxy;
86+
},
87+
})
88+
});
89+
};

esm/interpreter/pyodide.js

Lines changed: 3 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -7,63 +7,14 @@ const type = 'pyodide';
77
const toJsOptions = { dict_converter: Object.fromEntries };
88

99
const { stringify } = JSON;
10-
1110
const { apply } = Reflect;
12-
const FunctionPrototype = Function.prototype;
1311

1412
// REQUIRES INTEGRATION TEST
1513
/* c8 ignore start */
1614
const overrideMethod = method => function (...args) {
1715
return apply(method, this, args);
1816
};
1917

20-
let pyproxy, to_js;
21-
const override = intercept => {
22-
23-
const proxies = new WeakMap;
24-
25-
const patch = args => {
26-
for (let arg, i = 0; i < args.length; i++) {
27-
switch (typeof(arg = args[i])) {
28-
case 'object':
29-
if (arg === null) break;
30-
// falls through
31-
case 'function': {
32-
if (pyproxy in arg && !arg[pyproxy].shared?.gcRegistered) {
33-
intercept = false;
34-
let proxy = proxies.get(arg)?.deref();
35-
if (!proxy) {
36-
proxy = to_js(arg);
37-
const wr = new WeakRef(proxy);
38-
proxies.set(arg, wr);
39-
proxies.set(proxy, wr);
40-
}
41-
args[i] = proxy;
42-
intercept = true;
43-
}
44-
break;
45-
}
46-
}
47-
}
48-
};
49-
50-
// the patch
51-
Object.defineProperties(FunctionPrototype, {
52-
apply: {
53-
value(context, args) {
54-
if (intercept) patch(args);
55-
return apply(this, context, args);
56-
}
57-
},
58-
call: {
59-
value(context, ...args) {
60-
if (intercept) patch(args);
61-
return apply(this, context, args);
62-
}
63-
}
64-
});
65-
};
66-
6718
const progress = createProgress('py');
6819
const indexURLs = new WeakMap();
6920

@@ -72,6 +23,9 @@ export default {
7223
module: (version = '0.27.6') =>
7324
`https://cdn.jsdelivr.net/pyodide/v${version}/full/pyodide.mjs`,
7425
async engine({ loadPyodide }, config, url, baseURL) {
26+
if (config.experimental_create_proxy === 'auto') {
27+
this.transform = (_, value) => value;
28+
}
7529
progress('Loading Pyodide');
7630
let { packages, index_urls } = config;
7731
if (packages) packages = packages.map(fixedRelative, baseURL);
@@ -119,23 +73,6 @@ export default {
11973
await storage.close();
12074
if (options.lockFileURL) URL.revokeObjectURL(options.lockFileURL);
12175
progress('Loaded Pyodide');
122-
if (config.experimental_create_proxy === 'auto') {
123-
interpreter.runPython([
124-
'import js',
125-
'from pyodide.ffi import to_js',
126-
'o=js.Object.fromEntries',
127-
'js.experimental_create_proxy=lambda r:to_js(r,dict_converter=o)'
128-
].join(';'), { globals: interpreter.toPy({}) });
129-
to_js = globalThis.experimental_create_proxy;
130-
delete globalThis.experimental_create_proxy;
131-
[pyproxy] = Reflect.ownKeys(to_js).filter(
132-
k => (
133-
typeof k === 'symbol' &&
134-
String(k) === 'Symbol(pyproxy.attrs)'
135-
)
136-
);
137-
override(true);
138-
}
13976
return interpreter;
14077
},
14178
registerJSModule,

esm/interpreters.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The :RUNTIMES comment is a delimiter and no code should be written/changed after
33
// See rollup/build_interpreters.cjs to know more
44

5+
import patch from './interpreter/pyodide-patch.js';
6+
57
/** @type {Map<string, object>} */
68
export const registry = new Map();
79

@@ -25,16 +27,20 @@ export const interpreter = new Proxy(new Map(), {
2527
: interpreter.module(...rest);
2628
map.set(id, {
2729
url,
28-
module: import(/* webpackIgnore: true */url),
30+
module: () => import(/* webpackIgnore: true */url),
2931
engine: interpreter.engine.bind(interpreter),
3032
});
3133
}
3234
const { url, module, engine } = map.get(id);
33-
return (config, baseURL) =>
34-
module.then((module) => {
35+
return async (config, baseURL) => {
36+
if (config.experimental_create_proxy === 'auto') {
37+
patch();
38+
}
39+
return module().then((module) => {
3540
configs.set(id, config);
3641
return engine(module, config, url, baseURL);
3742
});
43+
};
3844
},
3945
});
4046
/* c8 ignore stop */
@@ -50,9 +56,10 @@ const register = (interpreter) => {
5056
//:RUNTIMES
5157
import dummy from './interpreter/dummy.js';
5258
import micropython from './interpreter/micropython.js';
59+
import pyodide_patch from './interpreter/pyodide-patch.js';
5360
import pyodide from './interpreter/pyodide.js';
5461
import ruby_wasm_wasi from './interpreter/ruby-wasm-wasi.js';
5562
import wasmoon from './interpreter/wasmoon.js';
5663
import webr from './interpreter/webr.js';
57-
for (const interpreter of [dummy, micropython, pyodide, ruby_wasm_wasi, wasmoon, webr])
64+
for (const interpreter of [dummy, micropython, pyodide_patch, pyodide, ruby_wasm_wasi, wasmoon, webr])
5865
register(interpreter);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,6 @@
9696
"to-json-callback": "^0.1.1"
9797
},
9898
"worker": {
99-
"blob": "sha256-2/bb462uv7euExej8laG8WSNLNVDCWz5ZQmCyayyAGs="
99+
"blob": "sha256-+sGDLtv2hVsnU+g72b4LNukOBKJ6rACYVNp4kkAAKfQ="
100100
}
101101
}

test/raw/converter.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { symbol } from 'https://esm.run/@ungap/serialization-registry';
1+
import { symbol } from 'https://esm.run/@ungap/[email protected]';
2+
23

34
const { construct } = Reflect;
45
const { defineProperty, fromEntries } = Object;

test/raw/micropython/index.html

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,21 @@
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width,initial-scale=1.0">
66
<script type="module">
7-
import { serialize } from 'https://esm.run/@ungap/serialization-registry';
7+
import { serialize } from 'https://esm.run/@ungap/serialization-registry@0.2.1';
88
import '../converter.js';
99

1010
globalThis.test = arg => {
11-
console.log(...serialize(arg));
11+
document.body.textContent = 'test';
12+
document.body.addEventListener('click', serialize(arg));
13+
console.log(serialize(arg) === arg);
1214
};
1315

1416
const base = 'https://cdn.jsdelivr.net/npm/@micropython/micropython-webassembly-pyscript@latest';
1517
const { loadMicroPython } = await import(`${base}/micropython.mjs`);
1618
const interpreter = await loadMicroPython({ url: `${base}/micropython.wasm` });
17-
const { textContent } = document.querySelector('script[type=micropython]');
18-
interpreter.runPythonAsync(textContent);
19-
</script>
20-
<script type="micropython" async>
21-
import js
22-
js.test([{'a': 123}, {'a': 456}])
19+
const { src } = document.querySelector('script[type=micropython]');
20+
interpreter.runPythonAsync(await fetch(src).then(r => r.text()));
2321
</script>
22+
<script type="micropython" src="../test.py"></script>
2423
</head>
2524
</html>

test/raw/pyodide/index.html

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,21 @@
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width,initial-scale=1.0">
66
<script type="module">
7-
import { serialize } from 'https://esm.run/@ungap/serialization-registry';
7+
import { serialize } from 'https://esm.run/@ungap/serialization-registry@0.2.1';
88
import '../converter.js';
99

1010
globalThis.test = arg => {
11-
console.log(...serialize(arg));
11+
document.body.textContent = 'test';
12+
document.body.addEventListener('click', serialize(arg));
13+
console.log(serialize(arg) === arg);
1214
};
1315

1416
const base = 'https://cdn.jsdelivr.net/npm/pyodide@latest';
1517
const { loadPyodide } = await import(`${base}/pyodide.mjs`);
1618
const interpreter = await loadPyodide();
17-
const { textContent } = document.querySelector('script[type=pyodide]');
18-
interpreter.runPythonAsync(textContent);
19-
</script>
20-
<script type="pyodide" async>
21-
import js
22-
js.test([{'a': 123}, {'a': 456}])
19+
const { src } = document.querySelector('script[type=pyodide]');
20+
interpreter.runPythonAsync(await fetch(src).then(r => r.text()));
2321
</script>
22+
<script type="pyodide" src="../test.py"></script>
2423
</head>
2524
</html>

test/raw/test.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import js
2+
3+
def cb(e):
4+
print(e.type)
5+
6+
js.test(cb)

0 commit comments

Comments
 (0)