pyodide 0.20.0-alpha.1 → 0.20.0

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.
package/api.ts CHANGED
@@ -33,22 +33,22 @@ export let version: string = ""; // actually defined in loadPyodide (see pyodide
33
33
 
34
34
  let runPythonPositionalGlobalsDeprecationWarned = false;
35
35
  /**
36
- * Runs a string of Python code from JavaScript.
36
+ * Runs a string of Python code from JavaScript, using :any:`pyodide.eval_code`
37
+ * to evaluate the code. If the last statement in the Python code is an
38
+ * expression (and the code doesn't end with a semicolon), the value of the
39
+ * expression is returned.
37
40
  *
38
- * The last part of the string may be an expression, in which case, its value is
39
- * returned.
41
+ * .. admonition:: Positional globals argument
42
+ * :class: warning
40
43
  *
41
- * .. admonition:: Positional globals argument :class: warning
42
- *
43
- * In Pyodide v0.19, this function took the globals parameter as a
44
- * positional argument rather than as a named argument. In v0.20 this will
45
- * still work but it is deprecated. It will be removed in v0.21.
44
+ * In Pyodide v0.19, this function took the globals parameter as a positional
45
+ * argument rather than as a named argument. In v0.20 this will still work
46
+ * but it is deprecated. It will be removed in v0.21.
46
47
  *
47
48
  * @param code Python code to evaluate
48
49
  * @param options
49
50
  * @param options.globals An optional Python dictionary to use as the globals.
50
- * Defaults to :any:`pyodide.globals`. Uses the Python API
51
- * :any:`pyodide.eval_code` to evaluate the code.
51
+ * Defaults to :any:`pyodide.globals`.
52
52
  * @returns The result of the Python code translated to JavaScript. See the
53
53
  * documentation for :any:`pyodide.eval_code` for more info.
54
54
  */
