@pyscript/core 0.2.10 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,158 @@
1
+ // PyScript py-terminal plugin
2
+ import { TYPES, hooks } from "../core.js";
3
+
4
+ const CDN = "https://cdn.jsdelivr.net/npm/xterm";
5
+ const XTERM = "5.3.0";
6
+ const XTERM_READLINE = "1.1.1";
7
+ const SELECTOR = [...TYPES.keys()]
8
+ .map((type) => `script[type="${type}"][terminal],${type}-script[terminal]`)
9
+ .join(",");
10
+
11
+ const pyTerminal = async () => {
12
+ const terminals = document.querySelectorAll(SELECTOR);
13
+
14
+ // no results will look further for runtime nodes
15
+ if (!terminals.length) return;
16
+
17
+ // we currently support only one terminal as in "classic"
18
+ if (terminals.length > 1)
19
+ console.warn("Unable to satisfy multiple terminals");
20
+
21
+ // if we arrived this far, let's drop the MutationObserver
22
+ mo.disconnect();
23
+
24
+ const [element] = terminals;
25
+ // hopefully to be removed in the near future!
26
+ if (element.matches('script[type="mpy"],mpy-script'))
27
+ throw new Error("Unsupported terminal");
28
+
29
+ // import styles once and lazily (only on valid terminal)
30
+ if (!document.querySelector(`link[href^="${CDN}"]`)) {
31
+ document.head.append(
32
+ Object.assign(document.createElement("link"), {
33
+ rel: "stylesheet",
34
+ href: `${CDN}@${XTERM}/css/xterm.min.css`,
35
+ }),
36
+ );
37
+ }
38
+
39
+ // lazy load these only when a valid terminal is found
40
+ const [{ Terminal }, { Readline }] = await Promise.all([
41
+ import(/* webpackIgnore: true */ `${CDN}@${XTERM}/+esm`),
42
+ import(
43
+ /* webpackIgnore: true */ `${CDN}-readline@${XTERM_READLINE}/+esm`
44
+ ),
45
+ ]);
46
+
47
+ const readline = new Readline();
48
+
49
+ // common main thread initialization for both worker
50
+ // or main case, bootstrapping the terminal on its target
51
+ const init = (options) => {
52
+ let target = element;
53
+ const selector = element.getAttribute("target");
54
+ if (selector) {
55
+ target =
56
+ document.getElementById(selector) ||
57
+ document.querySelector(selector);
58
+ if (!target) throw new Error(`Unknown target ${selector}`);
59
+ } else {
60
+ target = document.createElement(`${element.type}-terminal`);
61
+ target.style.display = "block";
62
+ element.after(target);
63
+ }
64
+ const terminal = new Terminal({
65
+ theme: {
66
+ background: "#191A19",
67
+ foreground: "#F5F2E7",
68
+ },
69
+ ...options,
70
+ });
71
+ terminal.loadAddon(readline);
72
+ terminal.open(target);
73
+ terminal.focus();
74
+ };
75
+
76
+ // branch logic for the worker
77
+ if (element.hasAttribute("worker")) {
78
+ // when the remote thread onReady triggers:
79
+ // setup the interpreter stdout and stderr
80
+ const workerReady = ({ interpreter }, { sync }) => {
81
+ sync.pyterminal_drop_hooks();
82
+ const decoder = new TextDecoder();
83
+ const generic = {
84
+ isatty: true,
85
+ write(buffer) {
86
+ sync.pyterminal_write(decoder.decode(buffer));
87
+ return buffer.length;
88
+ },
89
+ };
90
+ interpreter.setStdout(generic);
91
+ interpreter.setStderr(generic);
92
+ };
93
+
94
+ // run in python code able to replace builtins.input
95
+ // using the xworker.sync non blocking prompt
96
+ const codeBefore = `
97
+ import builtins
98
+ from pyscript import sync as _sync
99
+
100
+ builtins.input = lambda prompt: _sync.pyterminal_read(prompt)
101
+ `;
102
+
103
+ // at the end of the code, make the terminal interactive
104
+ const codeAfter = `
105
+ import code as _code
106
+ _code.interact()
107
+ `;
108
+
109
+ // add a hook on the main thread to setup all sync helpers
110
+ // also bootstrapping the XTerm target on main
111
+ hooks.main.onWorker.add(function worker(_, xworker) {
112
+ hooks.main.onWorker.delete(worker);
113
+ init({
114
+ disableStdin: false,
115
+ cursorBlink: true,
116
+ cursorStyle: "block",
117
+ });
118
+ xworker.sync.pyterminal_read = readline.read.bind(readline);
119
+ xworker.sync.pyterminal_write = readline.write.bind(readline);
120
+ // allow a worker to drop main thread hooks ASAP
121
+ xworker.sync.pyterminal_drop_hooks = () => {
122
+ hooks.worker.onReady.delete(workerReady);
123
+ hooks.worker.codeBeforeRun.delete(codeBefore);
124
+ hooks.worker.codeAfterRun.delete(codeAfter);
125
+ };
126
+ });
127
+
128
+ // setup remote thread JS/Python code for whenever the
129
+ // worker is ready to become a terminal
130
+ hooks.worker.onReady.add(workerReady);
131
+ hooks.worker.codeBeforeRun.add(codeBefore);
132
+ hooks.worker.codeAfterRun.add(codeAfter);
133
+ } else {
134
+ // in the main case, just bootstrap XTerm without
135
+ // allowing any input as that's not possible / awkward
136
+ hooks.main.onReady.add(function main({ io }) {
137
+ console.warn("py-terminal is read only on main thread");
138
+ hooks.main.onReady.delete(main);
139
+ init({
140
+ disableStdin: true,
141
+ cursorBlink: false,
142
+ cursorStyle: "underline",
143
+ });
144
+ io.stdout = (value) => {
145
+ readline.write(`${value}\n`);
146
+ };
147
+ io.stderr = (error) => {
148
+ readline.write(`${error.message || error}\n`);
149
+ };
150
+ });
151
+ }
152
+ };
153
+
154
+ const mo = new MutationObserver(pyTerminal);
155
+ mo.observe(document, { childList: true, subtree: true });
156
+
157
+ // try to check the current document ASAP
158
+ export default pyTerminal();
@@ -0,0 +1,26 @@
1
+ import { defineProperty } from "polyscript/exports";
2
+
3
+ // helper for all script[type="py"] out there
4
+ const before = (script) => {
5
+ defineProperty(document, "currentScript", {
6
+ configurable: true,
7
+ get: () => script,
8
+ });
9
+ };
10
+
11
+ const after = () => {
12
+ delete document.currentScript;
13
+ };
14
+
15
+ // common life-cycle handlers for any node
16
+ export default async (main, wrap, element, hook) => {
17
+ const isAsync = hook.endsWith("Async");
18
+ const isBefore = hook.startsWith("onBefore");
19
+ // make it possible to reach the current target node via Python
20
+ // or clean up for other scripts executing around this one
21
+ (isBefore ? before : after)(element);
22
+ for (const fn of main(hook)) {
23
+ if (isAsync) await fn(wrap, element);
24
+ else fn(wrap, element);
25
+ }
26
+ };
package/src/plugins.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // ⚠️ This file is an artifact: DO NOT MODIFY
2
2
  export default {
3
3
  error: () => import(/* webpackIgnore: true */ "./plugins/error.js"),
4
+ ["py-terminal"]: () => import(/* webpackIgnore: true */ "./plugins/py-terminal.js"),
4
5
  };
