@pyscript/core 0.4.33 → 0.4.35

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.
Files changed (61) hide show
  1. package/dist/{codemirror-Dr2Hgejs.js → codemirror-zS6ccqby.js} +2 -2
  2. package/dist/codemirror-zS6ccqby.js.map +1 -0
  3. package/dist/{codemirror_commands-MgxtVkrD.js → codemirror_commands-4671n0xX.js} +2 -2
  4. package/dist/{codemirror_commands-MgxtVkrD.js.map → codemirror_commands-4671n0xX.js.map} +1 -1
  5. package/dist/codemirror_lang-python-D74-Kgzp.js +2 -0
  6. package/dist/codemirror_lang-python-D74-Kgzp.js.map +1 -0
  7. package/dist/{codemirror_language-_XiX6II0.js → codemirror_language-BheMcNfw.js} +2 -2
  8. package/dist/{codemirror_language-_XiX6II0.js.map → codemirror_language-BheMcNfw.js.map} +1 -1
  9. package/dist/codemirror_state-D1qTXrff.js +2 -0
  10. package/dist/{codemirror_state-BKbyfKsm.js.map → codemirror_state-D1qTXrff.js.map} +1 -1
  11. package/dist/codemirror_view-C3_bb6sY.js +2 -0
  12. package/dist/codemirror_view-C3_bb6sY.js.map +1 -0
  13. package/dist/core-tc5wDVRu.js +3 -0
  14. package/dist/core-tc5wDVRu.js.map +1 -0
  15. package/dist/core.js +1 -1
  16. package/dist/{deprecations-manager-C4b5dQv_.js → deprecations-manager-CAgIm-Gn.js} +2 -2
  17. package/dist/{deprecations-manager-C4b5dQv_.js.map → deprecations-manager-CAgIm-Gn.js.map} +1 -1
  18. package/dist/{error-LGdr10Zu.js → error-CtG6l7bX.js} +2 -2
  19. package/dist/{error-LGdr10Zu.js.map → error-CtG6l7bX.js.map} +1 -1
  20. package/dist/index-O4achdbT.js +2 -0
  21. package/dist/index-O4achdbT.js.map +1 -0
  22. package/dist/mpy-CP-h-QIF.js +2 -0
  23. package/dist/mpy-CP-h-QIF.js.map +1 -0
  24. package/dist/py-C9FGLVGP.js +2 -0
  25. package/dist/py-C9FGLVGP.js.map +1 -0
  26. package/dist/{py-editor-BqaFoNyj.js → py-editor-djBi0dCK.js} +2 -2
  27. package/dist/{py-editor-BqaFoNyj.js.map → py-editor-djBi0dCK.js.map} +1 -1
  28. package/dist/py-terminal-H_VJVofp.js +2 -0
  29. package/dist/py-terminal-H_VJVofp.js.map +1 -0
  30. package/dist/xterm-BY7uk_OU.js +2 -0
  31. package/dist/{xterm-DqawCVsv.js.map → xterm-BY7uk_OU.js.map} +1 -1
  32. package/dist/zip-CKUyfu9C.js +2 -0
  33. package/dist/zip-CKUyfu9C.js.map +1 -0
  34. package/package.json +8 -8
  35. package/src/core.js +9 -2
  36. package/src/hooks.js +1 -1
  37. package/src/plugins/py-terminal/mpy.js +231 -0
  38. package/src/plugins/py-terminal/py.js +176 -0
  39. package/src/plugins/py-terminal.js +23 -260
  40. package/src/stdlib/pyscript.js +1 -1
  41. package/src/stdlib/pyweb/pydom.py +1 -1
  42. package/types/core.d.ts +2 -1
  43. package/types/hooks.d.ts +1 -0
  44. package/types/plugins/py-terminal/mpy.d.ts +2 -0
  45. package/types/plugins/py-terminal/py.d.ts +2 -0
  46. package/dist/codemirror-Dr2Hgejs.js.map +0 -1
  47. package/dist/codemirror_lang-python-Cxoc-ydj.js +0 -2
  48. package/dist/codemirror_lang-python-Cxoc-ydj.js.map +0 -1
  49. package/dist/codemirror_state-BKbyfKsm.js +0 -2
  50. package/dist/codemirror_view-C0PMO2z_.js +0 -2
  51. package/dist/codemirror_view-C0PMO2z_.js.map +0 -1
  52. package/dist/core-DbDOh4To.js +0 -3
  53. package/dist/core-DbDOh4To.js.map +0 -1
  54. package/dist/index-CTWZX_TW.js +0 -2
  55. package/dist/index-CTWZX_TW.js.map +0 -1
  56. package/dist/py-terminal-D1xo7BxU.js +0 -2
  57. package/dist/py-terminal-D1xo7BxU.js.map +0 -1
  58. package/dist/xterm-DqawCVsv.js +0 -2
  59. package/dist/zip-D2yvzXKD.js +0 -2
  60. package/dist/zip-D2yvzXKD.js.map +0 -1
  61. package/dist.zip +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyscript/core",
