pyodide 0.28.3 → 0.29.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,393 @@
1
+ <!DOCTYPE html>
2
+ <!--
3
+ Pyodide Console v2 (Experimental)
4
+
5
+ This is an experimental version of the Pyodide console that provides
6
+ an enhanced terminal experience using xterm.js. This implementation replaces
7
+ the jQuery Terminal used in the original console with a more feature-rich
8
+ terminal emulator that offers better performance and modern terminal capabilities.
9
+
10
+ Note: This console is still under development and may not have all the features
11
+ of the stable console.
12
+ -->
13
+ <html>
14
+ <head>
15
+ <title>Pyodide Console</title>
16
+ <meta charset="UTF-8" />
17
+ <meta
18
+ http-equiv="origin-trial"
19
+ content="Aq6vv/4syIkcyMszFgCc9LlH0kX88jdE7SXfCFnh2RQN0nhhL8o6PCQ2oE3a7n3mC7+d9n89Repw5HYBtjarDw4AAAB3eyJvcmlnaW4iOiJodHRwczovL3B5b2RpZGUub3JnOjQ0MyIsImZlYXR1cmUiOiJXZWJBc3NlbWJseUpTUHJvbWlzZUludGVncmF0aW9uIiwiZXhwaXJ5IjoxNzMwMjQ2Mzk5LCJpc1N1YmRvbWFpbiI6dHJ1ZX0="
20
+ />
21
+ <meta
22
+ http-equiv="origin-trial"
23
+ content="Ai8IXb0XqedlM/Q2guWXFfBkKiYY9uaPZpdjHqc8y0ZvpAfK9SKzp/dIuFH+txG/HEKxt59uIkk39hhWrhNgbw4AAABieyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwMDAiLCJmZWF0dXJlIjoiV2ViQXNzZW1ibHlKU1Byb21pc2VJbnRlZ3JhdGlvbiIsImV4cGlyeSI6MTczMDI0NjM5OX0="
24
+ />
25
+ <link
26
+ rel="stylesheet"
27
+ href="https://unpkg.com/@xterm/xterm@5.4.0/css/xterm.css"
28
+ />
29
+ <link
30
+ href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🐍</text></svg>"
31
+ rel="icon"
32
+ />
33
+ <script src="https://unpkg.com/@xterm/xterm@5.4.0/lib/xterm.js"></script>
34
+ <script src="https://unpkg.com/@xterm/addon-fit@0.9.0/lib/addon-fit.js"></script>
35
+ <style>
36
+ * {
37
+ margin: 0;
38
+ padding: 0;
39
+ box-sizing: border-box;
40
+ }
41
+
42
+ html,
43
+ body {
44
+ margin: 0;
45
+ background-color: #000000;
46
+ font-family: "Monaco", "Menlo", "Courier New", monospace;
47
+ overflow: hidden;
48
+ }
49
+
50
+ #terminal {
51
+ position: fixed;
52
+ inset: 10px;
53
+ }
54
+
55
+ #loading {
56
+ display: inline-block;
57
+ width: 50px;
58
+ height: 50px;
59
+ position: fixed;
60
+ top: 50%;
61
+ left: 50%;
62
+ border: 3px solid rgba(172, 237, 255, 0.5);
63
+ border-radius: 50%;
64
+ border-top-color: #fff;
65
+ animation: spin 1s ease-in-out infinite;
66
+ -webkit-animation: spin 1s ease-in-out infinite;
67
+ }
68
+
69
+ @keyframes spin {
70
+ to {
71
+ -webkit-transform: rotate(360deg);
72
+ }
73
+ }
74
+ @-webkit-keyframes spin {
75
+ to {
76
+ -webkit-transform: rotate(360deg);
77
+ }
78
+ }
79
+ </style>
80
+ </head>
81
+ <body>
82
+ <div id="loading"></div>
83
+ <div id="terminal"></div>
84
+ <script type="module">
85
+ async function main() {
86
+ const fitAddon = new FitAddon.FitAddon();
87
+ const term = new Terminal({
88
+ cursorBlink: true,
89
+ cursorStyle: "block",
90
+ convertEol: true,
91
+ scrollback: 2_000,
92
+ fontSize: 18,
93
+ lineHeight: 1.4,
94
+ fontFamily: "monospace",
95
+ theme: {
96
+ background: "#000000",
97
+ foreground: "rgba(255, 255, 255, 0.8)",
98
+ cursor: "rgba(255, 255, 255, 0.8)",
99
+ selection: "#404040",
100
+ error: "#ff0000",
101
+ },
102
+ });
103
+ window.term = term;
104
+
105
+ term.open(document.getElementById("terminal"));
106
+ term.loadAddon(fitAddon);
107
+
108
+ fitAddon.fit();
109
+ term.focus();
110
+
111
+ window.addEventListener("resize", () => {
112
+ setTimeout(() => fitAddon.fit(), 50);
113
+ });
114
+
115
+ // Re-fit after the page has fully loaded
116
+ window.addEventListener("load", () => {
117
+ setTimeout(() => fitAddon.fit(), 100);
118
+ });
119
+
120
+ // Initialize Pyodide
121
+ let indexURL = "./";
122
+ const urlParams = new URLSearchParams(window.location.search);
123
+ const buildParam = urlParams.get("build");
124
+ if (buildParam && ["full", "debug", "pyc"].includes(buildParam)) {
125
+ indexURL = indexURL.replace("/full/", "/" + buildParam + "/");
126
+ }
127
+
128
+ const { loadPyodide } = await import(indexURL + "pyodide.mjs");
129
+ const pyodide = await loadPyodide();
130
+ globalThis.pyodide = pyodide;
131
+
132
+ // Hide loading spinner
133
+ document.getElementById("loading").style.display = "none";
134
+
135
+ const { repr_shorten, BANNER, PyodideConsole } =
136
+ pyodide.pyimport("pyodide.console");
137
+
138
+ term.writeln(
139
+ `Welcome to the Pyodide ${pyodide.version} terminal emulator 🐍\n${BANNER}`
140
+ );
141
+
142
+ const pyconsole = PyodideConsole(pyodide.globals);
143
+
144
+ const namespace = pyodide.globals.get("dict")();
145
+ const await_fut = pyodide.runPython(
146
+ `
147
+ import builtins
148
+ from pyodide.ffi import to_js
149
+ async def await_fut(fut):
150
+ res = await fut
151
+ if res is not None:
152
+ builtins._ = res
153
+ return to_js([res], depth=1)
154
+ await_fut
155
+ `,
156
+ { globals: namespace }
157
+ );
158
+ namespace.destroy();
159
+
160
+ pyconsole.stdout_callback = (s) => term.write(s);
161
+ pyconsole.stderr_callback = (s) => term.write(`\x1b[31m${s}\x1b[0m`);
162
+
163
+ // Handle fatal errors
164
+ pyodide._api.on_fatal = async (e) => {
165
+ if (e.name === "Exit") {
166
+ term.write(`\x1b[31m${e}\x1b[0m\r\n`);
167
+ term.write(
168
+ "\x1b[31mPyodide exited and can no longer be used.\x1b[0m\r\n"
169
+ );
170
+ } else {
171
+ term.write(
172
+ "\x1b[31mPyodide has suffered a fatal error. Please report this to the Pyodide maintainers.\x1b[0m\r\n"
173
+ );
174
+ term.write("\x1b[31mThe cause of the fatal error was:\x1b[0m\r\n");
175
+ term.write(`\x1b[31m${e.message || e}\x1b[0m\r\n`);
176
+ term.write(
177
+ "\x1b[31mLook in the browser console for more details.\x1b[0m\r\n"
178
+ );
179
+ }
180
+ };
181
+
182
+ // REPL implementation
183
+ const ps1 = ">>> ";
184
+ const ps2 = "... ";
185
+ let buffer = "";
186
+ let cursorIndex = 0; // index within buffer for in-line editing
187
+ let prompt = ps1;
188
+ const history = [];
189
+ let historyIndex = null; // null means not navigating history
190
+
191
+ term.write(prompt);
192
+
193
+ function addToHistory(command) {
194
+ const trimmed = command.trimEnd();
195
+ if (!trimmed) return;
196
+ const last = history[history.length - 1];
197
+ if (last !== trimmed) history.push(trimmed);
198
+ }
199
+
200
+ function refreshLine() {
201
+ // Write left part, save cursor, write right part, clear, restore cursor.
202
+ const clearCommand = "\x1b[0K";
203
+ const leftPart = prompt + buffer.slice(0, cursorIndex);
204
+ const rightPart = buffer.slice(cursorIndex);
205
+ term.write(
206
+ `\x1b[0G${leftPart}\x1b[s${rightPart}${clearCommand}\x1b[u`
207
+ );
208
+ }
209
+
210
+ function setBuffer(newBuffer, newCursorIndex = null) {
211
+ buffer = newBuffer;
212
+ if (newCursorIndex === null) {
213
+ cursorIndex = buffer.length;
214
+ } else {
215
+ cursorIndex = Math.max(0, Math.min(newCursorIndex, buffer.length));
216
+ }
217
+ refreshLine();
218
+ }
219
+
220
+ async function execLine(line) {
221
+ // Normalize non-breaking spaces to regular spaces
222
+ line = line.replace(/\u00a0/g, " ");
223
+ // clear the terminal
224
+ if (line === "clear") {
225
+ term.clear();
226
+ return;
227
+ }
228
+
229
+ const fut = pyconsole.push(line);
230
+
231
+ switch (fut.syntax_check) {
232
+ case "syntax-error":
233
+ term.write(`\x1b[31m${fut.formatted_error.trimEnd()}\x1b[0m`);
234
+ term.write("\r\n");
235
+ prompt = ps1;
236
+ addToHistory(line);
237
+ historyIndex = null;
238
+ fut.destroy();
239
+ break;
240
+ case "incomplete":
241
+ prompt = ps2;
242
+ addToHistory(line);
243
+ historyIndex = null;
244
+ return;
245
+ case "complete":
246
+ prompt = ps1;
247
+ try {
248
+ const wrapped = await_fut(fut);
249
+ const [value] = await wrapped;
250
+ if (value !== undefined) {
251
+ const output = repr_shorten.callKwargs(value, {
252
+ separator: "\n<long output truncated>\n",
253
+ });
254
+ term.write(output);
255
+ term.write("\r\n");
256
+ }
257
+ if (value instanceof pyodide.ffi.PyProxy) value.destroy();
258
+ wrapped.destroy();
259
+ } catch (e) {
260
+ const msg = fut.formatted_error || e.message;
261
+ term.write(`\x1b[31m${String(msg).trimEnd()}\x1b[0m`);
262
+ term.write("\r\n");
263
+ } finally {
264
+ fut.destroy();
265
+ }
266
+ addToHistory(line);
267
+ historyIndex = null;
268
+ break;
269
+ default:
270
+ term.write(
271
+ `\r\nUnexpected syntax_check value: ${fut.syntax_check}`
272
+ );
273
+ }
274
+ }
275
+
276
+ term.onData(async (data) => {
277
+ switch (data) {
278
+ case "\r": // Enter
279
+ term.write("\r\n");
280
+ await execLine(buffer);
281
+ buffer = "";
282
+ cursorIndex = 0;
283
+ term.write(prompt);
284
+ break;
285
+ case "\u0003": // Ctrl-C
286
+ pyconsole.buffer.clear();
287
+ buffer = "";
288
+ cursorIndex = 0;
289
+ term.write("^C\r\nKeyboardInterrupt\r\n" + ps1);
290
+ prompt = ps1;
291
+ historyIndex = null;
292
+ break;
293
+ case "\u0016": // Ctrl-V
294
+ // paste from clipboard
295
+ const clipboard = await navigator.clipboard.readText();
296
+ const newBuf =
297
+ buffer.slice(0, cursorIndex) +
298
+ clipboard +
299
+ buffer.slice(cursorIndex);
300
+ setBuffer(newBuf, newBuf.length);
301
+ break;
302
+ case "\u007F": // Backspace
303
+ if (cursorIndex > 0) {
304
+ const before = buffer.slice(0, cursorIndex - 1);
305
+ const after = buffer.slice(cursorIndex);
306
+ cursorIndex -= 1;
307
+ setBuffer(before + after, cursorIndex);
308
+ }
309
+ break;
310
+ case "\x1B[A": // Up arrow
311
+ if (prompt === ps1) {
312
+ if (historyIndex === null) historyIndex = history.length;
313
+ if (historyIndex > 0) {
314
+ historyIndex -= 1;
315
+ const newBuf = history[historyIndex] || "";
316
+ setBuffer(newBuf, newBuf.length);
317
+ }
318
+ }
319
+ break;
320
+ case "\x1B[B": // Down arrow
321
+ if (prompt === ps1 && historyIndex !== null) {
322
+ if (historyIndex < history.length - 1) {
323
+ historyIndex += 1;
324
+ const newBuf = history[historyIndex] || "";
325
+ setBuffer(newBuf, newBuf.length);
326
+ } else {
327
+ historyIndex = null;
328
+ setBuffer("", 0);
329
+ }
330
+ }
331
+ break;
332
+ case "\x1B[C": // Right arrow
333
+ if (cursorIndex < buffer.length) {
334
+ cursorIndex += 1;
335
+ refreshLine();
336
+ }
337
+ break;
338
+ case "\x1B[D": // Left arrow
339
+ if (cursorIndex > 0) {
340
+ cursorIndex -= 1;
341
+ refreshLine();
342
+ }
343
+ break;
344
+ default:
345
+ if (data) {
346
+ // Normalize non-breaking spaces to regular spaces
347
+ data = data.replace(/\u00a0/g, " ");
348
+ // Insert arbitrary string at cursor position
349
+ const before = buffer.slice(0, cursorIndex);
350
+ const after = buffer.slice(cursorIndex);
351
+ const newBuf = before + data + after;
352
+ const newCursor = cursorIndex + data.length;
353
+ setBuffer(newBuf, newCursor);
354
+ }
355
+ }
356
+ });
357
+
358
+ // 4. Extra features
359
+ let idbkvPromise;
360
+ async function getIDBKV() {
361
+ if (!idbkvPromise) {
362
+ idbkvPromise = await import(
363
+ "https://unpkg.com/idb-keyval@5.0.2/dist/esm/index.js"
364
+ );
365
+ }
366
+ return idbkvPromise;
367
+ }
368
+
369
+ async function mountDirectory(pyodideDirectory, directoryKey) {
370
+ if (pyodide.FS.analyzePath(pyodideDirectory).exists) {
371
+ return;
372
+ }
373
+ const { get, set } = await getIDBKV();
374
+ const opts = { id: "mountdirid", mode: "readwrite" };
375
+ let directoryHandle = await get(directoryKey);
376
+ if (!directoryHandle) {
377
+ directoryHandle = await showDirectoryPicker(opts);
378
+ await set(directoryKey, directoryHandle);
379
+ }
380
+ const permissionStatus = await directoryHandle.requestPermission(
381
+ opts
382
+ );
383
+ if (permissionStatus !== "granted") {
384
+ throw new Error("readwrite access to directory not granted");
385
+ }
386
+ await pyodide.mountNativeFS(pyodideDirectory, directoryHandle);
387
+ }
388
+ globalThis.mountDirectory = mountDirectory;
389
+ }
390
+ window.console_ready = main();
391
+ </script>
392
+ </body>
393
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyodide",
3
- "version": "0.28.3",
3
+ "version": "0.29.1",
4
4
  "description": "The Pyodide JavaScript package",