@@ -60,7 +60,7 @@ export function runPython(
60
60
  options = { globals: options as PyProxy };
61
61
  if (!runPythonPositionalGlobalsDeprecationWarned) {
62
62
  console.warn(
63
- "Passing a PyProxy as the second argument to runPython is deprecated. Use 'runPython(code, {globals : some_dict})' instead."
63
+ "Passing a PyProxy as the second argument to runPython is deprecated and will be removed in v0.21. Use 'runPython(code, {globals : some_dict})' instead."
64
64
  );
65
65
  runPythonPositionalGlobalsDeprecationWarned = true;
66
66
  }
@@ -122,8 +122,11 @@ export async function loadPackagesFromImports(
122
122
  }
123
123
 
124
124
  /**
125
- * Runs Python code using `PyCF_ALLOW_TOP_LEVEL_AWAIT
126
- * <https://docs.python.org/3/library/ast.html?highlight=pycf_allow_top_level_await#ast.PyCF_ALLOW_TOP_LEVEL_AWAIT>`_.
125
+ * Run a Python code string with top level await using
126
+ * :any:`pyodide.eval_code_async` to evaluate the code. Returns a promise which
127
+ * resolves when execution completes. If the last statement in the Python code
128
+ * is an expression (and the code doesn't end with a semicolon), the returned
129
+ * promise will resolve to the value of this expression.
127
130
  *
128
131
  * For example:
129
132
  *
@@ -138,13 +141,15 @@ export async function loadPackagesFromImports(
138
141
  * `);
139
142
  * console.log(result); // 79
140
143
  *
141
- * .. admonition:: Python imports :class: warning
144
+ * .. admonition:: Python imports
145
+ * :class: warning
142
146
  *
143
147
  * Since pyodide 0.18.0, you must call :js:func:`loadPackagesFromImports` to
144
148
  * import any python packages referenced via `import` statements in your
145
149
  * code. This function will no longer do it for you.
146
150
  *
147
- * .. admonition:: Positional globals argument :class: warning
151
+ * .. admonition:: Positional globals argument
152
+ * :class: warning
148
153
  *
149
154
  * In Pyodide v0.19, this function took the globals parameter as a
150
155
  * positional argument rather than as a named argument. In v0.20 this will
@@ -153,8 +158,7 @@ export async function loadPackagesFromImports(
153
158
  * @param code Python code to evaluate
154
159
  * @param options
155
160
  * @param options.globals An optional Python dictionary to use as the globals.
156
- * Defaults to :any:`pyodide.globals`. Uses the Python API
157
- * :any:`pyodide.eval_code_async` to evaluate the code.
161
+ * Defaults to :any:`pyodide.globals`.
158
162
  * @returns The result of the Python code translated to JavaScript.
159
163
  * @async
160
164
  */
@@ -166,7 +170,7 @@ export async function runPythonAsync(
166
170
  options = { globals: options as PyProxy };
167
171
  if (!runPythonPositionalGlobalsDeprecationWarned) {
168
172
  console.warn(
169
- "Passing a PyProxy as the second argument to runPython is deprecated. Use 'runPythonAsync(code, {globals : some_dict})' instead."
173
+ "Passing a PyProxy as the second argument to runPythonAsync is deprecated and will be removed in v0.21. Use 'runPythonAsync(code, {globals : some_dict})' instead."
170
174
  );
171
175
  runPythonPositionalGlobalsDeprecationWarned = true;
172
176
  }
@@ -322,25 +326,46 @@ export function pyimport(mod_name: string): PyProxy {
322
326
  return API.importlib.import_module(mod_name);
323
327
  }
324
328
 
329
+ let unpackArchivePositionalExtractDirDeprecationWarned = false;
325
330
  /**
326
331
  * Unpack an archive into a target directory.
327
332
  *
333
+ * .. admonition:: Positional globals argument :class: warning
334
+ *
335
+ * In Pyodide v0.19, this function took the extract_dir parameter as a
336
+ * positional argument rather than as a named argument. In v0.20 this will
337
+ * still work but it is deprecated. It will be removed in v0.21.
338
+ *
328
339
  * @param buffer The archive as an ArrayBuffer or TypedArray.
329
- * @param format The format of the archive. Should be one of the formats recognized by `shutil.unpack_archive`.
330
- * By default the options are 'bztar', 'gztar', 'tar', 'zip', and 'wheel'. Several synonyms are accepted for each format, e.g.,
331
- * for 'gztar' any of '.gztar', '.tar.gz', '.tgz', 'tar.gz' or 'tgz' are considered to be synonyms.
340
+ * @param format The format of the archive. Should be one of the formats
341
+ * recognized by `shutil.unpack_archive`. By default the options are 'bztar',
342
+ * 'gztar', 'tar', 'zip', and 'wheel'. Several synonyms are accepted for each
343
+ * format, e.g., for 'gztar' any of '.gztar', '.tar.gz', '.tgz', 'tar.gz' or
344
+ * 'tgz' are considered to be synonyms.
332
345
  *
333
- * @param extract_dir The directory to unpack the archive into. Defaults to the working directory.
346
+ * @param options
347
+ * @param options.extractDir The directory to unpack the archive into. Defaults
348
+ * to the working directory.
334
349
  */
335
350
  export function unpackArchive(
336
351
  buffer: TypedArray,
337
352
  format: string,
338
- extract_dir?: string
353
+ options: {
354
+ extractDir?: string;
355
+ } = {}
339
356
  ) {
340
- if (!API._util_module) {
341
- API._util_module = pyimport("pyodide._util");
357
+ if (typeof options === "string") {
358
+ if (!unpackArchivePositionalExtractDirDeprecationWarned) {
359
+ console.warn(
360
+ "Passing a string as the third argument to unpackArchive is deprecated and will be removed in v0.21. Instead use { extract_dir : 'some_path' }"
361
+ );
362
+ unpackArchivePositionalExtractDirDeprecationWarned = true;
363
+ }
364
+ options = { extractDir: options };
342
365
  }
343
- API._util_module.unpack_buffer_archive.callKwargs(buffer, {
366
+ let extract_dir = options.extractDir;
367
+ API.package_loader.unpack_buffer.callKwargs({
368
+ buffer,
344
369
  format,
345
370
  extract_dir,
346
371
  });
@@ -357,15 +382,28 @@ API.saveState = () => API.pyodide_py._state.save_state();
357
382
  API.restoreState = (state: any) => API.pyodide_py._state.restore_state(state);
358
383
 
359
384
  /**
360
- * Sets the interrupt buffer to be `interrupt_buffer`. This is only useful when
361
- * Pyodide is used in a webworker. The buffer should be a `SharedArrayBuffer`
362
- * shared with the main browser thread (or another worker). To request an
363
- * interrupt, a `2` should be written into `interrupt_buffer` (2 is the posix
364
- * constant for SIGINT).
385
+ * Sets the interrupt buffer to be ``interrupt_buffer``. This is only useful
386
+ * when Pyodide is used in a webworker. The buffer should be a
387
+ * ``SharedArrayBuffer`` shared with the main browser thread (or another
388
+ * worker). In that case, signal ``signum`` may be sent by writing ``signum``
389
+ * into the interrupt buffer. If ``signum`` does not satisfy 0 < ``signum`` <
390
+ * ``NSIG`` it will be silently ignored. NSIG is 65 (internally signals are
391
+ * indicated by a bitflag).
392
+ *
393
+ * You can disable interrupts by calling `setInterruptBuffer(undefined)`.
394
+ *
395
+ * If you wish to trigger a ``KeyboardInterrupt``, write ``SIGINT`` (a 2), into
396
+ * the interrupt buffer.
397
+ *
398
+ * By default ``SIGINT`` raises a ``KeyboardInterrupt`` and all other signals
399
+ * are ignored. You can install custom signal handlers with the signal module.
400
+ * Even signals that normally have special meaning and can't be overridden like
401
+ * ``SIGKILL`` and ``SIGSEGV`` are ignored by default and can be used for any
402
+ * purpose you like.
365
403
  */
366
404
  export function setInterruptBuffer(interrupt_buffer: TypedArray) {
367
- API.interrupt_buffer = interrupt_buffer;
368
- Module._set_pyodide_callback(!!interrupt_buffer);
405
+ Module.HEAP8[Module._Py_EMSCRIPTEN_SIGNAL_HANDLING] = !!interrupt_buffer;
406
+ Module.Py_EmscriptenSignalBuffer = interrupt_buffer;
369
407
  }
370
408
 
371
409
  /**
@@ -377,10 +415,8 @@ export function setInterruptBuffer(interrupt_buffer: TypedArray) {
377
415
  * during execution of C code.
378
416
  */
379
417
  export function checkInterrupt() {
380
- if (API.interrupt_buffer[0] === 2) {
381
- API.interrupt_buffer[0] = 0;
382
- Module._PyErr_SetInterrupt();
383
- API.runPython("");
418
+ if (Module.__PyErr_CheckSignals()) {
419
+ Module._pythonexc2js();
384
420
  }
385
421
  }
386
422
 
@@ -11,6 +11,38 @@ API.dump_traceback = function () {
11
11
  Module.__Py_DumpTraceback(fd_stdout, Module._PyGILState_GetThisThreadState());
12
12
  };
13
13
 
14
+ function ensureCaughtObjectIsError(e: any) {
15
+ if (typeof e === "string") {
16
+ // Sometimes emscripten throws a raw string...
17
+ e = new Error(e);
18
+ } else if (
19
+ typeof e !== "object" ||
20
+ e === null ||
21
+ typeof e.stack !== "string" ||
22
+ typeof e.message !== "string"
23
+ ) {
24
+ // We caught something really weird. Be brave!
25
+ let msg = `A value of type ${typeof e} with tag ${Object.prototype.toString.call(
26
+ e
27
+ )} was thrown as an error!`;
28
+ try {
29
+ msg += `\nString interpolation of the thrown value gives """${e}""".`;
30
+ } catch (e) {
31
+ msg += `\nString interpolation of the thrown value fails.`;
32
+ }
33
+ try {
34
+ msg += `\nThe thrown value's toString method returns """${e.toString()}""".`;
35
+ } catch (e) {
36
+ msg += `\nThe thrown value's toString method fails.`;
37
+ }
38
+ e = new Error(msg);
39
+ }
40
+ // Post conditions:
41
+ // 1. typeof e is object
42
+ // 2. hiwire_is_error(e) returns true
43
+ return e;
44
+ }
45
+
14
46
  let fatal_error_occurred = false;
15
47
  /**
16
48
  * Signal a fatal error.
@@ -25,7 +57,7 @@ let fatal_error_occurred = false;
25
57
  * @private
26
58
  */
27
59
  API.fatal_error = function (e: any) {
28
- if (e.pyodide_fatal_error) {
60
+ if (e && e.pyodide_fatal_error) {
29
61
  return;
30
62
  }
31
63
  if (fatal_error_occurred) {
@@ -34,14 +66,10 @@ API.fatal_error = function (e: any) {
34
66
  return;
35
67
  }
36
68
  if (typeof e === "number") {
37
- // A C++ exception. Have to do some conversion work.
69
+ // Hopefully a C++ exception? Have to do some conversion work.
38
70
  e = convertCppException(e);
39
- } else if (typeof e === "string") {
40
- e = new Error(e);
41
- } else if (typeof e !== "object") {
42
- e = new Error(
43
- `An object of type ${typeof e} was thrown as an error. toString returns ${e.toString()}`
44
- );
71
+ } else {
72
+ e = ensureCaughtObjectIsError(e);
45
73
  }
46
74
  // Mark e so we know not to handle it later in EM_JS wrappers
47
75
  e.pyodide_fatal_error = true;
@@ -193,7 +221,7 @@ function isErrorStart(frame: ErrorStackParser.StackFrame): boolean {
193
221
  }
194
222
 
195
223
  Module.handle_js_error = function (e: any) {
196
- if (e.pyodide_fatal_error) {
224
+ if (e && e.pyodide_fatal_error) {
197
225
  throw e;
198
226
  }
199
227
  if (e instanceof Module._PropagatePythonError) {
@@ -207,6 +235,16 @@ Module.handle_js_error = function (e: any) {
207
235
  // Try to restore the original Python exception.
208
236
  restored_error = Module._restore_sys_last_exception(e.__error_address);
209
237
  }
238
+ let stack: any;
239
+ let weirdCatch;
240
+ try {
241
+ stack = ErrorStackParser.parse(e);
242
+ } catch (_) {
243
+ weirdCatch = true;
244
+ }
245
+ if (weirdCatch) {
246
+ e = ensureCaughtObjectIsError(e);
247
+ }
210
248
  if (!restored_error) {
211
249
  // Wrap the JavaScript error
212
250
  let eidx = Hiwire.new_value(e);
@@ -215,7 +253,10 @@ Module.handle_js_error = function (e: any) {
215
253
  Module._Py_DecRef(err);
216
254
  Hiwire.decref(eidx);
217
255
  }
218
- let stack = ErrorStackParser.parse(e);
256
+ if (weirdCatch) {
257
+ // In this case we have no stack frames so we can quit
258
+ return;
259
+ }
219
260
  if (isErrorStart(stack[0])) {
220
261
  while (isPyodideFrame(stack[0])) {
221
262
  stack.shift();
package/index.test-d.ts CHANGED
@@ -159,4 +159,9 @@ async function main() {
159
159
  expectAssignable<{ done?: any; value: any }>(px.next());
160
160
  expectAssignable<{ done?: any; value: any }>(px.next(22));
161
161
  }
162
+
163
+ pyodide.unpackArchive(new Uint8Array(40), "tar");
164
+ pyodide.unpackArchive(new Uint8Array(40), "tar", {
165
+ extractDir: "/some/path",
166
+ });
162
167
  }
package/load-package.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Module, API } from "./module.js";
1
+ import { Module, API, Tests } from "./module.js";
2
2
  import { IN_NODE, nodeFsPromisesMod, _loadBinaryFile } from "./compat.js";
3
3
  import { PyProxy, isPyProxy } from "./pyproxy.gen";
4
4
 
@@ -171,13 +171,14 @@ async function installPackage(name: string, buffer: Uint8Array) {
171
171
  imports: [] as string[],
172
172
  };
173
173
  }
174
- const file_name = pkg.file_name;
174
+ const filename = pkg.file_name;
175
175
  // This Python helper function unpacks the buffer and lists out any so files therein.
176
- const dynlibs = API.package_loader.unpack_buffer(
177
- file_name,
176
+ const dynlibs = API.package_loader.unpack_buffer.callKwargs({
178
177
  buffer,
179
- pkg.install_dir
180
- );
178
+ filename,
179
+ target: pkg.install_dir,
180
+ calculate_dynlibs: true,
181
+ });
181
182
  for (const dynlib of dynlibs) {
182
183
  await loadDynlib(dynlib, pkg.shared_library);
183
184
  }
@@ -248,10 +249,19 @@ async function loadDynlib(lib: string, shared: boolean) {
248
249
  nodelete: true,
249
250
  });
250
251
  }
252
+ } catch (e) {
253
+ if (e.message.includes("need to see wasm magic number")) {
254
+ console.warn(
255
+ `Failed to load dynlib ${lib}. We probably just tried to load a linux .so file or something.`
256
+ );
257
+ return;
258
+ }
259
+ throw e;
251
260
  } finally {
252
261
  releaseDynlibLock();
253
262
  }
254
263
  }
264
+ Tests.loadDynlib = loadDynlib;
255
265
 
256
266
  const acquirePackageLock = createLock();
257
267
 
@@ -353,6 +363,7 @@ export async function loadPackage(
353
363
  loadedPackages[name] = channel;
354
364
  })
355
365
  .catch((err) => {
366
+ console.warn(err);
356
367
  failed[name] = err;
357
368
  });
358
369
  }
@@ -366,6 +377,7 @@ export async function loadPackage(
366
377
  loadedPackages[name] = channel;
367
378
  })
368
379
  .catch((err) => {
380
+ console.warn(err);
369
381
  failed[name] = err;
370
382
  });
371
383
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyodide",
3
- "version": "0.20.0-alpha.1",
3
+ "version": "0.20.0",
4
4
  "description": "The Pyodide JavaScript package",
5
5
  "keywords": [
6
6
  "python",
package/pyproxy.gen.ts CHANGED
@@ -21,6 +21,90 @@
21
21
 
22
22
 
23
23
 
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+
86
+
87
+
88
+
89
+
90
+
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+
101
+
102
+
103
+
104
+
105
+
106
+
107
+
24
108
  /**
25
109
  * Every public Python entrypoint goes through this file! The main entrypoint is
26
110
  * the callPyObject method, but of course one can also execute arbitrary code
@@ -289,7 +373,12 @@ Module.callPyObjectKwargs = function (ptrobj: number, ...jsargs: any) {
289
373
  if (idresult === 0) {
290
374
  Module._pythonexc2js();
291
375
  }
292
- return Hiwire.pop_value(idresult);
376
+ let result = Hiwire.pop_value(idresult);
377
+ // Automatically schedule coroutines
378
+ if (result && result.type === "coroutine" && result._ensure_future) {
379
+ result._ensure_future();
380
+ }
381
+ return result;
293
382
  };
294
383
  Module.callPyObject = function (ptrobj: number, ...jsargs: any) {
295
384
  return Module.callPyObjectKwargs(ptrobj, ...jsargs, {});