// We have to specify pyodide.js because we need to import that file (not .mjs) // and 'import' defaults to .mjs import { loadPyodide, type PyodideInterface } from 'pyodide/pyodide.js'; import pkg from 'pyodide/package.json'; const ctx: Worker & typeof globalThis = self as unknown as Worker & typeof globalThis; let pyodide: PyodideInterface; interface PythonRunEvent extends MessageEvent { data: { code: { contents: string; editableContents: string; original: { [id: string]: string }; }; }; } async function setupPyodide() { if (pyodide) return pyodide; pyodide = await loadPyodide({ // TODO: host this ourselves indexURL: `https://cdn.jsdelivr.net/pyodide/v${pkg.version}/full/` }); // We freeze this to prevent learners from getting the worker into a // weird state. NOTE: this has to come after pyodide is loaded, because // pyodide modifies self while loading. Object.freeze(self); // eslint-disable-next-line @typescript-eslint/no-unsafe-call const str = pyodide.globals.get('str') as (x: unknown) => string; function print(...args: unknown[]) { const text = args.map(x => str(x)).join(' '); postMessage({ type: 'print', text }); } function input(text: string) { // TODO: send unique ids to the main thread and the service worker, so we // can have multiple concurrent input requests. postMessage({ type: 'input', text }); const request = new XMLHttpRequest(); request.open('POST', '/python/intercept-input/', false); request.send(null); return request.responseText; } // I tried setting jsglobals here, to provide 'input' and 'print' to python, // without having to modify the global window object. However, it didn't work // because pyodide needs access to that object. Instead, I used // registerJsModule when setting up runPython. // Make print available to python pyodide.registerJsModule('jscustom', { print, input }); // TODO: use a fresh global object for each runPython call if we stop terminating // the worker when the user input changes. (See python-test-evaluator.ts) pyodide.runPython(` import jscustom from jscustom import print from jscustom import input `); return pyodide; } void setupPyodide(); ctx.onmessage = async (e: PythonRunEvent) => { const code = (e.data.code.contents || '').slice(); const pyodide = await setupPyodide(); // use pyodide.runPythonAsync if we want top-level await pyodide.runPython(code); };