gwchq-textjam 0.1.3 → 0.1.4
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/LICENSE.txt +202 -202
- package/README.md +256 -255
- package/dist/360.index.js +1 -1
- package/dist/assets/{PyodideWorker917534d142d1f853d376.js → PyodideWorkerdf6f34ea7e2b4495f8d4.js} +528 -528
- package/dist/assets/{pygalc0b4f32d2d2cc5a0c638.js → pygalef3b78a56cb1d66beb61.js} +495 -495
- package/dist/index.js +2340 -12993
- package/dist/style.css +2 -290
- package/package.json +268 -267
package/dist/assets/{PyodideWorker917534d142d1f853d376.js → PyodideWorkerdf6f34ea7e2b4495f8d4.js}
RENAMED
|
@@ -1,528 +1,528 @@
|
|
|
1
|
-
/* global globalThis, importScripts, loadPyodide, SharedArrayBuffer, Atomics, pygal */
|
|
2
|
-
|
|
3
|
-
function toAbsoluteFromOrigin(url) {
|
|
4
|
-
if (url.startsWith("http") || url.startsWith("/")) return url;
|
|
5
|
-
return new URL(url, self.location.origin).href;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
// Nest the PyodideWorker function inside a globalThis object so we control when its initialised.
|
|
9
|
-
const PyodideWorker = () => {
|
|
10
|
-
let assets;
|
|
11
|
-
|
|
12
|
-
const handleInit = (data) => {
|
|
13
|
-
assets = data.assets;
|
|
14
|
-
|
|
15
|
-
// Import scripts dynamically based on the environment
|
|
16
|
-
console.log("PyodideWorker", "importing scripts");
|
|
17
|
-
importScripts(toAbsoluteFromOrigin(assets.pygalUrl));
|
|
18
|
-
|
|
19
|
-
assets.pyodideBaseUrl =
|
|
20
|
-
"https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.js";
|
|
21
|
-
importScripts(toAbsoluteFromOrigin(assets.pyodideBaseUrl));
|
|
22
|
-
|
|
23
|
-
initialisePyodide();
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const supportsAllFeatures = typeof SharedArrayBuffer !== "undefined";
|
|
27
|
-
|
|
28
|
-
// eslint-disable-next-line no-restricted-globals
|
|
29
|
-
if (!supportsAllFeatures && name !== "incremental-features") {
|
|
30
|
-
console.warn(
|
|
31
|
-
[
|
|
32
|
-
"The code editor will not be able to capture standard input or stop execution because these HTTP headers are not set:",
|
|
33
|
-
" - Cross-Origin-Opener-Policy: same-origin",
|
|
34
|
-
" - Cross-Origin-Embedder-Policy: require-corp",
|
|
35
|
-
"",
|
|
36
|
-
"If your app can cope with or without these features, please initialize the web worker with { name: 'incremental-features' } to silence this warning.",
|
|
37
|
-
"You can then check for the presence of { stdinBuffer, interruptBuffer } in the handleLoaded message to check whether these features are supported.",
|
|
38
|
-
"",
|
|
39
|
-
"If you definitely need these features, either configure your server to respond with the HTTP headers above, or register a service worker.",
|
|
40
|
-
"Once the HTTP headers are set, the browser will block cross-domain resources so you will need to add 'crossorigin' to <script> and other tags.",
|
|
41
|
-
"You may wish to scope the HTTP headers to only those pages that host the code editor to make the browser restriction easier to deal with.",
|
|
42
|
-
"",
|
|
43
|
-
"Please refer to these code snippets for registering a service worker:",
|
|
44
|
-
" - https://github.com/RaspberryPiFoundation/python-execution-prototypes/blob/fd2c50e032cba3bb0e92e19a88eb62e5b120fe7a/pyodide/index.html#L92-L98",
|
|
45
|
-
" - https://github.com/RaspberryPiFoundation/python-execution-prototypes/blob/fd2c50e032cba3bb0e92e19a88eb62e5b120fe7a/pyodide/serviceworker.js",
|
|
46
|
-
].join("\n"),
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
let pyodide, pyodidePromise, stdinBuffer, interruptBuffer, stopped;
|
|
50
|
-
|
|
51
|
-
const onmessage = async ({ data }) => {
|
|
52
|
-
pyodide = await pyodidePromise;
|
|
53
|
-
let encoder = new TextEncoder();
|
|
54
|
-
|
|
55
|
-
switch (data.method) {
|
|
56
|
-
case "init":
|
|
57
|
-
handleInit(data);
|
|
58
|
-
break;
|
|
59
|
-
case "writeFile":
|
|
60
|
-
pyodide.FS.writeFile(data.filename, encoder.encode(data.content));
|
|
61
|
-
break;
|
|
62
|
-
case "runPython":
|
|
63
|
-
runPython(data.python);
|
|
64
|
-
break;
|
|
65
|
-
case "stopPython":
|
|
66
|
-
stopped = true;
|
|
67
|
-
break;
|
|
68
|
-
default:
|
|
69
|
-
throw new Error(`Unsupported method: ${data.method}`);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// eslint-disable-next-line no-restricted-globals
|
|
74
|
-
addEventListener("message", async (event) => {
|
|
75
|
-
onmessage(event);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const runPython = async (python) => {
|
|
79
|
-
stopped = false;
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
await withSupportForPackages(python, async () => {
|
|
83
|
-
await pyodide.runPython(python);
|
|
84
|
-
});
|
|
85
|
-
} catch (error) {
|
|
86
|
-
if (!(error instanceof pyodide.ffi.PythonError)) {
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
postMessage({ method: "handleError", ...parsePythonError(error) });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
await clearPyodideData();
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const checkIfStopped = () => {
|
|
96
|
-
if (stopped) {
|
|
97
|
-
throw new pyodide.ffi.PythonError("KeyboardInterrupt");
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const withSupportForPackages = async (
|
|
102
|
-
python,
|
|
103
|
-
runPythonFn = async () => {},
|
|
104
|
-
) => {
|
|
105
|
-
const imports = await pyodide._api.pyodide_code.find_imports(python).toJs();
|
|
106
|
-
await Promise.all(imports.map((name) => loadDependency(name)));
|
|
107
|
-
|
|
108
|
-
checkIfStopped();
|
|
109
|
-
await pyodide.loadPackagesFromImports(python);
|
|
110
|
-
|
|
111
|
-
checkIfStopped();
|
|
112
|
-
await pyodide.runPythonAsync(
|
|
113
|
-
`
|
|
114
|
-
import basthon
|
|
115
|
-
import builtins
|
|
116
|
-
import os
|
|
117
|
-
|
|
118
|
-
MAX_FILES = 100
|
|
119
|
-
MAX_FILE_SIZE = 8500000
|
|
120
|
-
|
|
121
|
-
def _custom_open(filename, mode="r", *args, **kwargs):
|
|
122
|
-
if "x" in mode and os.path.exists(filename):
|
|
123
|
-
raise FileExistsError(f"File '{filename}' already exists")
|
|
124
|
-
if ("w" in mode or "a" in mode or "x" in mode) and "b" not in mode:
|
|
125
|
-
if len(os.listdir()) > MAX_FILES and not os.path.exists(filename):
|
|
126
|
-
raise OSError(f"File system limit reached, no more than {MAX_FILES} files allowed")
|
|
127
|
-
class CustomFile:
|
|
128
|
-
def __init__(self, filename):
|
|
129
|
-
self.filename = filename
|
|
130
|
-
self.content = ""
|
|
131
|
-
|
|
132
|
-
def write(self, content):
|
|
133
|
-
self.content += content
|
|
134
|
-
if len(self.content) > MAX_FILE_SIZE:
|
|
135
|
-
raise OSError(f"File '{self.filename}' exceeds maximum file size of {MAX_FILE_SIZE} bytes")
|
|
136
|
-
with _original_open(self.filename, mode) as f:
|
|
137
|
-
f.write(self.content)
|
|
138
|
-
basthon.kernel.write_file({ "filename": self.filename, "content": self.content, "mode": mode })
|
|
139
|
-
|
|
140
|
-
def close(self):
|
|
141
|
-
pass
|
|
142
|
-
|
|
143
|
-
def __enter__(self):
|
|
144
|
-
return self
|
|
145
|
-
|
|
146
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
147
|
-
self.close()
|
|
148
|
-
|
|
149
|
-
return CustomFile(filename)
|
|
150
|
-
else:
|
|
151
|
-
return _original_open(filename, mode, *args, **kwargs)
|
|
152
|
-
|
|
153
|
-
# Override the built-in open function
|
|
154
|
-
builtins.open = _custom_open
|
|
155
|
-
`,
|
|
156
|
-
{ filename: "__custom_open__.py" },
|
|
157
|
-
);
|
|
158
|
-
await runPythonFn();
|
|
159
|
-
|
|
160
|
-
for (let name of imports) {
|
|
161
|
-
checkIfStopped();
|
|
162
|
-
await vendoredPackages[name]?.after();
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const loadDependency = async (name) => {
|
|
167
|
-
checkIfStopped();
|
|
168
|
-
|
|
169
|
-
// If the import is for another user file then open it and load its dependencies.
|
|
170
|
-
if (pyodide.FS.readdir("/home/pyodide").includes(`${name}.py`)) {
|
|
171
|
-
const fileContent = pyodide.FS.readFile(`/home/pyodide/${name}.py`, {
|
|
172
|
-
encoding: "utf8",
|
|
173
|
-
});
|
|
174
|
-
await withSupportForPackages(fileContent);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// If the import is for a vendored package then run its .before() hook.
|
|
179
|
-
const vendoredPackage = vendoredPackages[name];
|
|
180
|
-
await vendoredPackage?.before();
|
|
181
|
-
if (vendoredPackage) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// If the import is for a module built into Python then do nothing.
|
|
186
|
-
let pythonModule;
|
|
187
|
-
try {
|
|
188
|
-
pythonModule = pyodide.pyimport(name);
|
|
189
|
-
} catch (_) {}
|
|
190
|
-
if (pythonModule) {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// If the import is for a package built into Pyodide then load it.
|
|
195
|
-
// Built-ins: https://pyodide.org/en/stable/usage/packages-in-pyodide.html
|
|
196
|
-
await pyodide.loadPackage(name)?.catch(() => {});
|
|
197
|
-
let pyodidePackage;
|
|
198
|
-
try {
|
|
199
|
-
pyodidePackage = pyodide.pyimport(name);
|
|
200
|
-
} catch (_) {}
|
|
201
|
-
if (pyodidePackage) {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Ensure micropip is loaded which can fetch packages from PyPi.
|
|
206
|
-
// See: https://pyodide.org/en/stable/usage/loading-packages.html
|
|
207
|
-
if (!pyodide.micropip) {
|
|
208
|
-
await pyodide.loadPackage("micropip");
|
|
209
|
-
pyodide.micropip = pyodide.pyimport("micropip");
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// If the import is for a PyPi package then load it.
|
|
213
|
-
// Otherwise, don't error now so that we get an error later from Python.
|
|
214
|
-
await pyodide.micropip.install(name).catch(() => {});
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const vendoredPackages = {
|
|
218
|
-
// Support for https://pypi.org/project/py-enigma/ due to package not having a whl file on PyPi.
|
|
219
|
-
enigma: {
|
|
220
|
-
before: async () => {
|
|
221
|
-
await pyodide.loadPackage(toAbsoluteFromOrigin(assets.enigmaWhlUrl));
|
|
222
|
-
},
|
|
223
|
-
after: () => {},
|
|
224
|
-
},
|
|
225
|
-
turtle: {
|
|
226
|
-
before: async () => {
|
|
227
|
-
pyodide.registerJsModule("basthon", fakeBasthonPackage);
|
|
228
|
-
await pyodide.loadPackage(toAbsoluteFromOrigin(assets.turtleWhlUrl));
|
|
229
|
-
},
|
|
230
|
-
after: () =>
|
|
231
|
-
pyodide.runPython(`
|
|
232
|
-
import turtle
|
|
233
|
-
import basthon
|
|
234
|
-
|
|
235
|
-
svg_dict = turtle.Screen().show_scene()
|
|
236
|
-
basthon.kernel.display_event({ "display_type": "turtle", "content": svg_dict })
|
|
237
|
-
turtle.restart()
|
|
238
|
-
`),
|
|
239
|
-
},
|
|
240
|
-
p5: {
|
|
241
|
-
before: async () => {
|
|
242
|
-
pyodide.registerJsModule("basthon", fakeBasthonPackage);
|
|
243
|
-
await pyodide.loadPackage([
|
|
244
|
-
"setuptools",
|
|
245
|
-
toAbsoluteFromOrigin(assets.p5WhlUrl),
|
|
246
|
-
]);
|
|
247
|
-
},
|
|
248
|
-
after: () => {},
|
|
249
|
-
},
|
|
250
|
-
pygal: {
|
|
251
|
-
before: () => {
|
|
252
|
-
pyodide.registerJsModule("pygal", { ...pygal });
|
|
253
|
-
pygal.config.renderChart = (content) => {
|
|
254
|
-
postMessage({ method: "handleVisual", origin: "pygal", content });
|
|
255
|
-
};
|
|
256
|
-
},
|
|
257
|
-
after: () => {},
|
|
258
|
-
},
|
|
259
|
-
matplotlib: {
|
|
260
|
-
before: async () => {
|
|
261
|
-
pyodide.registerJsModule("basthon", fakeBasthonPackage);
|
|
262
|
-
// Patch the document object to prevent matplotlib from trying to render. Since we are running in a web worker,
|
|
263
|
-
// the document object is not available. We will instead capture the image and send it back to the main thread.
|
|
264
|
-
pyodide.runPython(`
|
|
265
|
-
import js
|
|
266
|
-
|
|
267
|
-
class __DummyDocument__:
|
|
268
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
269
|
-
return
|
|
270
|
-
def __getattr__(self, __name: str):
|
|
271
|
-
return __DummyDocument__
|
|
272
|
-
js.document = __DummyDocument__()
|
|
273
|
-
`);
|
|
274
|
-
await pyodide.loadPackage("matplotlib")?.catch(() => {});
|
|
275
|
-
let pyodidePackage;
|
|
276
|
-
try {
|
|
277
|
-
pyodidePackage = pyodide.pyimport("matplotlib");
|
|
278
|
-
} catch (_) {}
|
|
279
|
-
if (pyodidePackage) {
|
|
280
|
-
pyodide.runPython(`
|
|
281
|
-
import matplotlib.pyplot as plt
|
|
282
|
-
import io
|
|
283
|
-
import basthon
|
|
284
|
-
|
|
285
|
-
def show_chart():
|
|
286
|
-
bytes_io = io.BytesIO()
|
|
287
|
-
plt.savefig(bytes_io, format='jpg')
|
|
288
|
-
bytes_io.seek(0)
|
|
289
|
-
basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
|
|
290
|
-
plt.show = show_chart
|
|
291
|
-
`);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
},
|
|
295
|
-
after: () => {
|
|
296
|
-
pyodide.runPython(`
|
|
297
|
-
import matplotlib.pyplot as plt
|
|
298
|
-
plt.clf()
|
|
299
|
-
`);
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
seaborn: {
|
|
303
|
-
before: async () => {
|
|
304
|
-
pyodide.registerJsModule("basthon", fakeBasthonPackage);
|
|
305
|
-
// Patch the document object to prevent matplotlib from trying to render. Since we are running in a web worker,
|
|
306
|
-
// the document object is not available. We will instead capture the image and send it back to the main thread.
|
|
307
|
-
pyodide.runPython(`
|
|
308
|
-
import js
|
|
309
|
-
|
|
310
|
-
class __DummyDocument__:
|
|
311
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
312
|
-
return
|
|
313
|
-
def __getattr__(self, __name: str):
|
|
314
|
-
return __DummyDocument__
|
|
315
|
-
js.document = __DummyDocument__()
|
|
316
|
-
`);
|
|
317
|
-
|
|
318
|
-
// Ensure micropip is loaded which can fetch packages from PyPi.
|
|
319
|
-
// See: https://pyodide.org/en/stable/usage/loading-packages.html
|
|
320
|
-
if (!pyodide.micropip) {
|
|
321
|
-
await pyodide.loadPackage("micropip");
|
|
322
|
-
pyodide.micropip = pyodide.pyimport("micropip");
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// If the import is for a PyPi package then load it.
|
|
326
|
-
// Otherwise, don't error now so that we get an error later from Python.
|
|
327
|
-
await pyodide.micropip.install("seaborn").catch(() => {});
|
|
328
|
-
},
|
|
329
|
-
after: () => {
|
|
330
|
-
pyodide.runPython(`
|
|
331
|
-
import matplotlib.pyplot as plt
|
|
332
|
-
import io
|
|
333
|
-
import basthon
|
|
334
|
-
|
|
335
|
-
def is_plot_empty():
|
|
336
|
-
fig = plt.gcf()
|
|
337
|
-
for ax in fig.get_axes():
|
|
338
|
-
# Check if the axes contain any lines, patches, collections, etc.
|
|
339
|
-
if ax.lines or ax.patches or ax.collections or ax.images or ax.texts:
|
|
340
|
-
return False
|
|
341
|
-
return True
|
|
342
|
-
|
|
343
|
-
if not is_plot_empty():
|
|
344
|
-
bytes_io = io.BytesIO()
|
|
345
|
-
plt.savefig(bytes_io, format='jpg')
|
|
346
|
-
bytes_io.seek(0)
|
|
347
|
-
basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
|
|
348
|
-
|
|
349
|
-
plt.clf()
|
|
350
|
-
`);
|
|
351
|
-
},
|
|
352
|
-
},
|
|
353
|
-
plotly: {
|
|
354
|
-
before: async () => {
|
|
355
|
-
if (!pyodide.micropip) {
|
|
356
|
-
await pyodide.loadPackage("micropip");
|
|
357
|
-
pyodide.micropip = pyodide.pyimport("micropip");
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// If the import is for a PyPi package then load it.
|
|
361
|
-
// Otherwise, don't error now so that we get an error later from Python.
|
|
362
|
-
await pyodide.micropip.install("plotly").catch(() => {});
|
|
363
|
-
await pyodide.micropip.install("pandas").catch(() => {});
|
|
364
|
-
pyodide.registerJsModule("basthon", fakeBasthonPackage);
|
|
365
|
-
pyodide.runPython(`
|
|
366
|
-
import plotly.graph_objs as go
|
|
367
|
-
|
|
368
|
-
def _hacked_show(self, *args, **kwargs):
|
|
369
|
-
basthon.kernel.display_event({
|
|
370
|
-
"display_type": "plotly",
|
|
371
|
-
"content": self.to_json()
|
|
372
|
-
})
|
|
373
|
-
|
|
374
|
-
go.Figure.show = _hacked_show
|
|
375
|
-
`);
|
|
376
|
-
},
|
|
377
|
-
after: () => {},
|
|
378
|
-
},
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
const fakeBasthonPackage = {
|
|
382
|
-
kernel: {
|
|
383
|
-
display_event: (event) => {
|
|
384
|
-
const origin = event.toJs().get("display_type");
|
|
385
|
-
const content = event.toJs().get("content");
|
|
386
|
-
|
|
387
|
-
postMessage({ method: "handleVisual", origin, content });
|
|
388
|
-
},
|
|
389
|
-
write_file: (event) => {
|
|
390
|
-
const filename = event.toJs().get("filename");
|
|
391
|
-
const content = event.toJs().get("content");
|
|
392
|
-
const mode = event.toJs().get("mode");
|
|
393
|
-
postMessage({ method: "handleFileWrite", filename, content, mode });
|
|
394
|
-
},
|
|
395
|
-
locals: () => pyodide.runPython("globals()"),
|
|
396
|
-
},
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
const clearPyodideData = async () => {
|
|
400
|
-
postMessage({ method: "handleLoading" });
|
|
401
|
-
console.log("clearPyodideData");
|
|
402
|
-
await pyodide.runPythonAsync(`
|
|
403
|
-
# Clear all user-defined variables and modules
|
|
404
|
-
for name in dir():
|
|
405
|
-
if not name.startswith('_') and not name=='basthon':
|
|
406
|
-
del globals()[name]
|
|
407
|
-
`);
|
|
408
|
-
console.log("clearPyodideData done");
|
|
409
|
-
postMessage({ method: "handleLoaded", stdinBuffer, interruptBuffer });
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
const initialisePyodide = async () => {
|
|
413
|
-
postMessage({ method: "handleLoading" });
|
|
414
|
-
|
|
415
|
-
pyodidePromise = loadPyodide({
|
|
416
|
-
stdout: (content) =>
|
|
417
|
-
postMessage({ method: "handleOutput", stream: "stdout", content }),
|
|
418
|
-
stderr: (content) =>
|
|
419
|
-
postMessage({ method: "handleOutput", stream: "stderr", content }),
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
pyodide = await pyodidePromise;
|
|
423
|
-
|
|
424
|
-
pyodide.registerJsModule("basthon", fakeBasthonPackage);
|
|
425
|
-
|
|
426
|
-
await pyodide.runPythonAsync(`
|
|
427
|
-
__old_input__ = input
|
|
428
|
-
def __patched_input__(prompt=False):
|
|
429
|
-
if (prompt):
|
|
430
|
-
print(prompt)
|
|
431
|
-
return __old_input__()
|
|
432
|
-
__builtins__.input = __patched_input__
|
|
433
|
-
`);
|
|
434
|
-
|
|
435
|
-
await pyodide.runPythonAsync(`
|
|
436
|
-
import builtins
|
|
437
|
-
# Save the original open function
|
|
438
|
-
_original_open = builtins.open
|
|
439
|
-
`);
|
|
440
|
-
|
|
441
|
-
await pyodide.loadPackage("pyodide-http");
|
|
442
|
-
await pyodide.runPythonAsync(`
|
|
443
|
-
import pyodide_http
|
|
444
|
-
pyodide_http.patch_all()
|
|
445
|
-
`);
|
|
446
|
-
|
|
447
|
-
if (supportsAllFeatures) {
|
|
448
|
-
stdinBuffer =
|
|
449
|
-
stdinBuffer || new Int32Array(new SharedArrayBuffer(1024 * 1024)); // 1 MiB
|
|
450
|
-
stdinBuffer[0] = 1; // Store the length of content in the buffer at index 0.
|
|
451
|
-
pyodide.setStdin({ isatty: true, read: readFromStdin });
|
|
452
|
-
|
|
453
|
-
interruptBuffer =
|
|
454
|
-
interruptBuffer || new Uint8Array(new SharedArrayBuffer(1));
|
|
455
|
-
pyodide.setInterruptBuffer(interruptBuffer);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
postMessage({ method: "handleLoaded", stdinBuffer, interruptBuffer });
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
const readFromStdin = (bufferToWrite) => {
|
|
462
|
-
const previousLength = stdinBuffer[0];
|
|
463
|
-
postMessage({ method: "handleInput" });
|
|
464
|
-
|
|
465
|
-
while (true) {
|
|
466
|
-
pyodide.checkInterrupt();
|
|
467
|
-
const result = Atomics.wait(stdinBuffer, 0, previousLength, 100);
|
|
468
|
-
if (result === "not-equal") {
|
|
469
|
-
break;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const currentLength = stdinBuffer[0];
|
|
474
|
-
if (currentLength === -1) {
|
|
475
|
-
return 0;
|
|
476
|
-
} // Signals that stdin was closed.
|
|
477
|
-
|
|
478
|
-
const addedBytes = stdinBuffer.slice(previousLength, currentLength);
|
|
479
|
-
bufferToWrite.set(addedBytes);
|
|
480
|
-
|
|
481
|
-
return addedBytes.length;
|
|
482
|
-
};
|
|
483
|
-
|
|
484
|
-
const parsePythonError = (error) => {
|
|
485
|
-
const type = error.type;
|
|
486
|
-
const [trace, info] = error.message.split(`${type}:`).map((s) => s?.trim());
|
|
487
|
-
|
|
488
|
-
const lines = trace.split("\n");
|
|
489
|
-
|
|
490
|
-
// if the third from last line matches /File "__custom_open__\.py", line (\d+)/g then strip off the last three lines
|
|
491
|
-
if (
|
|
492
|
-
lines.length > 3 &&
|
|
493
|
-
/File "__custom_open__\.py", line (\d+)/g.test(lines[lines.length - 3])
|
|
494
|
-
) {
|
|
495
|
-
lines.splice(-3, 3);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
const snippetLine = lines[lines.length - 2]; // print("hi")invalid
|
|
499
|
-
const caretLine = lines[lines.length - 1]; // ^^^^^^^
|
|
500
|
-
|
|
501
|
-
const showsMistake = caretLine.includes("^");
|
|
502
|
-
const mistake = showsMistake
|
|
503
|
-
? [snippetLine.slice(4), caretLine.slice(4)].join("\n")
|
|
504
|
-
: "";
|
|
505
|
-
|
|
506
|
-
const matches = [
|
|
507
|
-
...trace.matchAll(/File "(?!__custom_open__\.py)(.*)", line (\d+)/g),
|
|
508
|
-
];
|
|
509
|
-
const match = matches[matches.length - 1];
|
|
510
|
-
|
|
511
|
-
const path = match ? match[1] : "";
|
|
512
|
-
const base = path.split("/").reverse()[0];
|
|
513
|
-
const file = base === "<exec>" ? "main.py" : base;
|
|
514
|
-
|
|
515
|
-
const line = match ? parseInt(match[2], 10) : "";
|
|
516
|
-
|
|
517
|
-
return { file, line, mistake, type, info };
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
// return {
|
|
521
|
-
// postMessage,
|
|
522
|
-
// onmessage,
|
|
523
|
-
// };
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
globalThis.PyodideWorker = PyodideWorker;
|
|
527
|
-
PyodideWorker();
|
|
528
|
-
// export default PyodideWorker;
|
|
1
|
+
/* global globalThis, importScripts, loadPyodide, SharedArrayBuffer, Atomics, pygal */
|
|
2
|
+
|
|
3
|
+
function toAbsoluteFromOrigin(url) {
|
|
4
|
+
if (url.startsWith("http") || url.startsWith("/")) return url;
|
|
5
|
+
return new URL(url, self.location.origin).href;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Nest the PyodideWorker function inside a globalThis object so we control when its initialised.
|
|
9
|
+
const PyodideWorker = () => {
|
|
10
|
+
let assets;
|
|
11
|
+
|
|
12
|
+
const handleInit = (data) => {
|
|
13
|
+
assets = data.assets;
|
|
14
|
+
|
|
15
|
+
// Import scripts dynamically based on the environment
|
|
16
|
+
console.log("PyodideWorker", "importing scripts");
|
|
17
|
+
importScripts(toAbsoluteFromOrigin(assets.pygalUrl));
|
|
18
|
+
|
|
19
|
+
assets.pyodideBaseUrl =
|
|
20
|
+
"https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.js";
|
|
21
|
+
importScripts(toAbsoluteFromOrigin(assets.pyodideBaseUrl));
|
|
22
|
+
|
|
23
|
+
initialisePyodide();
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const supportsAllFeatures = typeof SharedArrayBuffer !== "undefined";
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line no-restricted-globals
|
|
29
|
+
if (!supportsAllFeatures && name !== "incremental-features") {
|
|
30
|
+
console.warn(
|
|
31
|
+
[
|
|
32
|
+
"The code editor will not be able to capture standard input or stop execution because these HTTP headers are not set:",
|
|
33
|
+
" - Cross-Origin-Opener-Policy: same-origin",
|
|
34
|
+
" - Cross-Origin-Embedder-Policy: require-corp",
|
|
35
|
+
"",
|
|
36
|
+
"If your app can cope with or without these features, please initialize the web worker with { name: 'incremental-features' } to silence this warning.",
|
|
37
|
+
"You can then check for the presence of { stdinBuffer, interruptBuffer } in the handleLoaded message to check whether these features are supported.",
|
|
38
|
+
"",
|
|
39
|
+
"If you definitely need these features, either configure your server to respond with the HTTP headers above, or register a service worker.",
|
|
40
|
+
"Once the HTTP headers are set, the browser will block cross-domain resources so you will need to add 'crossorigin' to <script> and other tags.",
|
|
41
|
+
"You may wish to scope the HTTP headers to only those pages that host the code editor to make the browser restriction easier to deal with.",
|
|
42
|
+
"",
|
|
43
|
+
"Please refer to these code snippets for registering a service worker:",
|
|
44
|
+
" - https://github.com/RaspberryPiFoundation/python-execution-prototypes/blob/fd2c50e032cba3bb0e92e19a88eb62e5b120fe7a/pyodide/index.html#L92-L98",
|
|
45
|
+
" - https://github.com/RaspberryPiFoundation/python-execution-prototypes/blob/fd2c50e032cba3bb0e92e19a88eb62e5b120fe7a/pyodide/serviceworker.js",
|
|
46
|
+
].join("\n"),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
let pyodide, pyodidePromise, stdinBuffer, interruptBuffer, stopped;
|
|
50
|
+
|
|
51
|
+
const onmessage = async ({ data }) => {
|
|
52
|
+
pyodide = await pyodidePromise;
|
|
53
|
+
let encoder = new TextEncoder();
|
|
54
|
+
|
|
55
|
+
switch (data.method) {
|
|
56
|
+
case "init":
|
|
57
|
+
handleInit(data);
|
|
58
|
+
break;
|
|
59
|
+
case "writeFile":
|
|
60
|
+
pyodide.FS.writeFile(data.filename, encoder.encode(data.content));
|
|
61
|
+
break;
|
|
62
|
+
case "runPython":
|
|
63
|
+
runPython(data.python);
|
|
64
|
+
break;
|
|
65
|
+
case "stopPython":
|
|
66
|
+
stopped = true;
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
throw new Error(`Unsupported method: ${data.method}`);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// eslint-disable-next-line no-restricted-globals
|
|
74
|
+
addEventListener("message", async (event) => {
|
|
75
|
+
onmessage(event);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const runPython = async (python) => {
|
|
79
|
+
stopped = false;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await withSupportForPackages(python, async () => {
|
|
83
|
+
await pyodide.runPython(python);
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (!(error instanceof pyodide.ffi.PythonError)) {
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
postMessage({ method: "handleError", ...parsePythonError(error) });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await clearPyodideData();
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const checkIfStopped = () => {
|
|
96
|
+
if (stopped) {
|
|
97
|
+
throw new pyodide.ffi.PythonError("KeyboardInterrupt");
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const withSupportForPackages = async (
|
|
102
|
+
python,
|
|
103
|
+
runPythonFn = async () => {},
|
|
104
|
+
) => {
|
|
105
|
+
const imports = await pyodide._api.pyodide_code.find_imports(python).toJs();
|
|
106
|
+
await Promise.all(imports.map((name) => loadDependency(name)));
|
|
107
|
+
|
|
108
|
+
checkIfStopped();
|
|
109
|
+
await pyodide.loadPackagesFromImports(python);
|
|
110
|
+
|
|
111
|
+
checkIfStopped();
|
|
112
|
+
await pyodide.runPythonAsync(
|
|
113
|
+
`
|
|
114
|
+
import basthon
|
|
115
|
+
import builtins
|
|
116
|
+
import os
|
|
117
|
+
|
|
118
|
+
MAX_FILES = 100
|
|
119
|
+
MAX_FILE_SIZE = 8500000
|
|
120
|
+
|
|
121
|
+
def _custom_open(filename, mode="r", *args, **kwargs):
|
|
122
|
+
if "x" in mode and os.path.exists(filename):
|
|
123
|
+
raise FileExistsError(f"File '{filename}' already exists")
|
|
124
|
+
if ("w" in mode or "a" in mode or "x" in mode) and "b" not in mode:
|
|
125
|
+
if len(os.listdir()) > MAX_FILES and not os.path.exists(filename):
|
|
126
|
+
raise OSError(f"File system limit reached, no more than {MAX_FILES} files allowed")
|
|
127
|
+
class CustomFile:
|
|
128
|
+
def __init__(self, filename):
|
|
129
|
+
self.filename = filename
|
|
130
|
+
self.content = ""
|
|
131
|
+
|
|
132
|
+
def write(self, content):
|
|
133
|
+
self.content += content
|
|
134
|
+
if len(self.content) > MAX_FILE_SIZE:
|
|
135
|
+
raise OSError(f"File '{self.filename}' exceeds maximum file size of {MAX_FILE_SIZE} bytes")
|
|
136
|
+
with _original_open(self.filename, mode) as f:
|
|
137
|
+
f.write(self.content)
|
|
138
|
+
basthon.kernel.write_file({ "filename": self.filename, "content": self.content, "mode": mode })
|
|
139
|
+
|
|
140
|
+
def close(self):
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
def __enter__(self):
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
147
|
+
self.close()
|
|
148
|
+
|
|
149
|
+
return CustomFile(filename)
|
|
150
|
+
else:
|
|
151
|
+
return _original_open(filename, mode, *args, **kwargs)
|
|
152
|
+
|
|
153
|
+
# Override the built-in open function
|
|
154
|
+
builtins.open = _custom_open
|
|
155
|
+
`,
|
|
156
|
+
{ filename: "__custom_open__.py" },
|
|
157
|
+
);
|
|
158
|
+
await runPythonFn();
|
|
159
|
+
|
|
160
|
+
for (let name of imports) {
|
|
161
|
+
checkIfStopped();
|
|
162
|
+
await vendoredPackages[name]?.after();
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const loadDependency = async (name) => {
|
|
167
|
+
checkIfStopped();
|
|
168
|
+
|
|
169
|
+
// If the import is for another user file then open it and load its dependencies.
|
|
170
|
+
if (pyodide.FS.readdir("/home/pyodide").includes(`${name}.py`)) {
|
|
171
|
+
const fileContent = pyodide.FS.readFile(`/home/pyodide/${name}.py`, {
|
|
172
|
+
encoding: "utf8",
|
|
173
|
+
});
|
|
174
|
+
await withSupportForPackages(fileContent);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// If the import is for a vendored package then run its .before() hook.
|
|
179
|
+
const vendoredPackage = vendoredPackages[name];
|
|
180
|
+
await vendoredPackage?.before();
|
|
181
|
+
if (vendoredPackage) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// If the import is for a module built into Python then do nothing.
|
|
186
|
+
let pythonModule;
|
|
187
|
+
try {
|
|
188
|
+
pythonModule = pyodide.pyimport(name);
|
|
189
|
+
} catch (_) {}
|
|
190
|
+
if (pythonModule) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// If the import is for a package built into Pyodide then load it.
|
|
195
|
+
// Built-ins: https://pyodide.org/en/stable/usage/packages-in-pyodide.html
|
|
196
|
+
await pyodide.loadPackage(name)?.catch(() => {});
|
|
197
|
+
let pyodidePackage;
|
|
198
|
+
try {
|
|
199
|
+
pyodidePackage = pyodide.pyimport(name);
|
|
200
|
+
} catch (_) {}
|
|
201
|
+
if (pyodidePackage) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Ensure micropip is loaded which can fetch packages from PyPi.
|
|
206
|
+
// See: https://pyodide.org/en/stable/usage/loading-packages.html
|
|
207
|
+
if (!pyodide.micropip) {
|
|
208
|
+
await pyodide.loadPackage("micropip");
|
|
209
|
+
pyodide.micropip = pyodide.pyimport("micropip");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// If the import is for a PyPi package then load it.
|
|
213
|
+
// Otherwise, don't error now so that we get an error later from Python.
|
|
214
|
+
await pyodide.micropip.install(name).catch(() => {});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const vendoredPackages = {
|
|
218
|
+
// Support for https://pypi.org/project/py-enigma/ due to package not having a whl file on PyPi.
|
|
219
|
+
enigma: {
|
|
220
|
+
before: async () => {
|
|
221
|
+
await pyodide.loadPackage(toAbsoluteFromOrigin(assets.enigmaWhlUrl));
|
|
222
|
+
},
|
|
223
|
+
after: () => {},
|
|
224
|
+
},
|
|
225
|
+
turtle: {
|
|
226
|
+
before: async () => {
|
|
227
|
+
pyodide.registerJsModule("basthon", fakeBasthonPackage);
|
|
228
|
+
await pyodide.loadPackage(toAbsoluteFromOrigin(assets.turtleWhlUrl));
|
|
229
|
+
},
|
|
230
|
+
after: () =>
|
|
231
|
+
pyodide.runPython(`
|
|
232
|
+
import turtle
|
|
233
|
+
import basthon
|
|
234
|
+
|
|
235
|
+
svg_dict = turtle.Screen().show_scene()
|
|
236
|
+
basthon.kernel.display_event({ "display_type": "turtle", "content": svg_dict })
|
|
237
|
+
turtle.restart()
|
|
238
|
+
`),
|
|
239
|
+
},
|
|
240
|
+
p5: {
|
|
241
|
+
before: async () => {
|
|
242
|
+
pyodide.registerJsModule("basthon", fakeBasthonPackage);
|
|
243
|
+
await pyodide.loadPackage([
|
|
244
|
+
"setuptools",
|
|
245
|
+
toAbsoluteFromOrigin(assets.p5WhlUrl),
|
|
246
|
+
]);
|
|
247
|
+
},
|
|
248
|
+
after: () => {},
|
|
249
|
+
},
|
|
250
|
+
pygal: {
|
|
251
|
+
before: () => {
|
|
252
|
+
pyodide.registerJsModule("pygal", { ...pygal });
|
|
253
|
+
pygal.config.renderChart = (content) => {
|
|
254
|
+
postMessage({ method: "handleVisual", origin: "pygal", content });
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
after: () => {},
|
|
258
|
+
},
|
|
259
|
+
matplotlib: {
|
|
260
|
+
before: async () => {
|
|
261
|
+
pyodide.registerJsModule("basthon", fakeBasthonPackage);
|
|
262
|
+
// Patch the document object to prevent matplotlib from trying to render. Since we are running in a web worker,
|
|
263
|
+
// the document object is not available. We will instead capture the image and send it back to the main thread.
|
|
264
|
+
pyodide.runPython(`
|
|
265
|
+
import js
|
|
266
|
+
|
|
267
|
+
class __DummyDocument__:
|
|
268
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
269
|
+
return
|
|
270
|
+
def __getattr__(self, __name: str):
|
|
271
|
+
return __DummyDocument__
|
|
272
|
+
js.document = __DummyDocument__()
|
|
273
|
+
`);
|
|
274
|
+
await pyodide.loadPackage("matplotlib")?.catch(() => {});
|
|
275
|
+
let pyodidePackage;
|
|
276
|
+
try {
|
|
277
|
+
pyodidePackage = pyodide.pyimport("matplotlib");
|
|
278
|
+
} catch (_) {}
|
|
279
|
+
if (pyodidePackage) {
|
|
280
|
+
pyodide.runPython(`
|
|
281
|
+
import matplotlib.pyplot as plt
|
|
282
|
+
import io
|
|
283
|
+
import basthon
|
|
284
|
+
|
|
285
|
+
def show_chart():
|
|
286
|
+
bytes_io = io.BytesIO()
|
|
287
|
+
plt.savefig(bytes_io, format='jpg')
|
|
288
|
+
bytes_io.seek(0)
|
|
289
|
+
basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
|
|
290
|
+
plt.show = show_chart
|
|
291
|
+
`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
after: () => {
|
|
296
|
+
pyodide.runPython(`
|
|
297
|
+
import matplotlib.pyplot as plt
|
|
298
|
+
plt.clf()
|
|
299
|
+
`);
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
seaborn: {
|
|
303
|
+
before: async () => {
|
|
304
|
+
pyodide.registerJsModule("basthon", fakeBasthonPackage);
|
|
305
|
+
// Patch the document object to prevent matplotlib from trying to render. Since we are running in a web worker,
|
|
306
|
+
// the document object is not available. We will instead capture the image and send it back to the main thread.
|
|
307
|
+
pyodide.runPython(`
|
|
308
|
+
import js
|
|
309
|
+
|
|
310
|
+
class __DummyDocument__:
|
|
311
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
312
|
+
return
|
|
313
|
+
def __getattr__(self, __name: str):
|
|
314
|
+
return __DummyDocument__
|
|
315
|
+
js.document = __DummyDocument__()
|
|
316
|
+
`);
|
|
317
|
+
|
|
318
|
+
// Ensure micropip is loaded which can fetch packages from PyPi.
|
|
319
|
+
// See: https://pyodide.org/en/stable/usage/loading-packages.html
|
|
320
|
+
if (!pyodide.micropip) {
|
|
321
|
+
await pyodide.loadPackage("micropip");
|
|
322
|
+
pyodide.micropip = pyodide.pyimport("micropip");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// If the import is for a PyPi package then load it.
|
|
326
|
+
// Otherwise, don't error now so that we get an error later from Python.
|
|
327
|
+
await pyodide.micropip.install("seaborn").catch(() => {});
|
|
328
|
+
},
|
|
329
|
+
after: () => {
|
|
330
|
+
pyodide.runPython(`
|
|
331
|
+
import matplotlib.pyplot as plt
|
|
332
|
+
import io
|
|
333
|
+
import basthon
|
|
334
|
+
|
|
335
|
+
def is_plot_empty():
|
|
336
|
+
fig = plt.gcf()
|
|
337
|
+
for ax in fig.get_axes():
|
|
338
|
+
# Check if the axes contain any lines, patches, collections, etc.
|
|
339
|
+
if ax.lines or ax.patches or ax.collections or ax.images or ax.texts:
|
|
340
|
+
return False
|
|
341
|
+
return True
|
|
342
|
+
|
|
343
|
+
if not is_plot_empty():
|
|
344
|
+
bytes_io = io.BytesIO()
|
|
345
|
+
plt.savefig(bytes_io, format='jpg')
|
|
346
|
+
bytes_io.seek(0)
|
|
347
|
+
basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
|
|
348
|
+
|
|
349
|
+
plt.clf()
|
|
350
|
+
`);
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
plotly: {
|
|
354
|
+
before: async () => {
|
|
355
|
+
if (!pyodide.micropip) {
|
|
356
|
+
await pyodide.loadPackage("micropip");
|
|
357
|
+
pyodide.micropip = pyodide.pyimport("micropip");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// If the import is for a PyPi package then load it.
|
|
361
|
+
// Otherwise, don't error now so that we get an error later from Python.
|
|
362
|
+
await pyodide.micropip.install("plotly").catch(() => {});
|
|
363
|
+
await pyodide.micropip.install("pandas").catch(() => {});
|
|
364
|
+
pyodide.registerJsModule("basthon", fakeBasthonPackage);
|
|
365
|
+
pyodide.runPython(`
|
|
366
|
+
import plotly.graph_objs as go
|
|
367
|
+
|
|
368
|
+
def _hacked_show(self, *args, **kwargs):
|
|
369
|
+
basthon.kernel.display_event({
|
|
370
|
+
"display_type": "plotly",
|
|
371
|
+
"content": self.to_json()
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
go.Figure.show = _hacked_show
|
|
375
|
+
`);
|
|
376
|
+
},
|
|
377
|
+
after: () => {},
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const fakeBasthonPackage = {
|
|
382
|
+
kernel: {
|
|
383
|
+
display_event: (event) => {
|
|
384
|
+
const origin = event.toJs().get("display_type");
|
|
385
|
+
const content = event.toJs().get("content");
|
|
386
|
+
|
|
387
|
+
postMessage({ method: "handleVisual", origin, content });
|
|
388
|
+
},
|
|
389
|
+
write_file: (event) => {
|
|
390
|
+
const filename = event.toJs().get("filename");
|
|
391
|
+
const content = event.toJs().get("content");
|
|
392
|
+
const mode = event.toJs().get("mode");
|
|
393
|
+
postMessage({ method: "handleFileWrite", filename, content, mode });
|
|
394
|
+
},
|
|
395
|
+
locals: () => pyodide.runPython("globals()"),
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const clearPyodideData = async () => {
|
|
400
|
+
postMessage({ method: "handleLoading" });
|
|
401
|
+
console.log("clearPyodideData");
|
|
402
|
+
await pyodide.runPythonAsync(`
|
|
403
|
+
# Clear all user-defined variables and modules
|
|
404
|
+
for name in dir():
|
|
405
|
+
if not name.startswith('_') and not name=='basthon':
|
|
406
|
+
del globals()[name]
|
|
407
|
+
`);
|
|
408
|
+
console.log("clearPyodideData done");
|
|
409
|
+
postMessage({ method: "handleLoaded", stdinBuffer, interruptBuffer });
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const initialisePyodide = async () => {
|
|
413
|
+
postMessage({ method: "handleLoading" });
|
|
414
|
+
|
|
415
|
+
pyodidePromise = loadPyodide({
|
|
416
|
+
stdout: (content) =>
|
|
417
|
+
postMessage({ method: "handleOutput", stream: "stdout", content }),
|
|
418
|
+
stderr: (content) =>
|
|
419
|
+
postMessage({ method: "handleOutput", stream: "stderr", content }),
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
pyodide = await pyodidePromise;
|
|
423
|
+
|
|
424
|
+
pyodide.registerJsModule("basthon", fakeBasthonPackage);
|
|
425
|
+
|
|
426
|
+
await pyodide.runPythonAsync(`
|
|
427
|
+
__old_input__ = input
|
|
428
|
+
def __patched_input__(prompt=False):
|
|
429
|
+
if (prompt):
|
|
430
|
+
print(prompt)
|
|
431
|
+
return __old_input__()
|
|
432
|
+
__builtins__.input = __patched_input__
|
|
433
|
+
`);
|
|
434
|
+
|
|
435
|
+
await pyodide.runPythonAsync(`
|
|
436
|
+
import builtins
|
|
437
|
+
# Save the original open function
|
|
438
|
+
_original_open = builtins.open
|
|
439
|
+
`);
|
|
440
|
+
|
|
441
|
+
await pyodide.loadPackage("pyodide-http");
|
|
442
|
+
await pyodide.runPythonAsync(`
|
|
443
|
+
import pyodide_http
|
|
444
|
+
pyodide_http.patch_all()
|
|
445
|
+
`);
|
|
446
|
+
|
|
447
|
+
if (supportsAllFeatures) {
|
|
448
|
+
stdinBuffer =
|
|
449
|
+
stdinBuffer || new Int32Array(new SharedArrayBuffer(1024 * 1024)); // 1 MiB
|
|
450
|
+
stdinBuffer[0] = 1; // Store the length of content in the buffer at index 0.
|
|
451
|
+
pyodide.setStdin({ isatty: true, read: readFromStdin });
|
|
452
|
+
|
|
453
|
+
interruptBuffer =
|
|
454
|
+
interruptBuffer || new Uint8Array(new SharedArrayBuffer(1));
|
|
455
|
+
pyodide.setInterruptBuffer(interruptBuffer);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
postMessage({ method: "handleLoaded", stdinBuffer, interruptBuffer });
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const readFromStdin = (bufferToWrite) => {
|
|
462
|
+
const previousLength = stdinBuffer[0];
|
|
463
|
+
postMessage({ method: "handleInput" });
|
|
464
|
+
|
|
465
|
+
while (true) {
|
|
466
|
+
pyodide.checkInterrupt();
|
|
467
|
+
const result = Atomics.wait(stdinBuffer, 0, previousLength, 100);
|
|
468
|
+
if (result === "not-equal") {
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const currentLength = stdinBuffer[0];
|
|
474
|
+
if (currentLength === -1) {
|
|
475
|
+
return 0;
|
|
476
|
+
} // Signals that stdin was closed.
|
|
477
|
+
|
|
478
|
+
const addedBytes = stdinBuffer.slice(previousLength, currentLength);
|
|
479
|
+
bufferToWrite.set(addedBytes);
|
|
480
|
+
|
|
481
|
+
return addedBytes.length;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
const parsePythonError = (error) => {
|
|
485
|
+
const type = error.type;
|
|
486
|
+
const [trace, info] = error.message.split(`${type}:`).map((s) => s?.trim());
|
|
487
|
+
|
|
488
|
+
const lines = trace.split("\n");
|
|
489
|
+
|
|
490
|
+
// if the third from last line matches /File "__custom_open__\.py", line (\d+)/g then strip off the last three lines
|
|
491
|
+
if (
|
|
492
|
+
lines.length > 3 &&
|
|
493
|
+
/File "__custom_open__\.py", line (\d+)/g.test(lines[lines.length - 3])
|
|
494
|
+
) {
|
|
495
|
+
lines.splice(-3, 3);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const snippetLine = lines[lines.length - 2]; // print("hi")invalid
|
|
499
|
+
const caretLine = lines[lines.length - 1]; // ^^^^^^^
|
|
500
|
+
|
|
501
|
+
const showsMistake = caretLine.includes("^");
|
|
502
|
+
const mistake = showsMistake
|
|
503
|
+
? [snippetLine.slice(4), caretLine.slice(4)].join("\n")
|
|
504
|
+
: "";
|
|
505
|
+
|
|
506
|
+
const matches = [
|
|
507
|
+
...trace.matchAll(/File "(?!__custom_open__\.py)(.*)", line (\d+)/g),
|
|
508
|
+
];
|
|
509
|
+
const match = matches[matches.length - 1];
|
|
510
|
+
|
|
511
|
+
const path = match ? match[1] : "";
|
|
512
|
+
const base = path.split("/").reverse()[0];
|
|
513
|
+
const file = base === "<exec>" ? "main.py" : base;
|
|
514
|
+
|
|
515
|
+
const line = match ? parseInt(match[2], 10) : "";
|
|
516
|
+
|
|
517
|
+
return { file, line, mistake, type, info };
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// return {
|
|
521
|
+
// postMessage,
|
|
522
|
+
// onmessage,
|
|
523
|
+
// };
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
globalThis.PyodideWorker = PyodideWorker;
|
|
527
|
+
PyodideWorker();
|
|
528
|
+
// export default PyodideWorker;
|