pyodide-code
```py live_pyo id=b26cde2c-5b0b-4acb-814e-9f2b3bdd29a9
print("Hello from Pyodide!")
```
print("Hello from Pyodide!")
Sleep
from time import sleep
for i in range(10):
print(i, i*i, i**3)
sleep(1)
Ausgabe löschen
os.system('clear') und os.system('cls') löschen die bisherige Ausgabe des aktuellen Editors. Nachfolgende Ausgaben erscheinen wieder im leeren Logfenster. Beide Befehle geben 0 zurück. Diese Unterstützung gilt für die beiden Löschbefehle; eine allgemeine Betriebssystem-Shell steht im Browser nicht zur Verfügung.
from os import system
from time import sleep
for i in range(5):
system('clear')
print(i)
sleep(1)
Es ist immer nur die aktuelle Ausgabe sichtbar.
Fibonacci
def fibonacci(n):
if n <= 0:
return []
elif n == 1:
return [0]
elif n == 2:
return [0, 1]
else:
seq = [0, 1]
for i in range(2, n):
seq.append(seq[-1] + seq[-2])
return seq
n = int(input("fib von? "))
print(fibonacci(n))
Mit Memoization
from functools import lru_cache
@lru_cache
def fibonacci(n):
if n in {0, 1}:
return n
return fibonacci(n-1) + fibonacci(n-2)
for n in range(1, 200):
print(f"Fibonacci({n}) = {fibonacci(n)}")
Minimierung mit SciPy
import numpy as np
from scipy import optimize
# Function to minimize: f(x) = (x - 3)^2
def func(x):
return (x - 3) ** 2
# Minimize using scipy.optimize
result = optimize.minimize(func, x0=0)
print("Minimum at x =", result.x[0])
print("Function value at minimum =", result.fun)
Python-Pakete laden
Imports werden mit Pythons AST ausgewertet, auch innerhalb von Funktionen oder zusammen mit weiteren Anweisungen auf einer Zeile. Pakete aus der Pyodide-Distribution werden mit deren eingebautem Loader geladen. Bereits verfügbare Module, die Standardbibliothek und lokale JavaScript-Module lösen keine zusätzlichen PyPI-Installationen aus. Fehlgeschlagene Installationen werden im Editor angezeigt.
Für zusätzliche oder versionierte Abhängigkeiten kann die Pyodide-Komponente eine Liste von Paketanforderungen erhalten:
import Pyodide from '@tdev/packages/pyodide-code/components/Pyodide';
export const code = `from slugify import slugify\n
print(slugify("C'est déjà l'été.", algorithm='modern'))
print(slugify('影師嗎', backend='text-unidecode', algorithm='modern'))`;
<Pyodide showLineNumbers code={code} packages={['python-slugify==9.1.3']} id="32553b77-e5eb-4736-aa35-9fdaa7ce8b5d" />
from slugify import slugify
print(slugify("C'est déjà l'été.", algorithm='modern'))
print(slugify('影師嗎', backend='text-unidecode', algorithm='modern'))
Diese Pakete werden vor der Ausführung installiert. So funktioniert auch import slugify, obwohl die Distribution python-slugify heisst. Die Liste steht ebenfalls als packages in den ModelMeta-Optionen zur Verfügung. Dynamisch zusammengesetzte Importnamen benötigen solche expliziten Paketanforderungen.
Explizite Paketanforderungen müssen erfolgreich installiert werden. Bei fehlgeschlagenen automatisch erkannten Abhängigkeiten wird eine Diagnose angezeigt und der Python-Code trotzdem ausgeführt. Dadurch können optionale Imports weiterhin mit try/except ImportError behandelt werden.
Fortschrittsmeldungen beim Laden von Paketen erscheinen ausschliesslich in der Entwicklerkonsole des Browsers. Fehlgeschlagene Installationen werden weiterhin im Editor angezeigt.
Freie Worker wiederverwenden
Nacheinander gestartete Skripte verwenden einen freien Worker desselben Browser-Tabs wieder. Dadurch bleiben bereits installierte Pakete und importierte Bibliotheken verfügbar. Gleichzeitig laufende Skripte erhalten jeweils einen eigenen Worker. Pro Tab bleiben höchstens zwei freie Worker erhalten; verschiedene Tabs teilen keinen Python-Interpreter.
Python-Fehler behalten den geladenen Worker beim jeweiligen Editor. Beim nächsten Start wird derselbe Worker wiederverwendet.
Abbrechen beendet den Worker sofort und initialisiert einen Ersatz. Das gilt auch beim Abbrechen einer Eingabe und stoppt Endlosschleifen zuverlässig. Andere gleichzeitig laufende Skripte behalten ihre Worker. Das abgebrochene Skript wird nicht automatisch erneut ausgeführt; seine Pakete müssen beim nächsten Start erneut geladen werden.
Skriptvariablen werden bei jeder Ausführung neu angelegt. Importierte Module, Dateien, das Arbeitsverzeichnis und anderer Interpreterzustand können zwischen Editoren erhalten bleiben. Worker mit noch laufenden asyncio-Tasks bleiben ebenfalls beim jeweiligen Editor. Erweiterungen müssen geplante asyncio-Callbacks und JavaScript-Timer vor Ende des Skripts selbst beenden; solche Hintergrundaktivitäten werden durch die Task-Prüfung nicht erkannt. Ein ausgefallener Worker wird beim nächsten Start eines Skripts ersetzt.
Interaktive Komponenten hinzufügen
Das pyodide-code-Package kann erweitert werden - sowohl mit neuen, lokalen Pythin-Bibliotheken als auch mit interaktiven Komponenten, die in React geschrieben sind und mit Python kommunizieren können. Ein Beispiel dafür ist die interaktive Uhr-Komponente:
Interaktive Uhr
import Clock from '@tdev/packages/pyodide-code/components/Clock';
<Clock clockId="sbb-uhr" />
```py live_pyo id=e63340b1-3821-4b70-99af-602272a48d8d title=Interaktive_Uhr
from clock import use_clock
from time import sleep
uhr = use_clock("sbb-uhr")
while True:
uhr.set_seconds(uhr.seconds + 1)
if uhr.seconds % 360 == 0:
sleep(0.99)
uhr.set_minutes(uhr.minutes + 6)
if uhr.minutes % 360 == 0:
uhr.set_hours(uhr.hours + 30)
sleep(0.01)
```
Uhr: sbb-uhr
from clock import use_clock
from time import sleep
uhr = use_clock("sbb-uhr")
while True:
uhr.set_seconds(uhr.seconds + 1)
if uhr.seconds % 360 == 0:
sleep(0.99)
uhr.set_minutes(uhr.minutes + 6)
if uhr.minutes % 360 == 0:
uhr.set_hours(uhr.hours + 30)
sleep(0.01)
Erweiterungen registrieren
Eine Erweiterung hat zwei getrennte Einstiegspunkte: worker.ts stellt Python-Module bereit, client.ts verarbeitet deren Nachrichten im Browser. Beide verwenden denselben Payload-Typ aus protocol.ts. React-Komponenten und MobX-Stores bleiben normale Bestandteile der Website.
Die Einstiegspunkte werden explizit im bestehenden Pyodide-Plugin konfiguriert:
import type { PluginOptions } from '@tdev/pyodide-code/plugin';
plugins: [
[
require.resolve('@tdev/pyodide-code/plugin'),
{
extensions: [
{
name: 'clock',
worker: require.resolve('./website/pyodide/clock/worker'),
client: require.resolve('./website/pyodide/clock/client')
}
]
} satisfies PluginOptions
]
]
Die Namen der Erweiterungen müssen eindeutig sein. require.resolve() liefert absolute Dateipfade; relative Einstiegspfade in den Optionen werden relativ zum Docusaurus-Seitenverzeichnis aufgelöst. Die Konfiguration enthält nur Dateipfade und Namen, keine React-Komponenten oder Store-Instanzen. Nach Änderungen an der Erweiterungsliste muss der Entwicklungsserver neu gestartet werden.
Das Plugin erzeugt getrennte Registries für Worker und Browser. Es braucht weder ein Alias-Override von Package-Dateien noch eine globale Registrierung beim App-Start. worker.ts darf keine React-, DOM- oder Store-Abhängigkeiten importieren; gemeinsame Payload-Typen werden mit import type eingebunden.
Beim Laden der jeweiligen Registry wird einmalig geprüft, ob die Worker-Einträge createModules() und die Browser-Einträge handleMessage() bereitstellen. Ein ungültiger Export verhindert die Initialisierung dieser Registry; die Fehlermeldung nennt die Erweiterung und den betroffenen Einstiegspunkt.
Uhr: gemeinsamer Vertrag
export interface ClockTime {
hours: number;
minutes: number;
seconds: number;
}
export type ClockUpdate = Partial<ClockTime>;
Alle Winkel sind in Grad angegeben. Eine Nachricht kann einzelne Zeiger oder die ganze Uhr ändern. Die beiden Einstiegspunkte verwenden ClockUpdate als Typparameter. Die Typen prüfen den selbst geschriebenen Worker und Handler, es gibt keine automatische Laufzeitvalidierung des Payloads.
Uhr: Python-Modul im Worker
import { defineWorkerExtension } from '@tdev/pyodide-code/extensions';
import type { ClockTime, ClockUpdate } from './protocol';
export default defineWorkerExtension<ClockUpdate>((context) => ({
clock: {
use_clock: (id: string) => {
const time: ClockTime = { hours: 0, minutes: 0, seconds: 0 };
const update = (values: Partial<ClockTime>) => {
Object.assign(time, values);
context.send(id, values);
};
return {
get hours() {
return time.hours;
},
get minutes() {
return time.minutes;
},
get seconds() {
return time.seconds;
},
reset: () => update({ hours: 0, minutes: 0, seconds: 0 }),
set_time: (hours: number, minutes: number, seconds: number) =>
update({ hours, minutes, seconds }),
set_hours: (hours: number) => update({ hours }),
set_minutes: (minutes: number) => update({ minutes }),
set_seconds: (seconds: number) => update({ seconds })
};
}
}
}));
defineWorkerExtension<Payload>() erhält eine Factory. Diese liefert ein Objekt, dessen Schlüssel die Python-Modulnamen sind, hier clock. Eine Erweiterung kann mehrere Module bereitstellen; Modulnamen und Nachrichtentypen sind unabhängig voneinander. Doppelte Modulnamen und der interne Name browser_input werden mit einer Fehlermeldung abgelehnt.
Der Factory-Kontext bietet:
editorId: die ID des ausführenden Editors.send(targetId, payload): sendet eine Nachricht an den Browser-Handler dieser Erweiterung. Das Framework ergänzt Editor-ID, Erweiterungsname und Zeitstempel. Der Payload muss durch strukturiertes Klonen übertragbar sein, beispielsweise ein Objekt aus Zahlen, Strings und Arrays.
Die Factory wird bei jeder Ausführung aufgerufen und ihre Module werden vor dem Python-Code registriert. Damit funktionieren auch dynamische Imports wie importlib.import_module('clock'). Der Worker wartet vor Abschluss auf ausstehende Nachrichten an die Browser-Handler. Ein Handler-Fehler wird als Ausführungsfehler im Editor angezeigt. Bereits zugestellte Änderungen werden nicht zurückgerollt.
Schlagen sowohl die Ausführung als auch deren Abschluss fehl, erscheint der ursprüngliche Ausführungsfehler zuerst. Zusätzliche Fehler beim Zustellen von Nachrichten oder beim Aufräumen werden darunter angezeigt. Die weiteren Aufräumschritte werden auch nach einem Fehler ausgeführt.
Uhr: Browser-Handler und React
import { defineClientExtension } from '@tdev/pyodide-code/extensions';
import type { ClockUpdate } from './protocol';
export default defineClientExtension<ClockUpdate>(({ root }, { targetId, payload }) => {
const clock = root.siteStore.clockStore.useClock(targetId);
if (payload.hours !== undefined) {
clock.setHours(payload.hours);
}
if (payload.minutes !== undefined) {
clock.setMinutes(payload.minutes);
}
if (payload.seconds !== undefined) {
clock.setSeconds(payload.seconds);
}
});
defineClientExtension<Payload>() erhält einen synchronen Handler. Der Kontext enthält den aktuellen root-Store. Die Nachricht enthält editorId, targetId, timeStamp und payload. Der Handler läuft innerhalb einer MobX-Action; er aktualisiert die bestehenden Website-Stores.
Die Uhr verwendet weiterhin den 👉 ClockStore, das 👉 Clock-Modell und die 👉 React-Komponente. Nur der Store muss im SiteStore vorhanden sein; ein handleMessage-Override ist nicht nötig. Die Komponente kann wie bisher unabhängig vom Editor im MDX platziert werden:
import Clock from '@tdev/packages/pyodide-code/components/Clock';
<Clock clockId="sbb-uhr" />
Python verwendet weiterhin from clock import use_clock und dieselbe Ziel-ID. Mehrere Komponenten oder Editoren können absichtlich dieselbe Ziel-ID verwenden; die Ziel-IDs gelten für den gemeinsamen Website-Store, nicht nur für einen Editor.
Zustand und Lebensdauer
Die Browser-Stores gehören weiterhin der Website und leben mit deren Root-Store. Der Zustand der Uhr bleibt nach Ende oder Abbruch eines Skripts erhalten; ein Neustart setzt ihn nicht automatisch zurück. Ein unbekanntes Ziel wird beim ersten Update im ClockStore angelegt, auch wenn noch keine React-Komponente dafür angezeigt wird.
Jeder Aufruf von use_clock() erzeugt einen eigenen Python-Handle mit zunächst lokalen Nullwerten. Seine Getter lesen die letzten Werte dieses Handles, nicht den Browser-Store und nicht andere Handles. set_seconds() ändert im Browser nur den Sekundenzeiger. reset() setzt alle Zeiger zurück. Diese Erweiterung bietet keine Rückfragen vom Browser an Python.
Worker können zwischen Editoren wiederverwendet werden. Die registrierten JavaScript-Module erhalten pro Ausführung einen neuen Kontext. Bereits importierte Python-Hilfsmodule können jedoch alte Referenzen auf Module oder Handles behalten; deren Cache wird nicht rekursiv geleert. Solche Hilfsmodule sollen das Modul oder die Factory innerhalb der pro Ausführung aufgerufenen Funktion neu importieren und erst dann Handles anfordern. Ein auf Modulebene gespeichertes from clock import use_clock bleibt sonst an den alten, bereits geschlossenen Kontext gebunden.
Erweiterungen müssen eigene Timer und Hintergrundaufgaben vor Ende des Skripts beenden. Nachrichten nach Abschluss werden verworfen. Abbrechen beendet den Worker; ausstehende Nachrichten einer abgebrochenen oder ersetzten Ausführung werden durch die bestehende Ausführungsprüfung ignoriert. Das Framework stellt keine automatische Ressourcenverwaltung oder Hintergrunddienste für Erweiterungen bereit.
Installation
packages/tdev/pyodide-codeKopiere des packages/tdev/pyodide-code-Verzeichnis in das tdev-website/website/packages-Verzeichnis oder über updateTdev.config.yaml hinzufügen.
- Pyodide-Plugin und Dokument-Provider im
siteConfig.tsregistrieren:siteConfig.tsconst getSiteConfig: SiteConfigProvider = () => {return {plugins: [require.resolve('@tdev/pyodide-code/plugin'),],apiDocumentProviders: [require.resolve('@tdev/pyodide-code/register'),]};}; - ServiceWorker hinzufügen:
Damit Eingaben von Benutzer:innen abgewartet werden können, braucht es einen Servie-Worker. Dieser muss direkt in den statischen Ordner 👉static/pyodide.sw.jskopiert werden. Bei Paket-Updates muss auch diese Datei aktualisiert werden, damit Frontend und ServiceWorker dasselbe Eingabeprotokoll verwenden.static/pyodide.sw.jspyodide.sw.jsconst PY_INPUT = 'PY_INPUT';const PY_CLIENT_ID = 'PY_CLIENT_ID';const PY_AWAIT_INPUT = 'PY_AWAIT_INPUT';const PY_STDIN_ROUTE = '/py-get-input/';const PY_CANCEL_INPUT = 'PY_CANCEL_INPUT';const PY_CANCEL_ALL = 'PY_CANCEL_ALL';const INPUT_POLL_TIMEOUT = 10000;const INPUT_RECONNECT_TIMEOUT = 60000;self.addEventListener('install', (event) => {event.waitUntil(self.skipWaiting());});self.addEventListener('activate', (event) => {event.waitUntil(self.clients.claim());});const requests = new Map();const completeRequest = (requestId, response) => {const request = requests.get(requestId);if (!request || request.response) {return;}if (!request.resolve) {// Preserve replies and cancellation received between polls.request.response = response;return;}clearTimeout(request.timer);requests.delete(requestId);request.resolve(response);};const pollInput = (requestId, request) => {clearTimeout(request.timer);if (request.response) {requests.delete(requestId);return request.response;}return new Promise((resolve) => {request.resolve = resolve;// Keep each fetch below browser service-worker event time limits.request.timer = setTimeout(() => {request.resolve = null;request.timer = setTimeout(() => requests.delete(requestId), INPUT_RECONNECT_TIMEOUT);resolve(new Response(null, { status: 204, headers: { 'Cache-Control': 'no-store' } }));}, INPUT_POLL_TIMEOUT);});};self.addEventListener('message', (event) => {const clientId = event.source?.id;if (!clientId) {return;}if (event.data.type === PY_CLIENT_ID) {event.ports[0]?.postMessage({ clientId });return;}if (event.data.type === PY_CANCEL_ALL) {for (const [requestId, request] of requests) {if (request.clientId === clientId) {completeRequest(requestId, new Response('Run cancelled', { status: 410 }));}}return;}const request = requests.get(event.data.requestId);if (!request || request.clientId !== clientId) {return;}if (event.data.type === PY_INPUT) {completeRequest(event.data.requestId,new Response(event.data.value, { status: 200, headers: { 'Cache-Control': 'no-store' } }));} else if (event.data.type === PY_CANCEL_INPUT) {completeRequest(event.data.requestId, new Response('Run cancelled', { status: 410 }));}});self.addEventListener('fetch', (event) => {const url = new URL(event.request.url);if (url.origin !== self.location.origin || url.pathname !== PY_STDIN_ROUTE) {return;}const id = url.searchParams.get('id');const clientId = url.searchParams.get('clientId');const requestId = url.searchParams.get('requestId');const executionId = Number(url.searchParams.get('executionId'));if (!id || !clientId || !requestId || !Number.isSafeInteger(executionId) || executionId < 1) {event.respondWith(new Response('Invalid input request', { status: 400 }));return;}const existingRequest = requests.get(requestId);if (existingRequest) {if (existingRequest.clientId !== clientId ||existingRequest.id !== id ||existingRequest.executionId !== executionId ||existingRequest.resolve) {event.respondWith(new Response('Conflicting input request', { status: 409 }));return;}event.respondWith(pollInput(requestId, existingRequest));return;}const request = { clientId, id, executionId, resolve: null, response: null, timer: null };requests.set(requestId, request);event.respondWith(pollInput(requestId, request));event.waitUntil(self.clients.get(clientId).then((client) => {if (!client || client.type !== 'window') {completeRequest(requestId, new Response('Input owner closed', { status: 410 }));return;}client.postMessage({type: PY_AWAIT_INPUT,id,requestId,executionId,prompt: url.searchParams.get('prompt') || ''});}).catch(() => {completeRequest(requestId, new Response('Input owner unavailable', { status: 410 }));}));});
Danach muss erneut installiert werden:
yarn install
Pyodide-Version konfigurieren
Die exakte pyodide-Abhängigkeit in package.json legt die Laufzeitversion fest.
config.ts leitet daraus PYODIDE_VERSION und PYODIDE_INDEX_URL ab. Worker und
Main-Thread-Hook verwenden denselben ES-Modul-Loader und dieselbe CDN-Adresse.
Das Docusaurus-Plugin prüft beim Start, dass die Version exakt angegeben ist,
und konfiguriert den für Pyodide erforderlichen Modul-Worker.
Zum Aktualisieren auf eine neue stabile Version:
yarn workspace @tdev/pyodide-code add pyodide@<version> --exact
Die geänderten Dateien package.json und yarn.lock müssen gemeinsam übernommen werden.
Docusaurus- und Rspack-Updates prüfen
Der ModuleWorker-Konstruktor, der Chunk-Name pyodide-worker und die Parser- und
SplitChunks-Konfiguration in plugin.ts gehören zusammen. Sie erhalten den
ES-Modul-Worker und verhindern gemeinsame Chunks, die Rspack sonst über
importScripts() laden würde. Nach Updates von Docusaurus, Rspack oder Pyodide
muss ein Produktions-Build im Browser geprüft werden: Der Worker muss als Modul
starten und Python einschliesslich Paketimporten und Eingaben ausführen können.