Plugins
PyScript offers a plugin API so anyone can extend its functionality and share their modifications.
PyScript only supports plugins written in Javascript (although causing the
evaluation of bespoke Python code can be a part of such plugins). The plugin's
JavaScript code should be included on the web page via a
<script type="module">
tag before PyScript is included in the web page.
The PyScript plugin API defines hooks that intercept named events in the lifecycle of a PyScript application, so you can modify how the platform behaves. The example at the bottom of this page demonstrates how this is done in code.
Lifecycle Events
PyScript emits events that capture the beginning or the end of specific stages in the application's life. No matter if code is running in the main thread or on a web worker, the exact same sequence of steps takes place:
- ready - the browser recognizes the PyScript
script
or tag, and the associated Python interpreter is ready to work. A JavaScript callback can be used to instrument the interpreter before anything else happens. - before run - some Python code is about to be evaluated. A JavaScript callback could setup a bespoke browser context, be that in the main thread or a web worker. This is similar to a generic setup callback found in test frameworks.
- code before run - sometimes a plugin needs Python code to be evaluated before the code given to PyScript, in order to set up a Python context for the referenced code. Such code is simply a string of Python evaluated immediately before the code given to PyScript.
- At this point in the lifecycle the code referenced in the
script
or other PyScript related tags is evaluated. - code after run - sometimes a plugin needs Python code to be evaluated after the code given to PyScript, in order to clean up or react to the final state created by the referenced code. Such code is simply a string of Python code evaluated immediately after the code given to PyScript.
- after run - all the Python code has been evaluated. A JavaScript callback could clean up or react to the resulting browser context, be that on the main thread or a web worker. This is similar to a generic teardown callback found in test frameworks.
PyScript's interpreters can run their code either synchronously or asynchronously (note, the default is asynchronously). No matter, the very same sequence is guaranteed to run in order in both cases, the only difference being the naming convention used to reference synchronous or asynchronous lifecycle hooks.
Hooks
Hooks are used to tell PyScript that your code wants to subscribe to specific lifecycle events. When such events happen, your code will be called by PyScript.
The available hooks depend upon where your code is running: on the browser's
(blocking) main thread or on a (non-blocking) web worker. These are defined via
the hooks.main
and hooks.worker
namespaces in JavaScript.
Main Hooks
Callbacks registered via hooks on the main thread will usually receive a wrapper of the interpreter with its unique-to-that-interpreter utilities, along with a reference to the element on the page from where the code was derived.
Please refer to the documentation for the interpreter you're using (Pyodide or MicroPython) to learn about its implementation details and the potential capabilities made available via the wrapper.
Callbacks whose purpose is simply to run raw contextual Python code, don't receive interpreter or element arguments.
This table lists all possible, yet optional, hooks a plugin can register on the main thread:
name | example | behavior |
---|---|---|
onReady | onReady(wrap:Wrap, el:Element) {} |
If defined, this hook is invoked before any other to signal that the interpreter is ready and code is going to be evaluated. This hook is in charge of eventually running Pythonic content in any way it is defined to do so. |
onBeforeRun | onBeforeRun(wrap:Wrap, el:Element) {} |
If defined, it is invoked before to signal that an element's code is going to be evaluated. |
onBeforeRunAsync | onBeforeRunAsync(wrap:Wrap, el:Element) {} |
Same as onBeforeRun , except for scripts that require async behaviour. |
codeBeforeRun | codeBeforeRun: () => 'print("before")' |
If defined, evaluate some Python code immediately before the element's code is evaluated. |
codeBeforeRunAsync | codeBeforeRunAsync: () => 'print("before")' |
Same as codeBeforeRun, except for scripts that require async behaviour. |
codeAfterRun | codeAfterRun: () => 'print("after")' |
If defined, evaluate come Python code immediately after the element's code has finished executing. |
codeAfterRunAsync | codeAfterRunAsync: () => 'print("after")' |
Same as codeAfterRun , except for scripts that require async behaviour. |
onAfterRun | onAfterRun(wrap:Wrap, el:Element) {} |
If defined, invoked immediately after the element's code has been executed. |
onAfterRunAsync | onAfterRunAsync(wrap:Wrap, el:Element) {} |
Same as onAfterRun , except for scripts that require async behaviour. |
onWorker | onWorker(wrap = null, xworker) {} |
If defined, whenever a script or tag with a worker attribute is processed, this hook is triggered on the main thread. This allows possible xworker features to be exposed before the code is evaluated on the web worker. The wrap reference is usually null unless an explicit XWorker call has been initialized manually and there is an interpreter on the main thread (very advanced use case). Please note, this is the only hook that doesn't exist in the worker counter list of hooks. |
Worker Hooks
When it comes to hooks on a web worker, callbacks cannot use any JavaScript outer scope references and must be completely self contained. This is because callbacks must be serializable in order to work on web workers since they are actually communicated as strings via a postMessage to the worker's isolated environment.
For example, this will work because all references are contained within the registered function:
import { hooks } from "https://pyscript.net/releases/2024.9.2/core.js";
hooks.worker.onReady.add(() => {
// NOT suggested, just an example!
// This code simply defines a global `i` reference if it doesn't exist.
if (!('i' in globalThis))
globalThis.i = 0;
console.log(++i);
});
However, due to the outer reference to the variable i
, this will fail:
import { hooks } from "https://pyscript.net/releases/2024.9.2/core.js";
// NO NO NO NO NO! ☠️
let i = 0;
hooks.worker.onReady.add(() => {
// The `i` in the outer-scope will cause an error if
// this code executes in the worker because only the
// body of this function gets stringified and re-evaluated
console.log(++i);
});
As the worker won't have an element
related to it, since workers can be
created procedurally, the second argument won't be a reference to an element
but a reference to the
related xworker
object
that is driving and coordinating things.
The list of possible yet optional hooks a custom plugin can use with a
web worker is exactly
the same as for the main thread
except for the absence of onWorker
and the replacement of the second
element
argument with that of an xworker
.
Example plugin
Here's a contrived example, written in JavaScript, that should be added to the
web page via a <script type="module">
tag before PyScript is included in
the page.
// import the hooks from PyScript first...
import { hooks } from "https://pyscript.net/releases/2024.9.2/core.js";
// The `hooks.main` attribute defines plugins that run on the main thread.
hooks.main.onReady.add((wrap, element) => {
console.log("main", "onReady");
if (location.search === '?debug') {
console.debug("main", "wrap", wrap);
console.debug("main", "element", element);
}
});
hooks.main.onBeforeRun.add(() => {
console.log("main", "onBeforeRun");
});
hooks.main.codeBeforeRun.add('print("main", "codeBeforeRun")');
hooks.main.codeAfterRun.add('print("main", "codeAfterRun")');
hooks.main.onAfterRun.add(() => {
console.log("main", "onAfterRun");
});
// The `hooks.worker` attribute defines plugins that run on workers.
hooks.worker.onReady.add((wrap, xworker) => {
console.log("worker", "onReady");
if (location.search === '?debug') {
console.debug("worker", "wrap", wrap);
console.debug("worker", "xworker", xworker);
}
});
hooks.worker.onBeforeRun.add(() => {
console.log("worker", "onBeforeRun");
});
hooks.worker.codeBeforeRun.add('print("worker", "codeBeforeRun")');
hooks.worker.codeAfterRun.add('print("worker", "codeAfterRun")');
hooks.worker.onAfterRun.add(() => {
console.log("worker", "onAfterRun");
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- JS plugins should be available before PyScript bootstraps -->
<script type="module" src="./log.js"></script>
<!-- PyScript -->
<link rel="stylesheet" href="https://pyscript.net/releases/2024.9.2/core.css">
<script type="module" src="https://pyscript.net/releases/2024.9.2/core.js"></script>
</head>
<body>
<script type="mpy">
print("ACTUAL CODE")
</script>
</body>
</html>