package/src/stdlib.js CHANGED
@@ -10,16 +10,22 @@ import pyscript from "./stdlib/pyscript.js";
10
10
 
11
11
  const { entries } = Object;
12
12
 
13
- const python = ["from pathlib import Path as _Path"];
13
+ const python = [
14
+ "import os as _os",
15
+ "from pathlib import Path as _Path",
16
+ "_path = None",
17
+ ];
14
18
 
15
19
  const write = (base, literal) => {
16
20
  for (const [key, value] of entries(literal)) {
17
- const path = `_Path("${base}/${key}")`;
21
+ python.push(`_path = _Path("${base}/${key}")`);
18
22
  if (typeof value === "string") {
19
23
  const code = JSON.stringify(value);
20
- python.push(`${path}.write_text(${code})`);
24
+ python.push(`_path.write_text(${code})`);
21
25
  } else {
22
- python.push(`${path}.mkdir(parents=True, exist_ok=True)`);
26
+ // @see https://github.com/pyscript/pyscript/pull/1813#issuecomment-1781502909
27
+ python.push(`if not _os.path.exists("${base}/${key}"):`);
28
+ python.push(" _path.mkdir(parents=True, exist_ok=True)");
23
29
  write(`${base}/${key}`, value);
24
30
  }
25
31
  }
@@ -28,6 +34,8 @@ const write = (base, literal) => {
28
34
  write(".", pyscript);
29
35
 
30
36
  python.push("del _Path");
37
+ python.push("del _path");
38
+ python.push("del _os");
31
39
  python.push("\n");
32
40
 
33
41
  export default python.join("\n");
package/types/core.d.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import TYPES from "./types.js";
1
2
  declare const exportedPyWorker: any;
2
3
  declare const exportedHooks: any;
3
4
  declare const exportedConfig: any;
4
- export { exportedPyWorker as PyWorker, exportedHooks as hooks, exportedConfig as config };
5
+ declare const exportedWhenDefined: any;
6
+ export { TYPES, exportedPyWorker as PyWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
package/types/hooks.d.ts CHANGED
@@ -1,13 +1,38 @@
1
- declare namespace _default {
2
- let onInterpreterReady: Set<Function>;
3
- let onBeforeRun: Set<Function>;
4
- let onBeforeRunAsync: Set<Function>;
5
- let onAfterRun: Set<Function>;
6
- let onAfterRunAsync: Set<Function>;
7
- let onWorkerReady: Set<Function>;
8
- let codeBeforeRunWorker: Set<string>;
9
- let codeBeforeRunWorkerAsync: Set<string>;
10
- let codeAfterRunWorker: Set<string>;
11
- let codeAfterRunWorkerAsync: Set<string>;
1
+ export function main(name: any): any;
2
+ export function worker(name: any): any;
3
+ export function codeFor(branch: any): {};
4
+ export function createFunction(self: any, name: any): any;
5
+ export namespace hooks {
6
+ namespace main {
7
+ let onWorker: Set<Function>;
8
+ let onReady: Set<Function>;
9
+ let onBeforeRun: Set<Function>;
10
+ let onBeforeRunAsync: Set<Function>;
11
+ let onAfterRun: Set<Function>;
12
+ let onAfterRunAsync: Set<Function>;
13
+ let codeBeforeRun: Set<string>;
14
+ let codeBeforeRunAsync: Set<string>;
15
+ let codeAfterRun: Set<string>;
16
+ let codeAfterRunAsync: Set<string>;
17
+ }
18
+ namespace worker {
19
+ let onReady_1: Set<Function>;
20
+ export { onReady_1 as onReady };
21
+ let onBeforeRun_1: Set<Function>;
22
+ export { onBeforeRun_1 as onBeforeRun };
23
+ let onBeforeRunAsync_1: Set<Function>;
24
+ export { onBeforeRunAsync_1 as onBeforeRunAsync };
25
+ let onAfterRun_1: Set<Function>;
26
+ export { onAfterRun_1 as onAfterRun };
27
+ let onAfterRunAsync_1: Set<Function>;
28
+ export { onAfterRunAsync_1 as onAfterRunAsync };
29
+ let codeBeforeRun_1: Set<string>;
30
+ export { codeBeforeRun_1 as codeBeforeRun };
31
+ let codeBeforeRunAsync_1: Set<string>;
32
+ export { codeBeforeRunAsync_1 as codeBeforeRunAsync };
33
+ let codeAfterRun_1: Set<string>;
34
+ export { codeAfterRun_1 as codeAfterRun };
35
+ let codeAfterRunAsync_1: Set<string>;
36
+ export { codeAfterRunAsync_1 as codeAfterRunAsync };
37
+ }
12
38
  }
13
- export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: Promise<void>;
2
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare function _default(main: any, wrap: any, element: any, hook: any): Promise<void>;
2
+ export default _default;
@@ -1,4 +1,5 @@
1
- declare namespace _default {
2
- function error(): Promise<typeof import("./plugins/error.js")>;
3
- }
1
+ declare const _default: {
2
+ error: () => Promise<typeof import("./plugins/error.js")>;
3
+ "py-terminal": () => Promise<typeof import("./plugins/py-terminal.js")>;
4
+ };
4
5
  export default _default;
@@ -1,2 +0,0 @@
1
- import{hooks as e}from"./core.js";function r(e){const r=document.createElement("div");r.className="py-error",r.textContent=e,r.style.cssText="\n border: 1px solid red;\n background: #ffdddd;\n color: black;\n font-family: courier, monospace;\n white-space: pre;\n overflow-x: auto;\n padding: 8px;\n margin-top: 8px;\n ",document.body.append(r)}e.onInterpreterReady.add((function o(n){e.onInterpreterReady.delete(o);const{stderr:t}=n.io;n.io.stderr=(e,...o)=>(r(e.message||e),t(e,...o)),addEventListener("error",(({message:e})=>{e.startsWith("Uncaught PythonError")&&r(e)}))}));export{r as notify};
2
- //# sourceMappingURL=error-0_IesYFM.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"error-0_IesYFM.js","sources":["../src/plugins/error.js"],"sourcesContent":["// PyScript Error Plugin\nimport { hooks } from \"../core.js\";\n\nhooks.onInterpreterReady.add(function override(pyScript) {\n // be sure this override happens only once\n hooks.onInterpreterReady.delete(override);\n\n // trap generic `stderr` to propagate to it regardless\n const { stderr } = pyScript.io;\n\n // override it with our own logic\n pyScript.io.stderr = (error, ...rest) => {\n notify(error.message || error);\n // let other plugins or stderr hook, if any, do the rest\n return stderr(error, ...rest);\n };\n\n // be sure uncaught Python errors are also visible\n addEventListener(\"error\", ({ message }) => {\n if (message.startsWith(\"Uncaught PythonError\")) notify(message);\n });\n});\n\n// Error hook utilities\n\n// Custom function to show notifications\n\n/**\n * Add a banner to the top of the page, notifying the user of an error\n * @param {string} message\n */\nexport function notify(message) {\n const div = document.createElement(\"div\");\n div.className = \"py-error\";\n div.textContent = message;\n div.style.cssText = `\n border: 1px solid red;\n background: #ffdddd;\n color: black;\n font-family: courier, monospace;\n white-space: pre;\n overflow-x: auto;\n padding: 8px;\n margin-top: 8px;\n `;\n document.body.append(div);\n}\n"],"names":["notify","message","div","document","createElement","className","textContent","style","cssText","body","append","hooks","onInterpreterReady","add","override","pyScript","delete","stderr","io","error","rest","addEventListener","startsWith"],"mappings":"kCA+BO,SAASA,EAAOC,GACnB,MAAMC,EAAMC,SAASC,cAAc,OACnCF,EAAIG,UAAY,WAChBH,EAAII,YAAcL,EAClBC,EAAIK,MAAMC,QAAU,6MAUpBL,SAASM,KAAKC,OAAOR,EACzB,CA3CAS,EAAMC,mBAAmBC,KAAI,SAASC,EAASC,GAE3CJ,EAAMC,mBAAmBI,OAAOF,GAGhC,MAAMG,OAAEA,GAAWF,EAASG,GAG5BH,EAASG,GAAGD,OAAS,CAACE,KAAUC,KAC5BpB,EAAOmB,EAAMlB,SAAWkB,GAEjBF,EAAOE,KAAUC,IAI5BC,iBAAiB,SAAS,EAAGpB,cACrBA,EAAQqB,WAAW,yBAAyBtB,EAAOC,EAAQ,GAEvE"}