Helpers for running Python code in a web worker with Pyodide.
Use npm or yarn to install the pyodide-worker-runner package as well as these peer dependencies:
pyodidecomsyncsync-messagecomlink
The function loadPyodideAndPackage loads Pyodide in parallel to downloading an archive with your own code and dependencies, which it then unpacks into the virtual filesystem ready for you to import. This helps you to work with Python code normally with several .py files instead of a giant string passed to Pyodide's runPython. It also lets you start up more quickly instead of waiting for Pyodide to load before calling loadPackage or micropip.install.
There are two arguments:
- Options for loading the package, an object with the following keys:
url(required): a string passed tofetchto download the archive file.format(required) andextractDir(optional): strings passed topyodide.unpackArchive.
- An optional function which takes no arguments and returns the Pyodide module as returned by the
loadPyodidefunction. By default it uses the official CDN.
The archive should contain your own Python files and any Python dependencies. A simple way to gather Python dependencies into a folder is with pip install -t <folder>. The location where the archive is extracted will be added to sys.path so it can be imported immediately, e.g. with pyodide.pyimport. There should be no top-level folder in the archive containing everything else, or that's what you'll have to import.
If you don't use loadPyodideAndPackage and just load Pyodide yourself, then we recommend passing the resulting module object to initPyodide for some other housekeeping.
Loading of both Pyodide and the package is retried up to 3 times in case of network errors.
Sometimes Pyodide encounters a fatal error from which it cannot recover, after which the module cannot be reused.
To deal with this, this package provides a class PyodideFatalErrorReloader. The constructor accepts a 'loader' function which should return a promise that resolves to a Pyodide module. We recommend a function which calls loadPyodideAndPackage. Then code that uses the Pyodide module should be wrapped in a withPyodide call. Here's an example:
import {loadPyodideAndPackage, PyodideFatalErrorReloader} from "pyodide-worker-runner";
const reloader = new PyodideFatalErrorReloader(() => loadPyodideAndPackage({ url: "package.tar.gz" }));
await reloader.withPyodide(async (pyodide) => {
pyodide.runCode(...);
});If a fatal error occurs, the loader function will be called again immediately to reload Pyodide in the background, while the error is rethrown for you to handle. The next call to withPyodide will then be able to use the new Pyodide instance.
This library builds on comsync to help with interrupting running code and synchronously sleeping and reading from stdin.
In the main thread, construct a PyodideClient instead of a comsync.SyncClient. If SharedArrayBuffer is available (see the guide to enabling cross-origin isolation) then it will create a buffer which can ultimately be passed to pyodide.setInterruptBuffer in the worker, and set an interrupter function on the client. Then calling PyodideClient.interrupt() (see the comsync documentation) may use that which will raise a KeyboardInterrupt in Python.
In the worker, call pyodideExpose(func) where func is a function which will be passed to comsync.syncExpose. The first argument passed to this function will be a SyncExtras object with one extra property interruptBuffer which can be passed to pyodide.setInterruptBuffer. The other arguments will be the arguments passed to PyodideClient.call. Here's what this may look like in the worker:
import {pyodideExpose} from "pyodide-worker-runner";
import * as Comlink from "comlink";
Comlink.expose({
runCode: pyodideExpose((extras, code) => {
if (extras.interruptBuffer) { // i.e. if SharedArrayBuffer is available so this could be sent by the client
pyodide.setInterruptBuffer(extras.interruptBuffer);
}
pyodide.runCode(code);
},
),
});The comsync integration is best used in combination with the python_runner Python library so that you don't have to call the methods on SyncExtras yourself.
- Make sure
python_runneris installed within Pyodide, ideally in advance by including it in the archive loaded byloadPyodideAndPackage. - Use the
python_runner.PyodideRunnerclass, which has patches forbuiltins.input,sys.stdin, andtime.sleepspecifically for use with this library andcomsync. This will handle blocking synchronously, reading input, and raisingKeyboardInterruptwhen reading/sleeping is interrupted from the main thread without relying onpyodide.setInterruptBuffer. - Call the
makeRunnerCallback(syncExtras, callbacks)function from this library.callbacksshould be an object containing callback functions to handle the different event types:output: Required. Called with an array of output parts, e.g.[{type: "stdout", text: "Hello world"}]. Use this to tell your UI to display the output.input: Optional. Called when the Python code reads fromsys.stdin, e.g. withinput(). Use this to tell your UI to wait for the user to enter some text. The entered text should be passed toPyodideClient.writeMessage()in the main thread, and will be returned synchronously by this function to the Python code. When the Python code callsinput(prompt), the stringpromptis passed to this callback. Two types of output part will also be passed to theoutputcallback:input_prompt: the prompt passed to theinput()function. Using this output part may be a better way to display the prompt in the UI rather than using the argument of theinputcallback, but theinputcallback is still needed even if it doesn't display the prompt.input: the user's input passed to stdin. Not actually 'output', but included as an output part because it's typically shown in regular Python consoles.
other: Optional. Called for all other event types (exceptsleepwhich is handled directly bymakeRunnerCallback). Receives the same two arguments (event type and data) that are passed torunner.callback()in Python.
makeRunnerCallbackreturns a single callback function which can be passed torunner.set_callback.
Pyodide provides loadPackagesFromImports to automatically call loadPackage for any imported libraries detected in the given Python code. However this only applies to packages specifically supported by pyodide.loadPackage. You can use the similar function install_imports to try to install arbitrary packages from PyPI with micropip.install, although the usual caveats still apply. You can import it from the pyodide_worker_runner Python module which is automatically installed by loadPyodideAndPackage or initPyodide. To use it from JS:
await pyodide.pyimport("pyodide_worker_runner").install_imports(python_source_code_string);The first argument is a string of Python source code or a list of module names being imported.
You can also provide an optional message_callback argument is provided to get info about packages as they load.
See the docstring for more details.
