gwchq-textjam 0.1.116 → 0.2.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.
@@ -1,747 +1,755 @@
1
- /* eslint-disable @typescript-eslint/no-empty-function */
2
- /* global globalThis, importScripts, loadPyodide, SharedArrayBuffer, Atomics, pygal */
3
-
4
- function toAbsoluteFromOrigin(url) {
5
- if (url.startsWith("http") || url.startsWith("/")) return url;
6
- // eslint-disable-next-line no-restricted-globals
7
- return new URL(url, self.location.origin).href;
8
- }
9
-
10
- const WORKING_DIR = "/home/pyodide";
11
-
12
- const toStructuredCloneable = (value) => {
13
- if (value == null) return value;
14
-
15
- if (
16
- typeof value === "string" ||
17
- typeof value === "number" ||
18
- typeof value === "boolean"
19
- ) {
20
- return value;
21
- }
22
-
23
- if (value instanceof Uint8Array) {
24
- return value.slice();
25
- }
26
-
27
- if (ArrayBuffer.isView(value)) {
28
- return new Uint8Array(
29
- value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength),
30
- );
31
- }
32
-
33
- if (value instanceof ArrayBuffer) {
34
- return value.slice(0);
35
- }
36
-
37
- if (value instanceof Map) {
38
- return Object.fromEntries(
39
- [...value.entries()].map(([k, v]) => [k, toStructuredCloneable(v)]),
40
- );
41
- }
42
-
43
- if (Array.isArray(value)) {
44
- return value.map(toStructuredCloneable);
45
- }
46
-
47
- if (typeof value?.toJs === "function") {
48
- try {
49
- const converted = value.toJs({ dict_converter: Object.fromEntries });
50
- return toStructuredCloneable(converted);
51
- } catch (_) {
52
- try {
53
- const converted = value.toJs();
54
- return toStructuredCloneable(converted);
55
- } catch (_) {
56
- return String(value);
57
- }
58
- }
59
- }
60
-
61
- if (typeof value === "object") {
62
- const result = {};
63
- for (const [key, val] of Object.entries(value)) {
64
- result[key] = toStructuredCloneable(val);
65
- }
66
- return result;
67
- }
68
-
69
- return String(value);
70
- };
71
-
72
- // Nest the PyodideWorker function inside a globalThis object so we control when its initialised.
73
- const PyodideWorker = () => {
74
- let assets;
75
- let packageApiUrl;
76
-
77
- const handleInit = (data) => {
78
- assets = data.assets;
79
- packageApiUrl = data.packageApiUrl;
80
-
81
- // Import scripts dynamically based on the environment
82
- console.log("PyodideWorker", "importing scripts");
83
- importScripts(toAbsoluteFromOrigin(assets.pygalUrl));
84
-
85
- assets.pyodideBaseUrl = `${packageApiUrl}/pyodide.js`;
86
- importScripts(toAbsoluteFromOrigin(assets.pyodideBaseUrl));
87
-
88
- initialisePyodide();
89
- };
90
-
91
- const supportsAllFeatures = typeof SharedArrayBuffer !== "undefined";
92
-
93
- // eslint-disable-next-line no-restricted-globals
94
- if (!supportsAllFeatures && name !== "incremental-features") {
95
- console.warn(
96
- [
97
- "The code editor will not be able to capture standard input or stop execution because these HTTP headers are not set:",
98
- " - Cross-Origin-Opener-Policy: same-origin",
99
- " - Cross-Origin-Embedder-Policy: require-corp",
100
- "",
101
- "If your app can cope with or without these features, please initialize the web worker with { name: 'incremental-features' } to silence this warning.",
102
- "You can then check for the presence of { stdinBuffer, interruptBuffer } in the handleLoaded message to check whether these features are supported.",
103
- "",
104
- "If you definitely need these features, either configure your server to respond with the HTTP headers above, or register a service worker.",
105
- "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.",
106
- "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.",
107
- "",
108
- "Please refer to these code snippets for registering a service worker:",
109
- " - https://github.com/RaspberryPiFoundation/python-execution-prototypes/blob/fd2c50e032cba3bb0e92e19a88eb62e5b120fe7a/pyodide/index.html#L92-L98",
110
- " - https://github.com/RaspberryPiFoundation/python-execution-prototypes/blob/fd2c50e032cba3bb0e92e19a88eb62e5b120fe7a/pyodide/serviceworker.js",
111
- ].join("\n"),
112
- );
113
- }
114
- let pyodide, pyodidePromise, stdinBuffer, interruptBuffer, stopped;
115
- /** When true, input() uses postMessage + run_sync(getInputAsync) instead of setStdin (requires JSPI). */
116
- let useMessageStdin = false;
117
- /** Used when SharedArrayBuffer is unavailable: resolve for the pending input() call. */
118
- let pendingStdinResolve = null;
119
- // Until Pyodide is fully initialised, keep stdout/stderr in the dev console only.
120
- // After initialisation, route them to the UI via postMessage.
121
- let userStdStreamsEnabled = false;
122
- // When true, suppress internal library / package loader logs from the UI console.
123
- let suppressInternalStdStreams = false;
124
-
125
- const onmessage = async ({ data }) => {
126
- pyodide = await pyodidePromise;
127
- let encoder = new TextEncoder();
128
-
129
- switch (data.method) {
130
- case "stdinResponse":
131
- if (pendingStdinResolve) {
132
- let content = data.ctrlD ? null : data.content ?? "";
133
- if (content && content.endsWith("\n")) {
134
- content = content.slice(0, -1);
135
- }
136
- pendingStdinResolve(content);
137
- pendingStdinResolve = null;
138
- }
139
- break;
140
- case "init":
141
- handleInit(data);
142
- break;
143
- case "createDirectories":
144
- if (Array.isArray(data.dirs)) {
145
- for (const dir of data.dirs) {
146
- try {
147
- pyodide.FS.mkdirTree(`${WORKING_DIR}/${dir}`);
148
- } catch (e) {
149
- console.error(`Failed to create directory ${dir}:`, e);
150
- }
151
- }
152
- }
153
- break;
154
- case "writeFile":
155
- pyodide.FS.writeFile(
156
- `${WORKING_DIR}/${data.filename}`,
157
- encoder.encode(data.content),
158
- );
159
- break;
160
- case "runPython":
161
- runPython(data.python, data.userModuleNames);
162
- break;
163
- case "stopPython":
164
- // Mark as stopped so future checks can raise KeyboardInterrupt.
165
- stopped = true;
166
- // If Python is currently blocked in input() via run_sync(getInputAsync),
167
- // resolve the pending stdin promise with EOF so execution can unwind
168
- // and clearPyodideData can run.
169
- if (pendingStdinResolve) {
170
- pendingStdinResolve(null);
171
- pendingStdinResolve = null;
172
- }
173
- break;
174
- default:
175
- throw new Error(`Unsupported method: ${data.method}`);
176
- }
177
- };
178
-
179
- // eslint-disable-next-line no-restricted-globals
180
- addEventListener("message", async (event) => {
181
- onmessage(event);
182
- });
183
-
184
- const runPython = async (python, userModuleNames) => {
185
- stopped = false;
186
-
187
- // When stdin uses postMessage (no SharedArrayBuffer), run_sync() in input() requires
188
- // runPythonAsync so that JSPI stack switching can suspend until the main thread sends stdinResponse.
189
- const runUserCode = useMessageStdin
190
- ? () => pyodide.runPythonAsync(python)
191
- : () => pyodide.runPython(python);
192
-
193
- try {
194
- await withSupportForPackages(python, async () => {
195
- await runUserCode();
196
- });
197
- } catch (error) {
198
- if (!(error instanceof pyodide.ffi.PythonError)) {
199
- throw error;
200
- }
201
- const parsed = parsePythonError(error);
202
- // Stop resolves stdin with EOF so input() raises EOFError; show as interrupt, not error.
203
- if (stopped && parsed.type === "EOFError") {
204
- postMessage({
205
- method: "handleError",
206
- ...parsed,
207
- type: "KeyboardInterrupt",
208
- info: "Execution interrupted",
209
- });
210
- } else {
211
- postMessage({ method: "handleError", ...parsed });
212
- }
213
- }
214
-
215
- await clearPyodideData(userModuleNames);
216
- };
217
-
218
- const checkIfStopped = () => {
219
- if (stopped) {
220
- throw new pyodide.ffi.PythonError("KeyboardInterrupt");
221
- }
222
- };
223
-
224
- const withSupportForPackages = async (
225
- python,
226
- runPythonFn = async () => {},
227
- ) => {
228
- // Suppress internal loader output (e.g. "Loading pyodide-http") from the
229
- // user console while resolving imports and loading packages.
230
- suppressInternalStdStreams = true;
231
- const imports = await pyodide._api.pyodide_code.find_imports(python).toJs();
232
- await Promise.all(imports.map((name) => loadDependency(name)));
233
-
234
- checkIfStopped();
235
- await pyodide.loadPackagesFromImports(python);
236
-
237
- checkIfStopped();
238
- await pyodide.runPythonAsync(
239
- `
240
- import basthon
241
- import builtins
242
- import os
243
-
244
- MAX_FILES = 100
245
- MAX_FILE_SIZE = 8500000
246
- PROJECT_ROOT = os.path.abspath("${WORKING_DIR}")
247
-
248
- def _is_project_file(filename):
249
- abs_path = os.path.abspath(filename)
250
- return abs_path == PROJECT_ROOT or abs_path.startswith(PROJECT_ROOT + os.sep)
251
-
252
- def _to_project_relative(filename):
253
- abs_path = os.path.abspath(filename)
254
- return os.path.relpath(abs_path, PROJECT_ROOT)
255
-
256
- def _custom_open(filename, mode="r", *args, **kwargs):
257
- abs_path = os.path.abspath(filename)
258
-
259
- if "x" in mode and os.path.exists(abs_path):
260
- raise FileExistsError(f"File '{filename}' already exists")
261
-
262
- is_text_write = ("w" in mode or "a" in mode or "x" in mode) and "b" not in mode
263
- is_project_file = _is_project_file(abs_path)
264
-
265
- if is_text_write and is_project_file:
266
- if len(os.listdir(PROJECT_ROOT)) > MAX_FILES and not os.path.exists(abs_path):
267
- raise OSError(f"File system limit reached, no more than {MAX_FILES} files allowed")
268
- class CustomFile:
269
- def __init__(self, filename):
270
- self.filename = filename
271
- self.content = ""
272
-
273
- def write(self, content):
274
- self.content += content
275
- if len(self.content) > MAX_FILE_SIZE:
276
- raise OSError(f"File '{self.filename}' exceeds maximum file size of {MAX_FILE_SIZE} bytes")
277
- with _original_open(self.filename, mode, *args, **kwargs) as f:
278
- f.write(self.content)
279
- basthon.kernel.write_file({
280
- "filename": _to_project_relative(self.filename),
281
- "content": self.content,
282
- "mode": mode
283
- })
284
-
285
- def close(self):
286
- pass
287
-
288
- def __enter__(self):
289
- return self
290
-
291
- def __exit__(self, exc_type, exc_val, exc_tb):
292
- self.close()
293
-
294
- return CustomFile(abs_path)
295
- else:
296
- return _original_open(filename, mode, *args, **kwargs)
297
-
298
- # Override the built-in open function
299
- builtins.open = _custom_open
300
- `,
301
- { filename: "__custom_open__.py" },
302
- );
303
-
304
- // Re-enable user-visible stdout / stderr for the actual user code run.
305
- suppressInternalStdStreams = false;
306
-
307
- await runPythonFn();
308
-
309
- for (let name of imports) {
310
- checkIfStopped();
311
- await vendoredPackages[name]?.after();
312
- }
313
- };
314
-
315
- const loadDependency = async (name) => {
316
- checkIfStopped();
317
-
318
- // If the import is for another user file then open it and load its dependencies.
319
- if (pyodide.FS.readdir(WORKING_DIR).includes(`${name}.py`)) {
320
- const fileContent = pyodide.FS.readFile(`${WORKING_DIR}/${name}.py`, {
321
- encoding: "utf8",
322
- });
323
- await withSupportForPackages(fileContent);
324
- return;
325
- }
326
-
327
- // If the import is for a vendored package then run its .before() hook.
328
- const vendoredPackage = vendoredPackages[name];
329
- await vendoredPackage?.before();
330
- if (vendoredPackage) {
331
- return;
332
- }
333
-
334
- // If the import is for a module built into Python then do nothing.
335
- let pythonModule;
336
- try {
337
- pythonModule = pyodide.pyimport(name);
338
- } catch (_) {}
339
- if (pythonModule) {
340
- return;
341
- }
342
-
343
- // If the import is for a package built into Pyodide then load it.
344
- // Built-ins: https://pyodide.org/en/stable/usage/packages-in-pyodide.html
345
- await pyodide.loadPackage(name)?.catch(() => {});
346
- let pyodidePackage;
347
- try {
348
- pyodidePackage = pyodide.pyimport(name);
349
- } catch (_) {}
350
- if (pyodidePackage) {
351
- return;
352
- }
353
-
354
- // Ensure micropip is loaded which can fetch packages from PyPi.
355
- // See: https://pyodide.org/en/stable/usage/loading-packages.html
356
- if (!pyodide.micropip) {
357
- await pyodide.loadPackage("micropip");
358
- pyodide.micropip = pyodide.pyimport("micropip");
359
- }
360
-
361
- // If the import is for a PyPi package then load it.
362
- // Otherwise, don't error now so that we get an error later from Python.
363
- await pyodide.micropip.install(name).catch(() => {});
364
- };
365
-
366
- const vendoredPackages = {
367
- // Support for https://pypi.org/project/py-enigma/ due to package not having a whl file on PyPi.
368
- enigma: {
369
- before: async () => {
370
- await pyodide.loadPackage(toAbsoluteFromOrigin(assets.enigmaWhlUrl));
371
- },
372
- after: () => {},
373
- },
374
- turtle: {
375
- before: async () => {
376
- pyodide.registerJsModule("basthon", fakeBasthonPackage);
377
- await pyodide.loadPackage(toAbsoluteFromOrigin(assets.turtleWhlUrl));
378
- },
379
- after: () =>
380
- pyodide.runPython(`
381
- import turtle
382
- import basthon
383
-
384
- svg_dict = turtle.Screen().show_scene()
385
- basthon.kernel.display_event({ "display_type": "turtle", "content": svg_dict })
386
- turtle.restart()
387
- `),
388
- },
389
- p5: {
390
- before: async () => {
391
- pyodide.registerJsModule("basthon", fakeBasthonPackage);
392
- await pyodide.loadPackage([
393
- "setuptools",
394
- toAbsoluteFromOrigin(assets.p5WhlUrl),
395
- ]);
396
- },
397
- after: () => {},
398
- },
399
- pygal: {
400
- before: () => {
401
- pyodide.registerJsModule("pygal", { ...pygal });
402
- pygal.config.renderChart = (content) => {
403
- postMessage({ method: "handleVisual", origin: "pygal", content });
404
- };
405
- },
406
- after: () => {},
407
- },
408
- matplotlib: {
409
- before: async () => {
410
- pyodide.registerJsModule("basthon", fakeBasthonPackage);
411
- // Patch the document object to prevent matplotlib from trying to render. Since we are running in a web worker,
412
- // the document object is not available. We will instead capture the image and send it back to the main thread.
413
- pyodide.runPython(`
414
- import js
415
-
416
- class __DummyDocument__:
417
- def __init__(self, *args, **kwargs) -> None:
418
- return
419
- def __getattr__(self, __name: str):
420
- return __DummyDocument__
421
- js.document = __DummyDocument__()
422
- `);
423
- await pyodide.loadPackage("matplotlib")?.catch(() => {});
424
- let pyodidePackage;
425
- try {
426
- pyodidePackage = pyodide.pyimport("matplotlib");
427
- } catch (_) {}
428
- if (pyodidePackage) {
429
- pyodide.runPython(`
430
- import matplotlib.pyplot as plt
431
- import io
432
- import basthon
433
-
434
- def show_chart():
435
- bytes_io = io.BytesIO()
436
- plt.savefig(bytes_io, format='jpg')
437
- bytes_io.seek(0)
438
- basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
439
- plt.show = show_chart
440
- `);
441
- return;
442
- }
443
- },
444
- after: () => {
445
- pyodide.runPython(`
446
- import matplotlib.pyplot as plt
447
- plt.clf()
448
- `);
449
- },
450
- },
451
- seaborn: {
452
- before: async () => {
453
- pyodide.registerJsModule("basthon", fakeBasthonPackage);
454
- // Patch the document object to prevent matplotlib from trying to render. Since we are running in a web worker,
455
- // the document object is not available. We will instead capture the image and send it back to the main thread.
456
- pyodide.runPython(`
457
- import js
458
-
459
- class __DummyDocument__:
460
- def __init__(self, *args, **kwargs) -> None:
461
- return
462
- def __getattr__(self, __name: str):
463
- return __DummyDocument__
464
- js.document = __DummyDocument__()
465
- `);
466
-
467
- // Ensure micropip is loaded which can fetch packages from PyPi.
468
- // See: https://pyodide.org/en/stable/usage/loading-packages.html
469
- if (!pyodide.micropip) {
470
- await pyodide.loadPackage("micropip");
471
- pyodide.micropip = pyodide.pyimport("micropip");
472
- }
473
-
474
- // If the import is for a PyPi package then load it.
475
- // Otherwise, don't error now so that we get an error later from Python.
476
- await pyodide.micropip.install("seaborn").catch(() => {});
477
- },
478
- after: () => {
479
- pyodide.runPython(`
480
- import matplotlib.pyplot as plt
481
- import io
482
- import basthon
483
-
484
- def is_plot_empty():
485
- fig = plt.gcf()
486
- for ax in fig.get_axes():
487
- # Check if the axes contain any lines, patches, collections, etc.
488
- if ax.lines or ax.patches or ax.collections or ax.images or ax.texts:
489
- return False
490
- return True
491
-
492
- if not is_plot_empty():
493
- bytes_io = io.BytesIO()
494
- plt.savefig(bytes_io, format='jpg')
495
- bytes_io.seek(0)
496
- basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
497
-
498
- plt.clf()
499
- `);
500
- },
501
- },
502
- plotly: {
503
- before: async () => {
504
- if (!pyodide.micropip) {
505
- await pyodide.loadPackage("micropip");
506
- pyodide.micropip = pyodide.pyimport("micropip");
507
- }
508
-
509
- // If the import is for a PyPi package then load it.
510
- // Otherwise, don't error now so that we get an error later from Python.
511
- await pyodide.micropip.install("plotly").catch(() => {});
512
- await pyodide.micropip.install("pandas").catch(() => {});
513
- pyodide.registerJsModule("basthon", fakeBasthonPackage);
514
- pyodide.runPython(`
515
- import plotly.graph_objs as go
516
-
517
- def _hacked_show(self, *args, **kwargs):
518
- basthon.kernel.display_event({
519
- "display_type": "plotly",
520
- "content": self.to_json()
521
- })
522
-
523
- go.Figure.show = _hacked_show
524
- `);
525
- },
526
- after: () => {},
527
- },
528
- };
529
-
530
- const fakeBasthonPackage = {
531
- kernel: {
532
- display_event: (event) => {
533
- let payload;
534
- try {
535
- payload = event.toJs({ dict_converter: Object.fromEntries });
536
- } catch (_) {
537
- payload = event.toJs();
538
- }
539
-
540
- const origin = String(payload.display_type);
541
- const content = toStructuredCloneable(payload.content);
542
-
543
- postMessage({ method: "handleVisual", origin, content });
544
- },
545
- write_file: (event) => {
546
- let payload;
547
- try {
548
- payload = event.toJs({ dict_converter: Object.fromEntries });
549
- } catch (_) {
550
- payload = event.toJs();
551
- }
552
-
553
- const filename = String(payload.filename);
554
- const content =
555
- typeof payload.content === "string"
556
- ? payload.content
557
- : toStructuredCloneable(payload.content);
558
- const mode = String(payload.mode);
559
-
560
- postMessage({ method: "handleFileWrite", filename, content, mode });
561
- },
562
- locals: () => pyodide.runPython("globals()"),
563
- /**
564
- * Returns a Promise that resolves with the next line of stdin when the main thread
565
- * sends stdinResponse. Resolves with null on EOF (e.g. Ctrl+D).
566
- * Used when SharedArrayBuffer is unavailable (no COOP/COEP) so we cannot block via Atomics.wait.
567
- * Requires JSPI and runPythonAsync() for run_sync() to work.
568
- */
569
- getInputAsync: () => {
570
- const promise = new Promise((resolve) => {
571
- pendingStdinResolve = resolve;
572
- });
573
- postMessage({ method: "handleInput" });
574
- return promise;
575
- },
576
- },
577
- };
578
-
579
- const clearPyodideData = async (userModuleNames) => {
580
- postMessage({ method: "handleLoading" });
581
- try {
582
- await pyodide.runPythonAsync(`
583
- # Clear all user-defined variables and modules
584
- for name in list(globals()):
585
- if not name.startswith('_') and not name=='basthon':
586
- del globals()[name]
587
-
588
- import sys
589
- # Remove user modules from sys.modules
590
- user_modules = ${JSON.stringify(userModuleNames)}
591
- for name in user_modules:
592
- if name in sys.modules:
593
- del sys.modules[name]
594
- `);
595
- } catch (error) {
596
- console.error("Error while clearing Pyodide data:", error);
597
- }
598
- console.log("clearPyodideData done");
599
- postMessage({ method: "handleLoaded", stdinBuffer, interruptBuffer });
600
- };
601
-
602
- const initialisePyodide = async () => {
603
- postMessage({ method: "handleLoading" });
604
-
605
- pyodidePromise = loadPyodide({
606
- stdout: (content) => {
607
- if (userStdStreamsEnabled && !suppressInternalStdStreams) {
608
- postMessage({ method: "handleOutput", stream: "stdout", content });
609
- } else {
610
- console.log(content);
611
- }
612
- },
613
- stderr: (content) => {
614
- if (userStdStreamsEnabled && !suppressInternalStdStreams) {
615
- postMessage({ method: "handleOutput", stream: "stderr", content });
616
- } else {
617
- console.error(content);
618
- }
619
- },
620
- });
621
-
622
- pyodide = await pyodidePromise;
623
-
624
- pyodide.registerJsModule("basthon", fakeBasthonPackage);
625
-
626
- // When SharedArrayBuffer is unavailable, always use the postMessage-based
627
- // stdin path. JSPI / run_sync will raise at runtime if the environment
628
- // cannot stack-switch, but in JSPI-capable browsers this enables
629
- // interactive input() without COOP/COEP.
630
- useMessageStdin = !supportsAllFeatures;
631
- pyodide.globals.set("__stdin_via_message__", useMessageStdin);
632
-
633
- await pyodide.runPythonAsync(`
634
- import builtins
635
- from pyodide.ffi import run_sync as __run_sync__
636
-
637
- __old_input__ = builtins.input
638
- def __patched_input__(prompt=None):
639
- if prompt is not None:
640
- print(prompt)
641
- if __stdin_via_message__:
642
- result = __run_sync__(basthon.kernel.getInputAsync())
643
- if result is None:
644
- raise EOFError("EOF when reading a line")
645
- return result
646
- return __old_input__()
647
- builtins.input = __patched_input__
648
- `);
649
-
650
- await pyodide.runPythonAsync(`
651
- import builtins
652
- # Save the original open function
653
- _original_open = builtins.open
654
- `);
655
-
656
- await pyodide.loadPackage("pyodide-http");
657
- await pyodide.runPythonAsync(`
658
- import pyodide_http
659
- pyodide_http.patch_all()
660
- `);
661
-
662
- if (supportsAllFeatures) {
663
- stdinBuffer =
664
- stdinBuffer || new Int32Array(new SharedArrayBuffer(1024 * 1024)); // 1 MiB
665
- stdinBuffer[0] = 1; // Store the length of content in the buffer at index 0.
666
- pyodide.setStdin({ isatty: true, read: readFromStdin });
667
-
668
- interruptBuffer =
669
- interruptBuffer || new Uint8Array(new SharedArrayBuffer(1));
670
- pyodide.setInterruptBuffer(interruptBuffer);
671
- }
672
-
673
- // From this point on, anything written to stdout / stderr is considered
674
- // user-visible and will be forwarded to the UI.
675
- userStdStreamsEnabled = true;
676
-
677
- postMessage({ method: "handleLoaded", stdinBuffer, interruptBuffer });
678
- };
679
-
680
- const readFromStdin = (bufferToWrite) => {
681
- const previousLength = stdinBuffer[0];
682
- postMessage({ method: "handleInput" });
683
-
684
- while (true) {
685
- pyodide.checkInterrupt();
686
- const result = Atomics.wait(stdinBuffer, 0, previousLength, 100);
687
- if (result === "not-equal") {
688
- break;
689
- }
690
- }
691
-
692
- const currentLength = stdinBuffer[0];
693
- if (currentLength === -1) {
694
- return 0;
695
- } // Signals that stdin was closed.
696
-
697
- const addedBytes = stdinBuffer.slice(previousLength, currentLength);
698
- bufferToWrite.set(addedBytes);
699
-
700
- return addedBytes.length;
701
- };
702
-
703
- const parsePythonError = (error) => {
704
- const type = error.type;
705
- const [trace, info] = error.message.split(`${type}:`).map((s) => s?.trim());
706
-
707
- const lines = trace.split("\n");
708
-
709
- // if the third from last line matches /File "__custom_open__\.py", line (\d+)/g then strip off the last three lines
710
- if (
711
- lines.length > 3 &&
712
- /File "__custom_open__\.py", line (\d+)/g.test(lines[lines.length - 3])
713
- ) {
714
- lines.splice(-3, 3);
715
- }
716
-
717
- const snippetLine = lines[lines.length - 2]; // print("hi")invalid
718
- const caretLine = lines[lines.length - 1]; // ^^^^^^^
719
-
720
- const showsMistake = caretLine.includes("^");
721
- const mistake = showsMistake
722
- ? [snippetLine.slice(4), caretLine.slice(4)].join("\n")
723
- : "";
724
-
725
- const matches = [
726
- ...trace.matchAll(/File "(?!__custom_open__\.py)(.*)", line (\d+)/g),
727
- ];
728
- const match = matches[matches.length - 1];
729
-
730
- const path = match ? match[1] : "";
731
- const base = path.split("/").reverse()[0];
732
- const file = base === "<exec>" ? "main.py" : base;
733
-
734
- const line = match ? parseInt(match[2], 10) : "";
735
-
736
- return { file, line, mistake, type, info };
737
- };
738
-
739
- // return {
740
- // postMessage,
741
- // onmessage,
742
- // };
743
- };
744
-
745
- globalThis.PyodideWorker = PyodideWorker;
746
- PyodideWorker();
747
- // export default PyodideWorker;
1
+ /* eslint-disable @typescript-eslint/no-empty-function */
2
+ /* global globalThis, importScripts, loadPyodide, SharedArrayBuffer, Atomics, pygal */
3
+
4
+ function toAbsoluteFromOrigin(url) {
5
+ if (url.startsWith("http") || url.startsWith("/")) return url;
6
+ // eslint-disable-next-line no-restricted-globals
7
+ return new URL(url, self.location.origin).href;
8
+ }
9
+
10
+ // removing submodules (matplotlib.pyplot = matplotlib)
11
+ const normalizeImportName = (name) => name.split(".")[0];
12
+
13
+ const WORKING_DIR = "/home/pyodide";
14
+
15
+ const toStructuredCloneable = (value) => {
16
+ if (value == null) return value;
17
+
18
+ if (
19
+ typeof value === "string" ||
20
+ typeof value === "number" ||
21
+ typeof value === "boolean"
22
+ ) {
23
+ return value;
24
+ }
25
+
26
+ if (value instanceof Uint8Array) {
27
+ return value.slice();
28
+ }
29
+
30
+ if (ArrayBuffer.isView(value)) {
31
+ return new Uint8Array(
32
+ value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength),
33
+ );
34
+ }
35
+
36
+ if (value instanceof ArrayBuffer) {
37
+ return value.slice(0);
38
+ }
39
+
40
+ if (value instanceof Map) {
41
+ return Object.fromEntries(
42
+ [...value.entries()].map(([k, v]) => [k, toStructuredCloneable(v)]),
43
+ );
44
+ }
45
+
46
+ if (Array.isArray(value)) {
47
+ return value.map(toStructuredCloneable);
48
+ }
49
+
50
+ if (typeof value?.toJs === "function") {
51
+ try {
52
+ const converted = value.toJs({ dict_converter: Object.fromEntries });
53
+ return toStructuredCloneable(converted);
54
+ } catch (_) {
55
+ try {
56
+ const converted = value.toJs();
57
+ return toStructuredCloneable(converted);
58
+ } catch (_) {
59
+ return String(value);
60
+ }
61
+ }
62
+ }
63
+
64
+ if (typeof value === "object") {
65
+ const result = {};
66
+ for (const [key, val] of Object.entries(value)) {
67
+ result[key] = toStructuredCloneable(val);
68
+ }
69
+ return result;
70
+ }
71
+
72
+ return String(value);
73
+ };
74
+
75
+ // Nest the PyodideWorker function inside a globalThis object so we control when its initialised.
76
+ const PyodideWorker = () => {
77
+ let assets;
78
+ let packageApiUrl;
79
+
80
+ const handleInit = (data) => {
81
+ assets = data.assets;
82
+ packageApiUrl = data.packageApiUrl;
83
+
84
+ // Import scripts dynamically based on the environment
85
+ console.log("PyodideWorker", "importing scripts");
86
+ importScripts(toAbsoluteFromOrigin(assets.pygalUrl));
87
+
88
+ assets.pyodideBaseUrl = `${packageApiUrl}/pyodide.js`;
89
+ importScripts(toAbsoluteFromOrigin(assets.pyodideBaseUrl));
90
+
91
+ initialisePyodide();
92
+ };
93
+
94
+ const supportsAllFeatures = typeof SharedArrayBuffer !== "undefined";
95
+
96
+ // eslint-disable-next-line no-restricted-globals
97
+ if (!supportsAllFeatures && name !== "incremental-features") {
98
+ console.warn(
99
+ [
100
+ "The code editor will not be able to capture standard input or stop execution because these HTTP headers are not set:",
101
+ " - Cross-Origin-Opener-Policy: same-origin",
102
+ " - Cross-Origin-Embedder-Policy: require-corp",
103
+ "",
104
+ "If your app can cope with or without these features, please initialize the web worker with { name: 'incremental-features' } to silence this warning.",
105
+ "You can then check for the presence of { stdinBuffer, interruptBuffer } in the handleLoaded message to check whether these features are supported.",
106
+ "",
107
+ "If you definitely need these features, either configure your server to respond with the HTTP headers above, or register a service worker.",
108
+ "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.",
109
+ "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.",
110
+ "",
111
+ "Please refer to these code snippets for registering a service worker:",
112
+ " - https://github.com/RaspberryPiFoundation/python-execution-prototypes/blob/fd2c50e032cba3bb0e92e19a88eb62e5b120fe7a/pyodide/index.html#L92-L98",
113
+ " - https://github.com/RaspberryPiFoundation/python-execution-prototypes/blob/fd2c50e032cba3bb0e92e19a88eb62e5b120fe7a/pyodide/serviceworker.js",
114
+ ].join("\n"),
115
+ );
116
+ }
117
+ let pyodide, pyodidePromise, stdinBuffer, interruptBuffer, stopped;
118
+ /** When true, input() uses postMessage + run_sync(getInputAsync) instead of setStdin (requires JSPI). */
119
+ let useMessageStdin = false;
120
+ /** Used when SharedArrayBuffer is unavailable: resolve for the pending input() call. */
121
+ let pendingStdinResolve = null;
122
+ // Until Pyodide is fully initialised, keep stdout/stderr in the dev console only.
123
+ // After initialisation, route them to the UI via postMessage.
124
+ let userStdStreamsEnabled = false;
125
+ // When true, suppress internal library / package loader logs from the UI console.
126
+ let suppressInternalStdStreams = false;
127
+
128
+ const onmessage = async ({ data }) => {
129
+ if (data.method !== "init") {
130
+ pyodide = await pyodidePromise;
131
+ }
132
+
133
+ switch (data.method) {
134
+ case "stdinResponse": {
135
+ if (pendingStdinResolve) {
136
+ let content = data.ctrlD ? null : data.content ?? "";
137
+ if (content && content.endsWith("\n")) {
138
+ content = content.slice(0, -1);
139
+ }
140
+ pendingStdinResolve(content);
141
+ pendingStdinResolve = null;
142
+ }
143
+ break;
144
+ }
145
+ case "init": {
146
+ handleInit(data);
147
+ break;
148
+ }
149
+ case "createDirectories": {
150
+ if (Array.isArray(data.dirs)) {
151
+ for (const dir of data.dirs) {
152
+ try {
153
+ pyodide.FS.mkdirTree(`${WORKING_DIR}/${dir}`);
154
+ } catch (e) {
155
+ console.error(`Failed to create directory ${dir}:`, e);
156
+ }
157
+ }
158
+ }
159
+ break;
160
+ }
161
+ case "writeFile": {
162
+ const encoder = new TextEncoder();
163
+ pyodide.FS.writeFile(
164
+ `${WORKING_DIR}/${data.filename}`,
165
+ encoder.encode(data.content),
166
+ );
167
+ break;
168
+ }
169
+ case "runPython":
170
+ runPython(data.python, data.userModuleNames);
171
+ break;
172
+ case "stopPython": {
173
+ // Mark as stopped so future checks can raise KeyboardInterrupt.
174
+ stopped = true;
175
+ // If Python is currently blocked in input() via run_sync(getInputAsync),
176
+ // resolve the pending stdin promise with EOF so execution can unwind
177
+ // and clearPyodideData can run.
178
+ if (pendingStdinResolve) {
179
+ pendingStdinResolve(null);
180
+ pendingStdinResolve = null;
181
+ }
182
+ break;
183
+ }
184
+ default: {
185
+ throw new Error(`Unsupported method: ${data.method}`);
186
+ }
187
+ }
188
+ };
189
+
190
+ // eslint-disable-next-line no-restricted-globals
191
+ addEventListener("message", async (event) => {
192
+ onmessage(event);
193
+ });
194
+
195
+ const runPython = async (python, userModuleNames) => {
196
+ stopped = false;
197
+
198
+ // When stdin uses postMessage (no SharedArrayBuffer), run_sync() in input() requires
199
+ // runPythonAsync so that JSPI stack switching can suspend until the main thread sends stdinResponse.
200
+ const runUserCode = useMessageStdin
201
+ ? () => pyodide.runPythonAsync(python)
202
+ : () => pyodide.runPython(python);
203
+
204
+ try {
205
+ await withSupportForPackages(python, async () => {
206
+ await runUserCode();
207
+ });
208
+ } catch (error) {
209
+ if (!(error instanceof pyodide.ffi.PythonError)) {
210
+ throw error;
211
+ }
212
+ const parsed = parsePythonError(error);
213
+ // Stop resolves stdin with EOF so input() raises EOFError; show as interrupt, not error.
214
+ if (stopped && parsed.type === "EOFError") {
215
+ postMessage({
216
+ method: "handleError",
217
+ ...parsed,
218
+ type: "KeyboardInterrupt",
219
+ info: "Execution interrupted",
220
+ });
221
+ } else {
222
+ postMessage({ method: "handleError", ...parsed });
223
+ }
224
+ }
225
+
226
+ await clearPyodideData(userModuleNames);
227
+ };
228
+
229
+ const checkIfStopped = () => {
230
+ if (stopped) {
231
+ throw new pyodide.ffi.PythonError("KeyboardInterrupt");
232
+ }
233
+ };
234
+
235
+ const withSupportForPackages = async (
236
+ python,
237
+ runPythonFn = async () => {},
238
+ ) => {
239
+ // Suppress internal loader output (e.g. "Loading pyodide-http") from the
240
+ // user console while resolving imports and loading packages.
241
+ suppressInternalStdStreams = true;
242
+ const imports = await pyodide._api.pyodide_code.find_imports(python).toJs();
243
+ await Promise.all(imports.map((name) => loadDependency(name)));
244
+
245
+ checkIfStopped();
246
+ await pyodide.loadPackagesFromImports(python);
247
+
248
+ checkIfStopped();
249
+ await pyodide.runPythonAsync(
250
+ `
251
+ import basthon
252
+ import builtins
253
+ import os
254
+
255
+ MAX_FILES = 100
256
+ MAX_FILE_SIZE = 8500000
257
+ PROJECT_ROOT = os.path.abspath("${WORKING_DIR}")
258
+
259
+ def _is_project_file(filename):
260
+ abs_path = os.path.abspath(filename)
261
+ return abs_path == PROJECT_ROOT or abs_path.startswith(PROJECT_ROOT + os.sep)
262
+
263
+ def _to_project_relative(filename):
264
+ abs_path = os.path.abspath(filename)
265
+ return os.path.relpath(abs_path, PROJECT_ROOT)
266
+
267
+ def _custom_open(filename, mode="r", *args, **kwargs):
268
+ abs_path = os.path.abspath(filename)
269
+
270
+ if "x" in mode and os.path.exists(abs_path):
271
+ raise FileExistsError(f"File '{filename}' already exists")
272
+
273
+ is_text_write = ("w" in mode or "a" in mode or "x" in mode) and "b" not in mode
274
+ is_project_file = _is_project_file(abs_path)
275
+
276
+ if is_text_write and is_project_file:
277
+ if len(os.listdir(PROJECT_ROOT)) > MAX_FILES and not os.path.exists(abs_path):
278
+ raise OSError(f"File system limit reached, no more than {MAX_FILES} files allowed")
279
+ class CustomFile:
280
+ def __init__(self, filename):
281
+ self.filename = filename
282
+ self.content = ""
283
+
284
+ def write(self, content):
285
+ self.content += content
286
+ if len(self.content) > MAX_FILE_SIZE:
287
+ raise OSError(f"File '{self.filename}' exceeds maximum file size of {MAX_FILE_SIZE} bytes")
288
+ with _original_open(self.filename, mode, *args, **kwargs) as f:
289
+ f.write(self.content)
290
+ basthon.kernel.write_file({
291
+ "filename": _to_project_relative(self.filename),
292
+ "content": self.content,
293
+ "mode": mode
294
+ })
295
+
296
+ def close(self):
297
+ pass
298
+
299
+ def __enter__(self):
300
+ return self
301
+
302
+ def __exit__(self, exc_type, exc_val, exc_tb):
303
+ self.close()
304
+
305
+ return CustomFile(abs_path)
306
+ else:
307
+ return _original_open(filename, mode, *args, **kwargs)
308
+
309
+ # Override the built-in open function
310
+ builtins.open = _custom_open
311
+ `,
312
+ { filename: "__custom_open__.py" },
313
+ );
314
+
315
+ // Re-enable user-visible stdout / stderr for the actual user code run.
316
+ suppressInternalStdStreams = false;
317
+
318
+ await runPythonFn();
319
+
320
+ for (let name of imports) {
321
+ checkIfStopped();
322
+ const pkgName = normalizeImportName(name);
323
+ await vendoredPackages[pkgName]?.after();
324
+ }
325
+ };
326
+
327
+ const loadDependency = async (name) => {
328
+ checkIfStopped();
329
+
330
+ const pkgName = normalizeImportName(name);
331
+ // If the import is for another user file then open it and load its dependencies.
332
+ if (pyodide.FS.readdir(WORKING_DIR).includes(`${pkgName}.py`)) {
333
+ const fileContent = pyodide.FS.readFile(`${WORKING_DIR}/${pkgName}.py`, {
334
+ encoding: "utf8",
335
+ });
336
+ await withSupportForPackages(fileContent);
337
+ return;
338
+ }
339
+
340
+ // If the import is for a vendored package then run its .before() hook.
341
+ const vendoredPackage = vendoredPackages[pkgName];
342
+ await vendoredPackage?.before();
343
+ if (vendoredPackage) {
344
+ return;
345
+ }
346
+
347
+ // If the import is for a module built into Python then do nothing.
348
+ try {
349
+ const pythonModule = pyodide.pyimport(pkgName);
350
+ if (pythonModule) {
351
+ return;
352
+ }
353
+ } catch (_) {}
354
+
355
+ // If the import is for a package built into Pyodide then load it.
356
+ // Built-ins: https://pyodide.org/en/stable/usage/packages-in-pyodide.html
357
+ try {
358
+ await pyodide.loadPackage(pkgName);
359
+
360
+ const pyodidePackage = pyodide.pyimport(pkgName);
361
+ if (pyodidePackage) {
362
+ return;
363
+ }
364
+ } catch (_) {}
365
+
366
+ // Ensure micropip is loaded which can fetch packages from PyPi.
367
+ // See: https://pyodide.org/en/stable/usage/loading-packages.html
368
+ if (!pyodide.micropip) {
369
+ await pyodide.loadPackage("micropip");
370
+ pyodide.micropip = pyodide.pyimport("micropip");
371
+ }
372
+
373
+ // If the import is for a PyPi package then load it.
374
+ // Otherwise, don't error now so that we get an error later from Python.
375
+ await pyodide.micropip.install(pkgName).catch(() => {});
376
+ };
377
+
378
+ const vendoredPackages = {
379
+ // Support for https://pypi.org/project/py-enigma/ due to package not having a whl file on PyPi.
380
+ enigma: {
381
+ before: async () => {
382
+ await pyodide.loadPackage(toAbsoluteFromOrigin(assets.enigmaWhlUrl));
383
+ },
384
+ after: () => {},
385
+ },
386
+ turtle: {
387
+ before: async () => {
388
+ pyodide.registerJsModule("basthon", fakeBasthonPackage);
389
+ await pyodide.loadPackage(toAbsoluteFromOrigin(assets.turtleWhlUrl));
390
+ },
391
+ after: () =>
392
+ pyodide.runPython(`
393
+ import turtle
394
+ import basthon
395
+
396
+ svg_dict = turtle.Screen().show_scene()
397
+ basthon.kernel.display_event({ "display_type": "turtle", "content": svg_dict })
398
+ turtle.restart()
399
+ `),
400
+ },
401
+ p5: {
402
+ before: async () => {
403
+ pyodide.registerJsModule("basthon", fakeBasthonPackage);
404
+ await pyodide.loadPackage([
405
+ "setuptools",
406
+ toAbsoluteFromOrigin(assets.p5WhlUrl),
407
+ ]);
408
+ },
409
+ after: () => {},
410
+ },
411
+ pygal: {
412
+ before: () => {
413
+ pyodide.registerJsModule("pygal", { ...pygal });
414
+ pygal.config.renderChart = (content) => {
415
+ postMessage({ method: "handleVisual", origin: "pygal", content });
416
+ };
417
+ },
418
+ after: () => {},
419
+ },
420
+ matplotlib: {
421
+ before: async () => {
422
+ pyodide.registerJsModule("basthon", fakeBasthonPackage);
423
+ // Patch the document object to prevent matplotlib from trying to render. Since we are running in a web worker,
424
+ // the document object is not available. We will instead capture the image and send it back to the main thread.
425
+ pyodide.runPython(`
426
+ import js
427
+
428
+ class __DummyDocument__:
429
+ def __init__(self, *args, **kwargs) -> None:
430
+ return
431
+ def __getattr__(self, __name: str):
432
+ return __DummyDocument__
433
+ js.document = __DummyDocument__()
434
+ `);
435
+ await pyodide.loadPackage("matplotlib")?.catch(() => {});
436
+ let pyodidePackage;
437
+ try {
438
+ pyodidePackage = pyodide.pyimport("matplotlib");
439
+ } catch (_) {}
440
+ if (pyodidePackage) {
441
+ pyodide.runPython(`
442
+ import matplotlib.pyplot as plt
443
+ import io
444
+ import basthon
445
+
446
+ def show_chart():
447
+ bytes_io = io.BytesIO()
448
+ plt.savefig(bytes_io, format='jpg')
449
+ bytes_io.seek(0)
450
+ basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
451
+ plt.show = show_chart
452
+ `);
453
+ return;
454
+ }
455
+ },
456
+ after: () => {
457
+ pyodide.runPython(`
458
+ import matplotlib.pyplot as plt
459
+ plt.clf()
460
+ `);
461
+ },
462
+ },
463
+ seaborn: {
464
+ before: async () => {
465
+ pyodide.registerJsModule("basthon", fakeBasthonPackage);
466
+ // Patch the document object to prevent matplotlib from trying to render. Since we are running in a web worker,
467
+ // the document object is not available. We will instead capture the image and send it back to the main thread.
468
+ pyodide.runPython(`
469
+ import js
470
+
471
+ class __DummyDocument__:
472
+ def __init__(self, *args, **kwargs) -> None:
473
+ return
474
+ def __getattr__(self, __name: str):
475
+ return __DummyDocument__
476
+ js.document = __DummyDocument__()
477
+ `);
478
+
479
+ // Ensure micropip is loaded which can fetch packages from PyPi.
480
+ // See: https://pyodide.org/en/stable/usage/loading-packages.html
481
+ if (!pyodide.micropip) {
482
+ await pyodide.loadPackage("micropip");
483
+ pyodide.micropip = pyodide.pyimport("micropip");
484
+ }
485
+
486
+ // If the import is for a PyPi package then load it.
487
+ // Otherwise, don't error now so that we get an error later from Python.
488
+ await pyodide.micropip.install("seaborn").catch(() => {});
489
+ },
490
+ after: () => {
491
+ pyodide.runPython(`
492
+ import matplotlib.pyplot as plt
493
+ import io
494
+ import basthon
495
+
496
+ def is_plot_empty():
497
+ fig = plt.gcf()
498
+ for ax in fig.get_axes():
499
+ # Check if the axes contain any lines, patches, collections, etc.
500
+ if ax.lines or ax.patches or ax.collections or ax.images or ax.texts:
501
+ return False
502
+ return True
503
+
504
+ if not is_plot_empty():
505
+ bytes_io = io.BytesIO()
506
+ plt.savefig(bytes_io, format='jpg')
507
+ bytes_io.seek(0)
508
+ basthon.kernel.display_event({ "display_type": "matplotlib", "content": bytes_io.read() })
509
+
510
+ plt.clf()
511
+ `);
512
+ },
513
+ },
514
+ plotly: {
515
+ before: async () => {
516
+ if (!pyodide.micropip) {
517
+ await pyodide.loadPackage("micropip");
518
+ pyodide.micropip = pyodide.pyimport("micropip");
519
+ }
520
+
521
+ // If the import is for a PyPi package then load it.
522
+ // Otherwise, don't error now so that we get an error later from Python.
523
+ await pyodide.micropip.install("plotly").catch(() => {});
524
+ await pyodide.micropip.install("pandas").catch(() => {});
525
+ pyodide.registerJsModule("basthon", fakeBasthonPackage);
526
+ pyodide.runPython(`
527
+ import plotly.graph_objs as go
528
+
529
+ def _hacked_show(self, *args, **kwargs):
530
+ basthon.kernel.display_event({
531
+ "display_type": "plotly",
532
+ "content": self.to_json()
533
+ })
534
+
535
+ go.Figure.show = _hacked_show
536
+ `);
537
+ },
538
+ after: () => {},
539
+ },
540
+ };
541
+
542
+ const fakeBasthonPackage = {
543
+ kernel: {
544
+ display_event: (event) => {
545
+ let payload;
546
+ try {
547
+ payload = event.toJs({ dict_converter: Object.fromEntries });
548
+ } catch (_) {
549
+ payload = event.toJs();
550
+ }
551
+
552
+ const origin = String(payload.display_type);
553
+ const content = toStructuredCloneable(payload.content);
554
+
555
+ postMessage({ method: "handleVisual", origin, content });
556
+ },
557
+ write_file: (event) => {
558
+ let payload;
559
+ try {
560
+ payload = event.toJs({ dict_converter: Object.fromEntries });
561
+ } catch (_) {
562
+ payload = event.toJs();
563
+ }
564
+
565
+ const filename = String(payload.filename);
566
+ const content =
567
+ typeof payload.content === "string"
568
+ ? payload.content
569
+ : toStructuredCloneable(payload.content);
570
+ const mode = String(payload.mode);
571
+
572
+ postMessage({ method: "handleFileWrite", filename, content, mode });
573
+ },
574
+ locals: () => pyodide.runPython("globals()"),
575
+ /**
576
+ * Returns a Promise that resolves with the next line of stdin when the main thread
577
+ * sends stdinResponse. Resolves with null on EOF (e.g. Ctrl+D).
578
+ * Used when SharedArrayBuffer is unavailable (no COOP/COEP) so we cannot block via Atomics.wait.
579
+ * Requires JSPI and runPythonAsync() for run_sync() to work.
580
+ */
581
+ getInputAsync: () => {
582
+ const promise = new Promise((resolve) => {
583
+ pendingStdinResolve = resolve;
584
+ });
585
+ postMessage({ method: "handleInput" });
586
+ return promise;
587
+ },
588
+ },
589
+ };
590
+
591
+ const clearPyodideData = async (userModuleNames) => {
592
+ postMessage({ method: "handleLoading" });
593
+ try {
594
+ await pyodide.runPythonAsync(`
595
+ # Clear all user-defined variables and modules
596
+ for name in list(globals()):
597
+ if not name.startswith('_') and not name=='basthon':
598
+ del globals()[name]
599
+
600
+ import sys
601
+ # Remove user modules from sys.modules
602
+ user_modules = ${JSON.stringify(userModuleNames)}
603
+ for name in user_modules:
604
+ if name in sys.modules:
605
+ del sys.modules[name]
606
+ `);
607
+ } catch (error) {
608
+ console.error("Error while clearing Pyodide data:", error);
609
+ }
610
+ console.log("clearPyodideData done");
611
+ postMessage({ method: "handleLoaded", stdinBuffer, interruptBuffer });
612
+ };
613
+
614
+ const initialisePyodide = async () => {
615
+ postMessage({ method: "handleLoading" });
616
+
617
+ pyodidePromise = loadPyodide({
618
+ stdout: (content) => {
619
+ if (stopped) return;
620
+ if (userStdStreamsEnabled && !suppressInternalStdStreams) {
621
+ postMessage({ method: "handleOutput", stream: "stdout", content });
622
+ } else {
623
+ console.log(content);
624
+ }
625
+ },
626
+ stderr: (content) => {
627
+ if (stopped) return;
628
+ if (userStdStreamsEnabled && !suppressInternalStdStreams) {
629
+ postMessage({ method: "handleOutput", stream: "stderr", content });
630
+ } else {
631
+ console.error(content);
632
+ }
633
+ },
634
+ });
635
+
636
+ pyodide = await pyodidePromise;
637
+
638
+ pyodide.registerJsModule("basthon", fakeBasthonPackage);
639
+
640
+ // When SharedArrayBuffer is unavailable, always use the postMessage-based
641
+ // stdin path. JSPI / run_sync will raise at runtime if the environment
642
+ // cannot stack-switch, but in JSPI-capable browsers this enables
643
+ // interactive input() without COOP/COEP.
644
+ useMessageStdin = !supportsAllFeatures;
645
+ pyodide.globals.set("__stdin_via_message__", useMessageStdin);
646
+
647
+ await pyodide.runPythonAsync(`
648
+ import builtins
649
+ from pyodide.ffi import run_sync as __run_sync__
650
+
651
+ __old_input__ = builtins.input
652
+ def __patched_input__(prompt=None):
653
+ if prompt is not None:
654
+ print(prompt)
655
+ if __stdin_via_message__:
656
+ result = __run_sync__(basthon.kernel.getInputAsync())
657
+ if result is None:
658
+ raise EOFError("EOF when reading a line")
659
+ return result
660
+ return __old_input__()
661
+ builtins.input = __patched_input__
662
+ `);
663
+
664
+ await pyodide.runPythonAsync(`
665
+ import builtins
666
+ # Save the original open function
667
+ _original_open = builtins.open
668
+ `);
669
+
670
+ await pyodide.loadPackage("pyodide-http");
671
+ await pyodide.runPythonAsync(`
672
+ import pyodide_http
673
+ pyodide_http.patch_all()
674
+ `);
675
+
676
+ if (supportsAllFeatures) {
677
+ stdinBuffer =
678
+ stdinBuffer || new Int32Array(new SharedArrayBuffer(1024 * 1024)); // 1 MiB
679
+ stdinBuffer[0] = 1; // Store the length of content in the buffer at index 0.
680
+ pyodide.setStdin({ isatty: true, read: readFromStdin });
681
+
682
+ interruptBuffer =
683
+ interruptBuffer || new Uint8Array(new SharedArrayBuffer(1));
684
+ pyodide.setInterruptBuffer(interruptBuffer);
685
+ }
686
+
687
+ // From this point on, anything written to stdout / stderr is considered
688
+ // user-visible and will be forwarded to the UI.
689
+ userStdStreamsEnabled = true;
690
+
691
+ postMessage({ method: "handleLoaded", stdinBuffer, interruptBuffer });
692
+ };
693
+
694
+ const readFromStdin = (bufferToWrite) => {
695
+ const previousLength = stdinBuffer[0];
696
+ postMessage({ method: "handleInput" });
697
+
698
+ while (true) {
699
+ pyodide.checkInterrupt();
700
+ const result = Atomics.wait(stdinBuffer, 0, previousLength, 100);
701
+ if (result === "not-equal") {
702
+ break;
703
+ }
704
+ }
705
+
706
+ const currentLength = stdinBuffer[0];
707
+ if (currentLength === -1) {
708
+ return 0;
709
+ } // Signals that stdin was closed.
710
+
711
+ const addedBytes = stdinBuffer.slice(previousLength, currentLength);
712
+ bufferToWrite.set(addedBytes);
713
+
714
+ return addedBytes.length;
715
+ };
716
+
717
+ const parsePythonError = (error) => {
718
+ const type = error.type;
719
+ const [trace, info] = error.message.split(`${type}:`).map((s) => s?.trim());
720
+
721
+ const lines = trace.split("\n");
722
+
723
+ // if the third from last line matches /File "__custom_open__\.py", line (\d+)/g then strip off the last three lines
724
+ if (
725
+ lines.length > 3 &&
726
+ /File "__custom_open__\.py", line (\d+)/g.test(lines[lines.length - 3])
727
+ ) {
728
+ lines.splice(-3, 3);
729
+ }
730
+
731
+ const snippetLine = lines[lines.length - 2]; // print("hi")invalid
732
+ const caretLine = lines[lines.length - 1]; // ^^^^^^^
733
+
734
+ const showsMistake = caretLine.includes("^");
735
+ const mistake = showsMistake
736
+ ? [snippetLine.slice(4), caretLine.slice(4)].join("\n")
737
+ : "";
738
+
739
+ const matches = [
740
+ ...trace.matchAll(/File "(?!__custom_open__\.py)(.*)", line (\d+)/g),
741
+ ];
742
+ const match = matches[matches.length - 1];
743
+
744
+ const path = match ? match[1] : "";
745
+ const base = path.split("/").reverse()[0];
746
+ const file = base === "<exec>" ? "main.py" : base;
747
+
748
+ const line = match ? parseInt(match[2], 10) : "";
749
+
750
+ return { file, line, mistake, type, info };
751
+ };
752
+ };
753
+
754
+ globalThis.PyodideWorker = PyodideWorker;
755
+ PyodideWorker();