5
5
  "keywords": [
6
6
  "python",
@@ -15,15 +15,17 @@
15
15
  "url": "https://github.com/pyodide/pyodide/issues"
16
16
  },
17
17
  "license": "MPL-2.0",
18
+ "dependencies": {
19
+ "@types/emscripten": "^1.41.4",
20
+ "ws": "^8.5.0"
21
+ },
18
22
  "devDependencies": {
23
+ "@biomejs/biome": "2.1.1",
24
+ "@playwright/test": "^1.55.1",
19
25
  "@types/assert": "^1.5.6",
20
- "@types/emscripten": "^1.40.1",
21
26
  "@types/expect": "^24.3.0",
22
- "@types/mocha": "^9.1.0",
23
27
  "@types/node": "^20.8.4",
24
28
  "@types/ws": "^8.5.3",
25
- "chai": "^4.3.6",
26
- "chai-as-promised": "^7.1.1",
27
29
  "cross-env": "^7.0.3",
28
30
  "dts-bundle-generator": "^8.1.1",
29
31
  "esbuild": "^0.25.0",
@@ -31,10 +33,10 @@
31
33
  "mocha": "^9.0.2",
32
34
  "npm-run-all": "^4.1.5",
33
35
  "nyc": "^15.1.0",
36
+ "playwright": "^1.55.1",
34
37
  "prettier": "^2.2.1",
35
- "sinon": "^18.0.0",
36
- "ts-mocha": "^9.0.2",
37
38
  "tsd": "^0.24.1",
39
+ "tsx": "^4.20.5",
38
40
  "typedoc": "^0.27.6",
39
41
  "typescript": "5.7",
40
42
  "wabt": "^1.0.32"
@@ -67,7 +69,8 @@
67
69
  "pyodide.d.ts",
68
70
  "ffi.d.ts",
69
71
  "pyodide-lock.json",
70
- "console.html"
72
+ "console.html",
73
+ "console-v2.html"
71
74
  ],
