pywidget-bridge 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +25 -0
- package/pywidget-bridge.mjs +200 -0
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pywidget-bridge",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Pyodide bridge for pywidget widgets in static MyST Markdown pages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": "./pywidget-bridge.mjs",
|
|
7
|
+
"files": [
|
|
8
|
+
"pywidget-bridge.mjs"
|
|
9
|
+
],
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pywidget",
|
|
12
|
+
"anywidget",
|
|
13
|
+
"myst",
|
|
14
|
+
"pyodide",
|
|
15
|
+
"jupyter",
|
|
16
|
+
"webassembly"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/ktaletsk/pywidget.git",
|
|
22
|
+
"directory": "js"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/ktaletsk/pywidget"
|
|
25
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pywidget bridge for MyST Markdown
|
|
3
|
+
*
|
|
4
|
+
* This is an anywidget-compatible ESM that loads Pyodide in the browser and
|
|
5
|
+
* executes user-supplied Python rendering code. It enables pywidget-style
|
|
6
|
+
* "pure Python" widgets inside static MyST sites — no kernel required.
|
|
7
|
+
*
|
|
8
|
+
* Usage in MyST Markdown:
|
|
9
|
+
*
|
|
10
|
+
* ```{anywidget} ./pywidget-bridge.mjs
|
|
11
|
+
* {
|
|
12
|
+
* "_py_render": "def render(el, model):\n el.innerHTML = '<h1>Hello from Pyodide!</h1>'"
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Model keys consumed:
|
|
17
|
+
* _py_render — Python source defining render(el, model) and optionally update(el, model)
|
|
18
|
+
* _py_packages — (optional) list of package names to install via micropip
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Pyodide singleton (one per page, ~11 MB WASM download on first load)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
let _pyodideReady = null;
|
|
26
|
+
|
|
27
|
+
function ensurePyodide() {
|
|
28
|
+
if (!_pyodideReady) {
|
|
29
|
+
_pyodideReady = (async () => {
|
|
30
|
+
const { loadPyodide } = await import(
|
|
31
|
+
/* webpackIgnore: true */
|
|
32
|
+
/* @vite-ignore */
|
|
33
|
+
"https://cdn.jsdelivr.net/pyodide/v0.27.5/full/pyodide.mjs"
|
|
34
|
+
);
|
|
35
|
+
const pyodide = await loadPyodide();
|
|
36
|
+
await pyodide.loadPackage("micropip");
|
|
37
|
+
return pyodide;
|
|
38
|
+
})();
|
|
39
|
+
}
|
|
40
|
+
return _pyodideReady;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Model proxy: bridges anywidget model API to Python-friendly interface
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
function createModelProxy(model, pyodide) {
|
|
48
|
+
return {
|
|
49
|
+
get(key) {
|
|
50
|
+
const val = model.get(key);
|
|
51
|
+
try {
|
|
52
|
+
return pyodide.toPy(val);
|
|
53
|
+
} catch {
|
|
54
|
+
return val;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
set(key, value) {
|
|
58
|
+
const jsVal =
|
|
59
|
+
value && typeof value.toJs === "function" ? value.toJs() : value;
|
|
60
|
+
model.set(key, jsVal);
|
|
61
|
+
},
|
|
62
|
+
save_changes() {
|
|
63
|
+
model.save_changes();
|
|
64
|
+
},
|
|
65
|
+
on(event, callback) {
|
|
66
|
+
model.on(event, callback);
|
|
67
|
+
},
|
|
68
|
+
off(event, callback) {
|
|
69
|
+
model.off(event, callback);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Error display helper
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
const ERROR_STYLE =
|
|
79
|
+
"color:#c00;white-space:pre-wrap;font-family:monospace;font-size:13px;" +
|
|
80
|
+
"padding:12px;margin:0;background:#fff0f0;border:1px solid #fcc;" +
|
|
81
|
+
"border-radius:4px;overflow-x:auto;";
|
|
82
|
+
|
|
83
|
+
function showError(el, title, message) {
|
|
84
|
+
el.innerHTML = `<pre style="${ERROR_STYLE}"><strong>${title}</strong>\n${message}</pre>`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Lifecycle hooks (anywidget spec, consumed by MyST widget runtime)
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
export default {
|
|
92
|
+
async render({ model, el }) {
|
|
93
|
+
// Show a loading indicator while Pyodide boots
|
|
94
|
+
el.innerHTML =
|
|
95
|
+
'<div style="padding:12px;color:#666;font-family:sans-serif;font-style:italic">' +
|
|
96
|
+
"Loading Pyodide (~11 MB)…</div>";
|
|
97
|
+
|
|
98
|
+
const pyodide = await ensurePyodide();
|
|
99
|
+
|
|
100
|
+
// --- Install packages ---
|
|
101
|
+
const packages = model.get("_py_packages") || [];
|
|
102
|
+
if (packages.length > 0) {
|
|
103
|
+
el.innerHTML =
|
|
104
|
+
'<div style="padding:8px;color:#666;font-style:italic">' +
|
|
105
|
+
"Installing packages: " +
|
|
106
|
+
packages.join(", ") +
|
|
107
|
+
"…</div>";
|
|
108
|
+
try {
|
|
109
|
+
const micropip = pyodide.pyimport("micropip");
|
|
110
|
+
await micropip.install(packages);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
showError(el, "Package install error", err.message);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Get user's Python code ---
|
|
118
|
+
const pyCode = model.get("_py_render");
|
|
119
|
+
if (!pyCode) {
|
|
120
|
+
showError(
|
|
121
|
+
el,
|
|
122
|
+
"Missing _py_render",
|
|
123
|
+
'Provide a "_py_render" key in the widget JSON body containing Python code ' +
|
|
124
|
+
"that defines render(el, model).",
|
|
125
|
+
);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- Create isolated namespace ---
|
|
130
|
+
const ns = pyodide.toPy({});
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await pyodide.runPythonAsync(
|
|
134
|
+
"from pyodide.ffi import create_proxy, to_js\nfrom js import console, document",
|
|
135
|
+
{ globals: ns },
|
|
136
|
+
);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
showError(el, "pywidget setup error", err.message);
|
|
139
|
+
ns.destroy();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- Execute user's Python code ---
|
|
144
|
+
try {
|
|
145
|
+
await pyodide.runPythonAsync(pyCode, { globals: ns });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
showError(el, "Error in _py_render code", err.message);
|
|
148
|
+
ns.destroy();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Clear loading indicator
|
|
153
|
+
el.innerHTML = "";
|
|
154
|
+
|
|
155
|
+
// --- Create the model proxy ---
|
|
156
|
+
const proxy = createModelProxy(model, pyodide);
|
|
157
|
+
|
|
158
|
+
// --- Call render(el, model) ---
|
|
159
|
+
const renderFn = ns.get("render");
|
|
160
|
+
if (renderFn) {
|
|
161
|
+
try {
|
|
162
|
+
const result = renderFn(el, proxy);
|
|
163
|
+
if (result && typeof result.then === "function") await result;
|
|
164
|
+
} catch (err) {
|
|
165
|
+
showError(el, "render() error", err.message);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
showError(
|
|
170
|
+
el,
|
|
171
|
+
"Missing render function",
|
|
172
|
+
'_py_render code must define a "render(el, model)" function.',
|
|
173
|
+
);
|
|
174
|
+
ns.destroy();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- Wire up optional update(el, model) ---
|
|
179
|
+
const updateFn = ns.get("update");
|
|
180
|
+
if (updateFn) {
|
|
181
|
+
model.on("change", () => {
|
|
182
|
+
try {
|
|
183
|
+
const result = updateFn(el, proxy);
|
|
184
|
+
if (result && typeof result.then === "function") {
|
|
185
|
+
result.catch((e) =>
|
|
186
|
+
console.error("[pywidget] update() error:", e),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.error("[pywidget] update() error:", err);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// --- Cleanup ---
|
|
196
|
+
return () => {
|
|
197
|
+
ns.destroy();
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
};
|