3
- "version": "0.4.33",
3
+ "version": "0.4.35",
4
4
  "type": "module",
5
5
  "description": "PyScript",
6
6
  "module": "./index.js",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "scripts": {
22
22
  "server": "npx static-handler --coi .",
23
- "build": "export ESLINT_USE_FLAT_CONFIG=false; npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && eslint src/ && npm run ts && npm run test:mpy",
23
+ "build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && eslint src/ && npm run ts && npm run test:mpy",
24
24
  "build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
25
25
  "build:plugins": "node rollup/plugins.cjs",
26
26
  "build:stdlib": "node rollup/stdlib.cjs",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@ungap/with-resolvers": "^0.1.0",
45
45
  "basic-devtools": "^0.1.6",
46
- "polyscript": "^0.12.9",
46
+ "polyscript": "^0.12.11",
47
47
  "sticky-module": "^0.1.1",
48
48
  "to-json-callback": "^0.1.1",
49
49
  "type-checked-collections": "^0.1.7"
@@ -54,18 +54,18 @@
54
54
  "@codemirror/language": "^6.10.1",
55
55
  "@codemirror/state": "^6.4.1",
56
56
  "@codemirror/view": "^6.26.3",
57
- "@playwright/test": "^1.44.0",
58
- "@rollup/plugin-commonjs": "^25.0.7",
57
+ "@playwright/test": "^1.44.1",
58
+ "@rollup/plugin-commonjs": "^25.0.8",
59
59
  "@rollup/plugin-node-resolve": "^15.2.3",
60
60
  "@rollup/plugin-terser": "^0.4.4",
61
61
  "@webreflection/toml-j0.4": "^1.1.3",
62
62
  "@xterm/addon-fit": "^0.10.0",
63
63
  "@xterm/addon-web-links": "^0.11.0",
64
- "bun": "^1.1.7",
64
+ "bun": "^1.1.10",
65
65
  "chokidar": "^3.6.0",
66
66
  "codemirror": "^6.0.1",
67
- "eslint": "^9.2.0",
68
- "rollup": "^4.17.2",
67
+ "eslint": "^9.3.0",
68
+ "rollup": "^4.18.0",
69
69
  "rollup-plugin-postcss": "^4.0.2",
70
70
  "rollup-plugin-string": "^3.0.0",
71
71
  "static-handler": "^0.4.3",
package/src/core.js CHANGED
@@ -24,10 +24,17 @@ import sync from "./sync.js";
24
24
  import bootstrapNodeAndPlugins from "./plugins-helper.js";
25
25
  import { ErrorCode } from "./exceptions.js";
26
26
  import { robustFetch as fetch, getText } from "./fetch.js";
27
- import { hooks, main, worker, codeFor, createFunction } from "./hooks.js";
27
+ import {
28
+ hooks,
29
+ main,
30
+ worker,
31
+ codeFor,
32
+ createFunction,
33
+ inputFailure,
34
+ } from "./hooks.js";
28
35
 
29
36
  import { stdlib, optional } from "./stdlib.js";
30
- export { stdlib, optional };
37
+ export { stdlib, optional, inputFailure };
31
38
 
32
39
  // generic helper to disambiguate between custom element and script
33
40
  const isScript = ({ tagName }) => tagName === "SCRIPT";
package/src/hooks.js CHANGED
@@ -46,7 +46,7 @@ export const createFunction = (self, name) => {
46
46
  const SetFunction = typedSet({ typeof: "function" });
47
47
  const SetString = typedSet({ typeof: "string" });
48
48
 
49
- const inputFailure = `
49
+ export const inputFailure = `
50
50
  import builtins
51
51
  def input(prompt=""):
52
52
  raise Exception("\\n ".join([
@@ -0,0 +1,231 @@
1
+ // PyScript pyodide terminal plugin
2
+ import { hooks, inputFailure } from "../../core.js";
3
+ import { defineProperties } from "polyscript/exports";
4
+
5
+ const bootstrapped = new WeakSet();
6
+
7
+ // this callback will be serialized as string and it never needs
8
+ // to be invoked multiple times. Each xworker here is bootstrapped
9
+ // only once thanks to the `sync.is_pyterminal()` check.
10
+ const workerReady = ({ interpreter, io, run, type }, { sync }) => {
11
+ if (type !== "mpy" || !sync.is_pyterminal()) return;
12
+
13
+ const { pyterminal_ready, pyterminal_read, pyterminal_write } = sync;
14
+
15
+ interpreter.registerJsModule("_pyscript_input", {
16
+ input: pyterminal_read,
17
+ });
18
+
19
+ run(
20
+ [
21
+ "from _pyscript_input import input",
22
+ "from polyscript import currentScript as _",
23
+ "__terminal__ = _.terminal",
24
+ "del _",
25
+ ].join(";"),
26
+ );
27
+
28
+ const missingReturn = new Uint8Array([13]);
29
+ io.stdout = (buffer) => {
30
+ if (buffer[0] === 10) pyterminal_write(missingReturn);
31
+ pyterminal_write(buffer);
32
+ };
33
+ io.stderr = (error) => {
34
+ pyterminal_write(String(error.message || error));
35
+ };
36
+
37
+ // tiny shim of the code module with only interact
38
+ // to bootstrap a REPL like environment
39
+ interpreter.registerJsModule("code", {
40
+ interact() {
41
+ const encoder = new TextEncoderStream();
42
+ encoder.readable.pipeTo(
43
+ new WritableStream({
44
+ write(buffer) {
45
+ for (const c of buffer) interpreter.replProcessChar(c);
46
+ },
47
+ }),
48
+ );
49
+
50
+ const writer = encoder.writable.getWriter();
51
+ sync.pyterminal_stream_write = (buffer) => writer.write(buffer);
52
+ pyterminal_ready();
53
+
54
+ interpreter.replInit();
55
+ },
56
+ });
57
+ };
58
+
59
+ export default async (element) => {
60
+ // lazy load these only when a valid terminal is found
61
+ const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = await Promise.all([
62
+ import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
63
+ import(/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"),
64
+ import(
65
+ /* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
66
+ ),
67
+ ]);
68
+
69
+ const terminalOptions = {
70
+ disableStdin: false,
71
+ cursorBlink: true,
72
+ cursorStyle: "block",
73
+ };
74
+
75
+ let stream;
76
+
77
+ // common main thread initialization for both worker
78
+ // or main case, bootstrapping the terminal on its target
79
+ const init = () => {
80
+ let target = element;
81
+ const selector = element.getAttribute("target");
82
+ if (selector) {
83
+ target =
84
+ document.getElementById(selector) ||
85
+ document.querySelector(selector);
86
+ if (!target) throw new Error(`Unknown target ${selector}`);
87
+ } else {
88
+ target = document.createElement("py-terminal");
89
+ target.style.display = "block";
90
+ element.after(target);
91
+ }
92
+ const terminal = new Terminal({
93
+ theme: {
94
+ background: "#191A19",
95
+ foreground: "#F5F2E7",
96
+ },
97
+ ...terminalOptions,
98
+ });
99
+ const fitAddon = new FitAddon();
100
+ terminal.loadAddon(fitAddon);
101
+ terminal.loadAddon(new WebLinksAddon());
102
+ terminal.open(target);
103
+ fitAddon.fit();
104
+ terminal.focus();
105
+ defineProperties(element, {
106
+ terminal: { value: terminal },
107
+ process: {
108
+ value: async (code) => stream.write(code),
109
+ },
110
+ });
111
+ return terminal;
112
+ };
113
+
114
+ // branch logic for the worker
115
+ if (element.hasAttribute("worker")) {
116
+ // add a hook on the main thread to setup all sync helpers
117
+ // also bootstrapping the XTerm target on main *BUT* ...
118
+ hooks.main.onWorker.add(function worker(_, xworker) {
119
+ // ... as multiple workers will add multiple callbacks
120
+ // be sure no xworker is ever initialized twice!
121
+ if (bootstrapped.has(xworker)) return;
122
+ bootstrapped.add(xworker);
123
+
124
+ // still cleanup this callback for future scripts/workers
125
+ hooks.main.onWorker.delete(worker);
126
+
127
+ const terminal = init();
128
+
129
+ const { sync } = xworker;
130
+
131
+ // handle the read mode on input
132
+ let promisedChunks = null;
133
+ let readChunks = "";
134
+
135
+ sync.is_pyterminal = () => true;
136
+
137
+ // put the terminal in a read-only state
138
+ // frees the worker on \r
139
+ sync.pyterminal_read = (buffer) => {
140
+ terminal.write(buffer);
141
+ promisedChunks = Promise.withResolvers();
142
+ return promisedChunks.promise;
143
+ };
144
+
145
+ // write if not reading input
146
+ sync.pyterminal_write = (buffer) => {
147
+ if (!promisedChunks) terminal.write(buffer);
148
+ };
149
+
150
+ // add the onData terminal listener which forwards to the worker
151
+ // everything typed in a queued char-by-char way
152
+ sync.pyterminal_ready = () => {
153
+ let queue = Promise.resolve();
154
+ stream = {
155
+ write: (buffer) =>
156
+ (queue = queue.then(() =>
157
+ sync.pyterminal_stream_write(buffer),
158
+ )),
159
+ };
160
+ terminal.onData((buffer) => {
161
+ if (promisedChunks) {
162
+ readChunks += buffer;
163
+ terminal.write(buffer);
164
+ if (readChunks.endsWith("\r")) {
165
+ terminal.write("\n");
166
+ promisedChunks.resolve(readChunks.slice(0, -1));
167
+ promisedChunks = null;
168
+ readChunks = "";
169
+ }
170
+ } else {
171
+ stream.write(buffer);
172
+ }
173
+ });
174
+ };
175
+ });
176
+
177
+ // setup remote thread JS/Python code for whenever the
178
+ // worker is ready to become a terminal
179
+ hooks.worker.onReady.add(workerReady);
180
+ } else {
181
+ // ⚠️ In an ideal world the inputFailure should never be used on main.
182
+ // However, Pyodide still can't compete with MicroPython REPL mode
183
+ // so while it's OK to keep that entry on main as default, we need
184
+ // to remove it ASAP from `mpy` use cases, otherwise MicroPython would
185
+ // also throw whenever an `input(...)` is required / digited.
186
+ hooks.main.codeBeforeRun.delete(inputFailure);
187
+ hooks.main.codeBeforeRun.add("from js import prompt as input");
188
+
189
+ // in the main case, just bootstrap XTerm without
190
+ // allowing any input as that's not possible / awkward
191
+ hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
192
+ if (type !== "mpy") return;
193
+
194
+ hooks.main.onReady.delete(main);
195
+
196
+ const terminal = init();
197
+
198
+ const missingReturn = new Uint8Array([13]);
199
+ io.stdout = (buffer) => {
200
+ if (buffer[0] === 10) terminal.write(missingReturn);
201
+ terminal.write(buffer);
202
+ };
203
+
204
+ // expose the __terminal__ one-off reference
205
+ globalThis.__py_terminal__ = terminal;
206
+ run("from js import __py_terminal__ as __terminal__");
207
+ delete globalThis.__py_terminal__;
208
+
209
+ // NOTE: this is NOT the same as the one within
210
+ // the onWorkerReady callback!
211
+ interpreter.registerJsModule("code", {
212
+ interact() {
213
+ const encoder = new TextEncoderStream();
214
+ encoder.readable.pipeTo(
215
+ new WritableStream({
216
+ write(buffer) {
217
+ for (const c of buffer)
218
+ interpreter.replProcessChar(c);
219
+ },
220
+ }),
221
+ );
222
+
223
+ stream = encoder.writable.getWriter();
224
+ terminal.onData((buffer) => stream.write(buffer));
225
+
226
+ interpreter.replInit();
227
+ },
228
+ });
229
+ });
230
+ }
231
+ };
@@ -0,0 +1,176 @@
1
+ // PyScript py-terminal plugin
2
+ import { hooks } from "../../core.js";
3
+ import { defineProperties } from "polyscript/exports";
4
+
5
+ const bootstrapped = new WeakSet();
6
+
7
+ // this callback will be serialized as string and it never needs
8
+ // to be invoked multiple times. Each xworker here is bootstrapped
9
+ // only once thanks to the `sync.is_pyterminal()` check.
10
+ const workerReady = ({ interpreter, io, run, type }, { sync }) => {
11
+ if (type !== "py" || !sync.is_pyterminal()) return;
12
+
13
+ run(
14
+ [
15
+ "from polyscript import currentScript as _",
16
+ "__terminal__ = _.terminal",
17
+ "del _",
18
+ ].join(";"),
19
+ );
20
+
21
+ let data = "";
22
+ const { pyterminal_read, pyterminal_write } = sync;
23
+ const decoder = new TextDecoder();
24
+ const generic = {
25
+ isatty: false,
26
+ write(buffer) {
27
+ data = decoder.decode(buffer);
28
+ pyterminal_write(data);
29
+ return buffer.length;
30
+ },
31
+ };
32
+
33
+ io.stderr = (error) => {
34
+ pyterminal_write(String(error.message || error));
35
+ };
36
+
37
+ interpreter.setStdout(generic);
38
+ interpreter.setStderr(generic);
39
+ interpreter.setStdin({
40
+ isatty: false,
41
+ stdin: () => pyterminal_read(data),
42
+ });
43
+ };
44
+
45
+ export default async (element) => {
46
+ // lazy load these only when a valid terminal is found
47
+ const [{ Terminal }, { Readline }, { FitAddon }, { WebLinksAddon }] =
48
+ await Promise.all([
49
+ import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
50
+ import(/* webpackIgnore: true */ "../../3rd-party/xterm-readline.js"),
51
+ import(/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"),
52
+ import(
53
+ /* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
54
+ ),
55
+ ]);
56
+
57
+ const readline = new Readline();
58
+
59
+ // common main thread initialization for both worker
60
+ // or main case, bootstrapping the terminal on its target
61
+ const init = (options) => {
62
+ let target = element;
63
+ const selector = element.getAttribute("target");
64
+ if (selector) {
65
+ target =
66
+ document.getElementById(selector) ||
67
+ document.querySelector(selector);
68
+ if (!target) throw new Error(`Unknown target ${selector}`);
69
+ } else {
70
+ target = document.createElement("py-terminal");
71
+ target.style.display = "block";
72
+ element.after(target);
73
+ }
74
+ const terminal = new Terminal({
75
+ theme: {
76
+ background: "#191A19",
77
+ foreground: "#F5F2E7",
78
+ },
79
+ ...options,
80
+ });
81
+ const fitAddon = new FitAddon();
82
+ terminal.loadAddon(fitAddon);
83
+ terminal.loadAddon(readline);
84
+ terminal.loadAddon(new WebLinksAddon());
85
+ terminal.open(target);
86
+ fitAddon.fit();
87
+ terminal.focus();
88
+ defineProperties(element, {
89
+ terminal: { value: terminal },
90
+ process: {
91
+ value: async (code) => {
92
+ // this loop is the only way I could find to actually simulate
93
+ // the user input char after char in a way that works
94
+ for (const line of code.split(/(?:\r|\n|\r\n)/)) {
95
+ terminal.paste(`${line}\n`);
96
+ do {
97
+ await new Promise((resolve) =>
98
+ setTimeout(resolve, 0),
99
+ );
100
+ } while (!readline.activeRead?.resolve);
101
+ readline.activeRead.resolve(line);
102
+ }
103
+ },
104
+ },
105
+ });
106
+ return terminal;
107
+ };
108
+
109
+ // branch logic for the worker
110
+ if (element.hasAttribute("worker")) {
111
+ // add a hook on the main thread to setup all sync helpers
112
+ // also bootstrapping the XTerm target on main *BUT* ...
113
+ hooks.main.onWorker.add(function worker(_, xworker) {
114
+ // ... as multiple workers will add multiple callbacks
115
+ // be sure no xworker is ever initialized twice!
116
+ if (bootstrapped.has(xworker)) return;
117
+ bootstrapped.add(xworker);
118
+
119
+ // still cleanup this callback for future scripts/workers
120
+ hooks.main.onWorker.delete(worker);
121
+
122
+ init({
123
+ disableStdin: false,
124
+ cursorBlink: true,
125
+ cursorStyle: "block",
126
+ });
127
+
128
+ xworker.sync.is_pyterminal = () => true;
129
+ xworker.sync.pyterminal_read = readline.read.bind(readline);
130
+ xworker.sync.pyterminal_write = readline.write.bind(readline);
131
+ });
132
+
133
+ // setup remote thread JS/Python code for whenever the
134
+ // worker is ready to become a terminal
135
+ hooks.worker.onReady.add(workerReady);
136
+ } else {
137
+ // in the main case, just bootstrap XTerm without
138
+ // allowing any input as that's not possible / awkward
139
+ hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
140
+ if (type !== "py") return;
141
+
142
+ console.warn("py-terminal is read only on main thread");
143
+ hooks.main.onReady.delete(main);
144
+
145
+ // on main, it's easy to trash and clean the current terminal
146
+ globalThis.__py_terminal__ = init({
147
+ disableStdin: true,
148
+ cursorBlink: false,
149
+ cursorStyle: "underline",
150
+ });
151
+ run("from js import __py_terminal__ as __terminal__");
152
+ delete globalThis.__py_terminal__;
153
+
154
+ io.stderr = (error) => {
155
+ readline.write(String(error.message || error));
156
+ };
157
+
158
+ let data = "";
159
+ const decoder = new TextDecoder();
160
+ const generic = {
161
+ isatty: false,
162
+ write(buffer) {
163
+ data = decoder.decode(buffer);
164
+ readline.write(data);
165
+ return buffer.length;
166
+ },
167
+ };
168
+ interpreter.setStdout(generic);
169
+ interpreter.setStderr(generic);
170
+ interpreter.setStdin({
171
+ isatty: false,
172
+ stdin: () => readline.read(data),
173
+ });
174
+ });
175
+ }
176
+ };