72
75
  "browser": {
73
76
  "child_process": false,
@@ -83,25 +86,13 @@
83
86
  "build-inner": "node esbuild.config.inner.mjs",
84
87
  "build": "tsc --noEmit && node esbuild.config.outer.mjs",
85
88
  "test": "npm-run-all test:*",
86
- "test:unit": "cross-env TEST_NODE=1 ts-mocha --node-option=experimental-loader=./test/loader.mjs --node-option=experimental-wasm-stack-switching -p tsconfig.test.json \"test/unit/**\"",
87
- "test:node": "cross-env TEST_NODE=1 mocha test/integration/**/*.test.js",
88
- "test:browser": "mocha test/integration/**/*.test.js",
89
+ "test:unit": "cross-env TEST_NODE=1 node --import tsx --experimental-wasm-stack-switching --test 'test/unit/**/*.test.*'",
90
+ "test:node": "cross-env TEST_NODE=1 npx playwright test",
91
+ "test:browser": "npx playwright test",
89
92
  "tsc": "tsc --noEmit",
90
93
  "coverage": "cross-env TEST_NODE=1 npm-run-all coverage:*",
91
94
  "coverage:build": "nyc npm run test:node"
92
95
  },
93
- "mocha": {
94
- "bail": false,
95
- "timeout": 30000,
96
- "full-trace": true,
97
- "inline-diffs": true,
98
- "check-leaks": false,
99
- "global": [
100
- "pyodide",
101
- "page",
102
- "chai"
103
- ]
104
- },
105
96
  "nyc": {
106
97
  "reporter": [
107
98
  "html",
@@ -128,9 +119,6 @@
128
119
  ]
129
120
  }
130
121
  },
131
- "dependencies": {
132
- "ws": "^8.5.0"
133
- },
134
122
  "types": "./pyodide.d.ts",
135
123
  "engines": {
136
124
  "node": ">=18.0.0"