mirror of
https://github.com/pyscript/pyscript.git
synced 2025-12-21 19:25:35 -05:00
Merge branch 'main' into poc_ui_blocks
This commit is contained in:
@@ -26,6 +26,9 @@ import { ErrorCode } from "./exceptions.js";
|
||||
import { robustFetch as fetch, getText } from "./fetch.js";
|
||||
import { hooks, main, worker, codeFor, createFunction } from "./hooks.js";
|
||||
|
||||
import stdlib from "./stdlib.js";
|
||||
export { stdlib };
|
||||
|
||||
// generic helper to disambiguate between custom element and script
|
||||
const isScript = ({ tagName }) => tagName === "SCRIPT";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// PyScript py-editor plugin
|
||||
import { Hook, XWorker, dedent } from "polyscript/exports";
|
||||
import { TYPES } from "../core.js";
|
||||
import { TYPES, stdlib } from "../core.js";
|
||||
|
||||
const RUN_BUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
|
||||
|
||||
@@ -8,9 +8,11 @@ let id = 0;
|
||||
const getID = (type) => `${type}-editor-${id++}`;
|
||||
|
||||
const envs = new Map();
|
||||
const configs = new Map();
|
||||
|
||||
const hooks = {
|
||||
worker: {
|
||||
codeBeforeRun: () => stdlib,
|
||||
// works on both Pyodide and MicroPython
|
||||
onReady: ({ runAsync, io }, { sync }) => {
|
||||
io.stdout = (line) => sync.write(line);
|
||||
@@ -32,9 +34,17 @@ async function execute({ currentTarget }) {
|
||||
|
||||
if (!envs.has(env)) {
|
||||
const srcLink = URL.createObjectURL(new Blob([""]));
|
||||
const xworker = XWorker.call(new Hook(null, hooks), srcLink, {
|
||||
type: this.interpreter,
|
||||
});
|
||||
const details = { type: this.interpreter };
|
||||
const { config } = this;
|
||||
if (config) {
|
||||
details.configURL = config;
|
||||
const { parse } = config.endsWith(".toml")
|
||||
? await import(/* webpackIgnore: true */ "../3rd-party/toml.js")
|
||||
: JSON;
|
||||
details.config = parse(await fetch(config).then((r) => r.text()));
|
||||
}
|
||||
|
||||
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
|
||||
|
||||
const { sync } = xworker;
|
||||
const { promise, resolve } = Promise.withResolvers();
|
||||
@@ -138,13 +148,28 @@ const init = async (script, type, interpreter) => {
|
||||
]);
|
||||
|
||||
const isSetup = script.hasAttribute("setup");
|
||||
const hasConfig = script.hasAttribute("config");
|
||||
const env = `${interpreter}-${script.getAttribute("env") || getID(type)}`;
|
||||
|
||||
if (hasConfig && configs.has(env)) {
|
||||
throw new SyntaxError(
|
||||
configs.get(env)
|
||||
? `duplicated config for env: ${env}`
|
||||
: `unable to add a config to the env: ${env}`,
|
||||
);
|
||||
}
|
||||
|
||||
configs.set(env, hasConfig);
|
||||
|
||||
const source = script.src
|
||||
? await fetch(script.src).then((b) => b.text())
|
||||
: script.textContent;
|
||||
const context = {
|
||||
interpreter,
|
||||
env,
|
||||
config:
|
||||
hasConfig &&
|
||||
new URL(script.getAttribute("config"), location.href).href,
|
||||
get pySrc() {
|
||||
return isSetup ? source : editor.state.doc.toString();
|
||||
},
|
||||
@@ -225,7 +250,7 @@ const resetTimeout = () => {
|
||||
};
|
||||
|
||||
// triggered both ASAP on the living DOM and via MutationObserver later
|
||||
const pyEditor = async () => {
|
||||
const pyEditor = () => {
|
||||
if (timeout) return;
|
||||
timeout = setTimeout(resetTimeout, 250);
|
||||
for (const [type, interpreter] of TYPES) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// PyScript py-terminal plugin
|
||||
import { TYPES, hooks } from "../core.js";
|
||||
import { notify } from "./error.js";
|
||||
import { defineProperty } from "polyscript/exports";
|
||||
import { customObserver, defineProperty } from "polyscript/exports";
|
||||
|
||||
const SELECTOR = [...TYPES.keys()]
|
||||
.map((type) => `script[type="${type}"][terminal],${type}-script[terminal]`)
|
||||
.join(",");
|
||||
// will contain all valid selectors
|
||||
const SELECTORS = [];
|
||||
|
||||
// show the error on main and
|
||||
// stops the module from keep executing
|
||||
@@ -14,8 +13,6 @@ const notifyAndThrow = (message) => {
|
||||
throw new Error(message);
|
||||
};
|
||||
|
||||
const notParsedYet = (script) => !bootstrapped.has(script);
|
||||
|
||||
const onceOnMain = ({ attributes: { worker } }) => !worker;
|
||||
|
||||
const bootstrapped = new WeakSet();
|
||||
@@ -25,11 +22,68 @@ let addStyle = true;
|
||||
// this callback will be serialized as string and it never needs
|
||||
// to be invoked multiple times. Each xworker here is bootstrapped
|
||||
// only once thanks to the `sync.is_pyterminal()` check.
|
||||
const workerReady = ({ interpreter, io, run }, { sync }) => {
|
||||
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
|
||||
if (!sync.is_pyterminal()) return;
|
||||
|
||||
// in workers it's always safe to grab the polyscript currentScript
|
||||
run("from polyscript.currentScript import terminal as __terminal__");
|
||||
// the ugly `_` dance is due MicroPython not able to import via:
|
||||
// `from polyscript.currentScript import terminal as __terminal__`
|
||||
run(
|
||||
"from polyscript import currentScript as _; __terminal__ = _.terminal; del _",
|
||||
);
|
||||
|
||||
// This part is shared among both Pyodide and MicroPython
|
||||
io.stderr = (error) => {
|
||||
sync.pyterminal_write(`${error.message || error}\n`);
|
||||
};
|
||||
|
||||
const isMicroPython = type === "mpy";
|
||||
|
||||
// MicroPython has no code or code.interact()
|
||||
// This part patches it in a way that simulate
|
||||
// the code.interact() module in Pyodide.
|
||||
if (isMicroPython) {
|
||||
const encoder = new TextEncoder();
|
||||
const processData = () => {
|
||||
if (data.length) {
|
||||
for (
|
||||
let i = 0, b = encoder.encode(`${data}\r`);
|
||||
i < b.length;
|
||||
i++
|
||||
) {
|
||||
const code = interpreter.replProcessChar(b[i]);
|
||||
if (code) {
|
||||
throw new Error(
|
||||
`replProcessChar failed with code ${code}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
data = ">>> ";
|
||||
data = io.stdin();
|
||||
processData();
|
||||
};
|
||||
interpreter.setStderr = Object; // as no-op
|
||||
interpreter.setStdout = ({ write }) => {
|
||||
io.stdout = (str) => {
|
||||
// avoid duplicated outcome due i/o + readline
|
||||
const ignore = str.startsWith(`>>> ${data}`);
|
||||
return ignore ? 0 : write(`${str}\n`);
|
||||
};
|
||||
};
|
||||
interpreter.setStdin = ({ stdin }) => {
|
||||
io.stdin = stdin;
|
||||
};
|
||||
// tiny shim of the code module with only interact
|
||||
// to bootstrap a REPL like environment
|
||||
interpreter.registerJsModule("code", {
|
||||
interact() {
|
||||
interpreter.replInit();
|
||||
data = "";
|
||||
processData();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// This part is inevitably duplicated as external scope
|
||||
// can't be reached by workers out of the box.
|
||||
@@ -39,7 +93,7 @@ const workerReady = ({ interpreter, io, run }, { sync }) => {
|
||||
const generic = {
|
||||
isatty: true,
|
||||
write(buffer) {
|
||||
data = decoder.decode(buffer);
|
||||
data = isMicroPython ? buffer : decoder.decode(buffer);
|
||||
sync.pyterminal_write(data);
|
||||
return buffer.length;
|
||||
},
|
||||
@@ -50,38 +104,9 @@ const workerReady = ({ interpreter, io, run }, { sync }) => {
|
||||
isatty: true,
|
||||
stdin: () => sync.pyterminal_read(data),
|
||||
});
|
||||
|
||||
io.stderr = (error) => {
|
||||
sync.pyterminal_write(`${error.message || error}\n`);
|
||||
};
|
||||
};
|
||||
|
||||
const pyTerminal = async () => {
|
||||
const terminals = document.querySelectorAll(SELECTOR);
|
||||
|
||||
const unknown = [].filter.call(terminals, notParsedYet);
|
||||
|
||||
// no results will look further for runtime nodes
|
||||
if (!unknown.length) return;
|
||||
// early flag elements as known to avoid concurrent
|
||||
// MutationObserver invokes of this async handler
|
||||
else unknown.forEach(bootstrapped.add, bootstrapped);
|
||||
|
||||
// we currently support only one terminal as in "classic"
|
||||
if ([].filter.call(terminals, onceOnMain).length > 1)
|
||||
notifyAndThrow("You can use at most 1 main terminal");
|
||||
|
||||
// import styles lazily
|
||||
if (addStyle) {
|
||||
addStyle = false;
|
||||
document.head.append(
|
||||
Object.assign(document.createElement("link"), {
|
||||
rel: "stylesheet",
|
||||
href: new URL("./xterm.css", import.meta.url),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const pyTerminal = async (element) => {
|
||||
// lazy load these only when a valid terminal is found
|
||||
const [{ Terminal }, { Readline }, { FitAddon }] = await Promise.all([
|
||||
import(/* webpackIgnore: true */ "../3rd-party/xterm.js"),
|
||||
@@ -89,118 +114,140 @@ const pyTerminal = async () => {
|
||||
import(/* webpackIgnore: true */ "../3rd-party/xterm_addon-fit.js"),
|
||||
]);
|
||||
|
||||
for (const element of unknown) {
|
||||
// hopefully to be removed in the near future!
|
||||
if (element.matches('script[type="mpy"],mpy-script'))
|
||||
notifyAndThrow("Unsupported terminal.");
|
||||
const readline = new Readline();
|
||||
|
||||
const readline = new Readline();
|
||||
|
||||
// common main thread initialization for both worker
|
||||
// or main case, bootstrapping the terminal on its target
|
||||
const init = (options) => {
|
||||
let target = element;
|
||||
const selector = element.getAttribute("target");
|
||||
if (selector) {
|
||||
target =
|
||||
document.getElementById(selector) ||
|
||||
document.querySelector(selector);
|
||||
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||
} else {
|
||||
target = document.createElement("py-terminal");
|
||||
target.style.display = "block";
|
||||
element.after(target);
|
||||
}
|
||||
const terminal = new Terminal({
|
||||
theme: {
|
||||
background: "#191A19",
|
||||
foreground: "#F5F2E7",
|
||||
},
|
||||
...options,
|
||||
});
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(readline);
|
||||
terminal.open(target);
|
||||
fitAddon.fit();
|
||||
terminal.focus();
|
||||
defineProperty(element, "terminal", { value: terminal });
|
||||
return terminal;
|
||||
};
|
||||
|
||||
// branch logic for the worker
|
||||
if (element.hasAttribute("worker")) {
|
||||
// add a hook on the main thread to setup all sync helpers
|
||||
// also bootstrapping the XTerm target on main *BUT* ...
|
||||
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||
// ... as multiple workers will add multiple callbacks
|
||||
// be sure no xworker is ever initialized twice!
|
||||
if (bootstrapped.has(xworker)) return;
|
||||
bootstrapped.add(xworker);
|
||||
|
||||
// still cleanup this callback for future scripts/workers
|
||||
hooks.main.onWorker.delete(worker);
|
||||
|
||||
init({
|
||||
disableStdin: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
});
|
||||
|
||||
xworker.sync.is_pyterminal = () => true;
|
||||
xworker.sync.pyterminal_read = readline.read.bind(readline);
|
||||
xworker.sync.pyterminal_write = readline.write.bind(readline);
|
||||
});
|
||||
|
||||
// setup remote thread JS/Python code for whenever the
|
||||
// worker is ready to become a terminal
|
||||
hooks.worker.onReady.add(workerReady);
|
||||
// common main thread initialization for both worker
|
||||
// or main case, bootstrapping the terminal on its target
|
||||
const init = (options) => {
|
||||
let target = element;
|
||||
const selector = element.getAttribute("target");
|
||||
if (selector) {
|
||||
target =
|
||||
document.getElementById(selector) ||
|
||||
document.querySelector(selector);
|
||||
if (!target) throw new Error(`Unknown target ${selector}`);
|
||||
} else {
|
||||
// in the main case, just bootstrap XTerm without
|
||||
// allowing any input as that's not possible / awkward
|
||||
hooks.main.onReady.add(function main({ interpreter, io, run }) {
|
||||
console.warn("py-terminal is read only on main thread");
|
||||
hooks.main.onReady.delete(main);
|
||||
|
||||
// on main, it's easy to trash and clean the current terminal
|
||||
globalThis.__py_terminal__ = init({
|
||||
disableStdin: true,
|
||||
cursorBlink: false,
|
||||
cursorStyle: "underline",
|
||||
});
|
||||
run("from js import __py_terminal__ as __terminal__");
|
||||
delete globalThis.__py_terminal__;
|
||||
|
||||
// This part is inevitably duplicated as external scope
|
||||
// can't be reached by workers out of the box.
|
||||
// The detail is that here we use readline here, not sync.
|
||||
const decoder = new TextDecoder();
|
||||
let data = "";
|
||||
const generic = {
|
||||
isatty: true,
|
||||
write(buffer) {
|
||||
data = decoder.decode(buffer);
|
||||
readline.write(data);
|
||||
return buffer.length;
|
||||
},
|
||||
};
|
||||
interpreter.setStdout(generic);
|
||||
interpreter.setStderr(generic);
|
||||
interpreter.setStdin({
|
||||
isatty: true,
|
||||
stdin: () => readline.read(data),
|
||||
});
|
||||
|
||||
io.stderr = (error) => {
|
||||
readline.write(`${error.message || error}\n`);
|
||||
};
|
||||
});
|
||||
target = document.createElement("py-terminal");
|
||||
target.style.display = "block";
|
||||
element.after(target);
|
||||
}
|
||||
const terminal = new Terminal({
|
||||
theme: {
|
||||
background: "#191A19",
|
||||
foreground: "#F5F2E7",
|
||||
},
|
||||
...options,
|
||||
});
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.loadAddon(readline);
|
||||
terminal.open(target);
|
||||
fitAddon.fit();
|
||||
terminal.focus();
|
||||
defineProperty(element, "terminal", { value: terminal });
|
||||
return terminal;
|
||||
};
|
||||
|
||||
// branch logic for the worker
|
||||
if (element.hasAttribute("worker")) {
|
||||
// add a hook on the main thread to setup all sync helpers
|
||||
// also bootstrapping the XTerm target on main *BUT* ...
|
||||
hooks.main.onWorker.add(function worker(_, xworker) {
|
||||
// ... as multiple workers will add multiple callbacks
|
||||
// be sure no xworker is ever initialized twice!
|
||||
if (bootstrapped.has(xworker)) return;
|
||||
bootstrapped.add(xworker);
|
||||
|
||||
// still cleanup this callback for future scripts/workers
|
||||
hooks.main.onWorker.delete(worker);
|
||||
|
||||
init({
|
||||
disableStdin: false,
|
||||
cursorBlink: true,
|
||||
cursorStyle: "block",
|
||||
});
|
||||
|
||||
xworker.sync.is_pyterminal = () => true;
|
||||
xworker.sync.pyterminal_read = readline.read.bind(readline);
|
||||
xworker.sync.pyterminal_write = readline.write.bind(readline);
|
||||
});
|
||||
|
||||
// setup remote thread JS/Python code for whenever the
|
||||
// worker is ready to become a terminal
|
||||
hooks.worker.onReady.add(workerReady);
|
||||
} else {
|
||||
// in the main case, just bootstrap XTerm without
|
||||
// allowing any input as that's not possible / awkward
|
||||
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
|
||||
console.warn("py-terminal is read only on main thread");
|
||||
hooks.main.onReady.delete(main);
|
||||
|
||||
// on main, it's easy to trash and clean the current terminal
|
||||
globalThis.__py_terminal__ = init({
|
||||
disableStdin: true,
|
||||
cursorBlink: false,
|
||||
cursorStyle: "underline",
|
||||
});
|
||||
run("from js import __py_terminal__ as __terminal__");
|
||||
delete globalThis.__py_terminal__;
|
||||
|
||||
io.stderr = (error) => {
|
||||
readline.write(`${error.message || error}\n`);
|
||||
};
|
||||
|
||||
const isMicroPython = type === "mpy";
|
||||
|
||||
if (isMicroPython) {
|
||||
interpreter.setStderr = Object; // as no-op
|
||||
interpreter.setStdin = Object; // as no-op
|
||||
interpreter.setStdout = ({ write }) => {
|
||||
io.stdout = (str) => write(`${str}\n`);
|
||||
};
|
||||
}
|
||||
|
||||
// This part is inevitably duplicated as external scope
|
||||
// can't be reached by workers out of the box.
|
||||
// The detail is that here we use readline here, not sync.
|
||||
const decoder = new TextDecoder();
|
||||
let data = "";
|
||||
const generic = {
|
||||
isatty: true,
|
||||
write(buffer) {
|
||||
data = isMicroPython ? buffer : decoder.decode(buffer);
|
||||
readline.write(data);
|
||||
return buffer.length;
|
||||
},
|
||||
};
|
||||
interpreter.setStdout(generic);
|
||||
interpreter.setStderr(generic);
|
||||
interpreter.setStdin({
|
||||
isatty: true,
|
||||
stdin: () => readline.read(data),
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const mo = new MutationObserver(pyTerminal);
|
||||
mo.observe(document, { childList: true, subtree: true });
|
||||
for (const key of TYPES.keys()) {
|
||||
const selector = `script[type="${key}"][terminal],${key}-script[terminal]`;
|
||||
SELECTORS.push(selector);
|
||||
customObserver.set(selector, async (element) => {
|
||||
// we currently support only one terminal on main as in "classic"
|
||||
const terminals = document.querySelectorAll(SELECTORS.join(","));
|
||||
if ([].filter.call(terminals, onceOnMain).length > 1)
|
||||
notifyAndThrow("You can use at most 1 main terminal");
|
||||
|
||||
// try to check the current document ASAP
|
||||
export default pyTerminal();
|
||||
// import styles lazily
|
||||
if (addStyle) {
|
||||
addStyle = false;
|
||||
document.head.append(
|
||||
Object.assign(document.createElement("link"), {
|
||||
rel: "stylesheet",
|
||||
href: new URL("./xterm.css", import.meta.url),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await pyTerminal(element);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user