pyodide 0.18.0 → 0.19.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/pyodide.js CHANGED
@@ -1,17 +1,16 @@
1
1
  /**
2
2
  * The main bootstrap code for loading pyodide.
3
3
  */
4
- import { Module, setStandardStreams } from "./module.js";
4
+ import { Module, setStandardStreams, setHomeDirectory } from "./module.js";
5
5
  import {
6
6
  loadScript,
7
7
  initializePackageIndex,
8
+ _fetchBinaryFile,
8
9
  loadPackage,
9
10
  } from "./load-pyodide.js";
10
11
  import { makePublicAPI, registerJsModule } from "./api.js";
11
12
  import "./pyproxy.gen.js";
12
13
 
13
- import { wrapNamespace } from "./pyproxy.gen.js";
14
-
15
14
  /**
16
15
  * @typedef {import('./pyproxy.gen').PyProxy} PyProxy
17
16
  * @typedef {import('./pyproxy.gen').PyProxyWithLength} PyProxyWithLength
@@ -36,7 +35,7 @@ import { wrapNamespace } from "./pyproxy.gen.js";
36
35
  * @private
37
36
  */
38
37
  Module.dump_traceback = function () {
39
- let fd_stdout = 1;
38
+ const fd_stdout = 1;
40
39
  Module.__Py_DumpTraceback(fd_stdout, Module._PyGILState_GetThisThreadState());
41
40
  };
42
41
 
@@ -44,21 +43,26 @@ let fatal_error_occurred = false;
44
43
  /**
45
44
  * Signal a fatal error.
46
45
  *
47
- * Dumps the Python traceback, shows a Javascript traceback, and prints a clear
46
+ * Dumps the Python traceback, shows a JavaScript traceback, and prints a clear
48
47
  * message indicating a fatal error. It then dummies out the public API so that
49
48
  * further attempts to use Pyodide will clearly indicate that Pyodide has failed
50
- * and can no longer be used. pyodide._module is left accessible and it is
49
+ * and can no longer be used. pyodide._module is left accessible, and it is
51
50
  * possible to continue using Pyodide for debugging purposes if desired.
52
51
  *
53
52
  * @argument e {Error} The cause of the fatal error.
54
53
  * @private
55
54
  */
56
55
  Module.fatal_error = function (e) {
56
+ if (e.pyodide_fatal_error) {
57
+ return;
58
+ }
57
59
  if (fatal_error_occurred) {
58
60
  console.error("Recursive call to fatal_error. Inner error was:");
59
61
  console.error(e);
60
62
  return;
61
63
  }
64
+ // Mark e so we know not to handle it later in EM_JS wrappers
65
+ e.pyodide_fatal_error = true;
62
66
  fatal_error_occurred = true;
63
67
  console.error(
64
68
  "Pyodide has suffered a fatal error. Please report this to the Pyodide maintainers."
@@ -98,128 +102,179 @@ Module.fatal_error = function (e) {
98
102
  throw e;
99
103
  };
100
104
 
105
+ let runPythonInternal_dict; // Initialized in finalizeBootstrap
101
106
  /**
102
- * Run Python code in the simplest way possible. The primary purpose of this
103
- * method is for bootstrapping. It is also useful for debugging: If the Python
104
- * interpreter is initialized successfully then it should be possible to use
105
- * this method to run Python code even if everything else in the Pyodide
106
- * `core` module fails.
107
- *
108
- * The differences are:
109
- * 1. `runPythonSimple` doesn't return anything (and so won't leak
110
- * PyProxies)
111
- * 2. `runPythonSimple` doesn't require access to any state on the
112
- * Javascript `pyodide` module.
113
- * 3. `runPython` uses `pyodide.eval_code`, whereas `runPythonSimple` uses
114
- * `PyRun_String` which is the C API for `eval` / `exec`.
115
- * 4. `runPythonSimple` runs with `globals` a separate dict which is called
116
- * `init_dict` (keeps global state private)
117
- * 5. `runPythonSimple` doesn't dedent the argument
118
- *
119
- * When `core` initialization is completed, the globals for `runPythonSimple`
120
- * is made available as `Module.init_dict`.
121
- *
107
+ * Just like `runPython` except uses a different globals dict and gets
108
+ * `eval_code` from `_pyodide` so that it can work before `pyodide` is imported.
122
109
  * @private
123
110
  */
124
- Module.runPythonSimple = function (code) {
125
- let code_c_string = Module.stringToNewUTF8(code);
126
- let errcode;
127
- try {
128
- errcode = Module._run_python_simple_inner(code_c_string);
129
- } catch (e) {
130
- Module.fatal_error(e);
131
- } finally {
132
- Module._free(code_c_string);
133
- }
134
- if (errcode === -1) {
135
- Module._pythonexc2js();
136
- }
111
+ Module.runPythonInternal = function (code) {
112
+ return Module._pyodide._base.eval_code(code, runPythonInternal_dict);
137
113
  };
138
114
 
139
115
  /**
140
- * The Javascript/Wasm call stack is too small to handle the default Python call
141
- * stack limit of 1000 frames. Here, we determine the Javascript call stack
142
- * depth available, and then divide by 50 (determined heuristically) to set the
143
- * maximum Python call stack depth.
144
- *
116
+ * A proxy around globals that falls back to checking for a builtin if has or
117
+ * get fails to find a global with the given key. Note that this proxy is
118
+ * transparent to js2python: it won't notice that this wrapper exists at all and
119
+ * will translate this proxy to the globals dictionary.
145
120
  * @private
146
121
  */
147
- function fixRecursionLimit() {
148
- let depth = 0;
149
- function recurse() {
150
- depth += 1;
151
- recurse();
152
- }
153
- try {
154
- recurse();
155
- } catch (err) {}
122
+ function wrapPythonGlobals(globals_dict, builtins_dict) {
123
+ return new Proxy(globals_dict, {
124
+ get(target, symbol) {
125
+ if (symbol === "get") {
126
+ return (key) => {
127
+ let result = target.get(key);
128
+ if (result === undefined) {
129
+ result = builtins_dict.get(key);
130
+ }
131
+ return result;
132
+ };
133
+ }
134
+ if (symbol === "has") {
135
+ return (key) => target.has(key) || builtins_dict.has(key);
136
+ }
137
+ return Reflect.get(target, symbol);
138
+ },
139
+ });
140
+ }
156
141
 
157
- let recursionLimit = Math.min(depth / 25, 500);
158
- Module.runPythonSimple(
159
- `import sys; sys.setrecursionlimit(int(${recursionLimit}))`
142
+ function unpackPyodidePy(pyodide_py_tar) {
143
+ const fileName = "/pyodide_py.tar";
144
+ let stream = Module.FS.open(fileName, "w");
145
+ Module.FS.write(
146
+ stream,
147
+ new Uint8Array(pyodide_py_tar),
148
+ 0,
149
+ pyodide_py_tar.byteLength,
150
+ undefined,
151
+ true
160
152
  );
153
+ Module.FS.close(stream);
154
+ const code_ptr = Module.stringToNewUTF8(`
155
+ import shutil
156
+ shutil.unpack_archive("/pyodide_py.tar", "/lib/python3.9/site-packages/")
157
+ del shutil
158
+ import importlib
159
+ importlib.invalidate_caches()
160
+ del importlib
161
+ `);
162
+ let errcode = Module._PyRun_SimpleString(code_ptr);
163
+ if (errcode) {
164
+ throw new Error("OOPS!");
165
+ }
166
+ Module._free(code_ptr);
167
+ Module.FS.unlink(fileName);
168
+ }
169
+
170
+ /**
171
+ * This function is called after the emscripten module is finished initializing,
172
+ * so eval_code is newly available.
173
+ * It finishes the bootstrap so that once it is complete, it is possible to use
174
+ * the core `pyodide` apis. (But package loading is not ready quite yet.)
175
+ * @private
176
+ */
177
+ function finalizeBootstrap(config) {
178
+ // First make internal dict so that we can use runPythonInternal.
179
+ // runPythonInternal uses a separate namespace, so we don't pollute the main
180
+ // environment with variables from our setup.
181
+ runPythonInternal_dict = Module._pyodide._base.eval_code("{}");
182
+ Module.importlib = Module.runPythonInternal("import importlib; importlib");
183
+ let import_module = Module.importlib.import_module;
184
+
185
+ Module.sys = import_module("sys");
186
+ Module.sys.path.insert(0, config.homedir);
187
+
188
+ // Set up globals
189
+ let globals = Module.runPythonInternal("import __main__; __main__.__dict__");
190
+ let builtins = Module.runPythonInternal("import builtins; builtins.__dict__");
191
+ Module.globals = wrapPythonGlobals(globals, builtins);
192
+
193
+ // Set up key Javascript modules.
194
+ let importhook = Module._pyodide._importhook;
195
+ importhook.register_js_finder();
196
+ importhook.register_js_module("js", config.jsglobals);
197
+
198
+ let pyodide = makePublicAPI();
199
+ importhook.register_js_module("pyodide_js", pyodide);
200
+
201
+ // import pyodide_py. We want to ensure that as much stuff as possible is
202
+ // already set up before importing pyodide_py to simplify development of
203
+ // pyodide_py code (Otherwise it's very hard to keep track of which things
204
+ // aren't set up yet.)
205
+ Module.pyodide_py = import_module("pyodide");
206
+ Module.version = Module.pyodide_py.__version__;
207
+
208
+ // copy some last constants onto public API.
209
+ pyodide.pyodide_py = Module.pyodide_py;
210
+ pyodide.version = Module.version;
211
+ pyodide.globals = Module.globals;
212
+ return pyodide;
161
213
  }
214
+
162
215
  /**
163
216
  * Load the main Pyodide wasm module and initialize it.
164
217
  *
165
- * Only one copy of Pyodide can be loaded in a given Javascript global scope
218
+ * Only one copy of Pyodide can be loaded in a given JavaScript global scope
166
219
  * because Pyodide uses global variables to load packages. If an attempt is made
167
220
  * to load a second copy of Pyodide, :any:`loadPyodide` will throw an error.
168
221
  * (This can be fixed once `Firefox adopts support for ES6 modules in webworkers
169
222
  * <https://bugzilla.mozilla.org/show_bug.cgi?id=1247687>`_.)
170
223
  *
171
- * @param {{ indexURL : string, fullStdLib? : boolean = true, stdin?: () => string, stdout?: (text: string) => void, stderr?: (text: string) => void }} config
172
224
  * @param {string} config.indexURL - The URL from which Pyodide will load
173
225
  * packages
226
+ * @param {string} config.homedir - The home directory which Pyodide will use inside virtual file system
227
+ * Default: /home/pyodide
174
228
  * @param {boolean} config.fullStdLib - Load the full Python standard library.
175
229
  * Setting this to false excludes following modules: distutils.
176
230
  * Default: true
177
- * @param {undefined | (() => string)} config.stdin - Override the standard input callback. Should ask the user for one line of input.
231
+ * @param {undefined | function(): string} config.stdin - Override the standard input callback. Should ask the user for one line of input.
178
232
  * Default: undefined
179
- * @param {undefined | ((text: string) => void)} config.stdout - Override the standard output callback.
233
+ * @param {undefined | function(string)} config.stdout - Override the standard output callback.
180
234
  * Default: undefined
181
- * @param {undefined | ((text: string) => void)} config.stderr - Override the standard error output callback.
235
+ * @param {undefined | function(string)} config.stderr - Override the standard error output callback.
182
236
  * Default: undefined
183
237
  * @returns The :ref:`js-api-pyodide` module.
184
238
  * @memberof globalThis
185
239
  * @async
186
240
  */
187
241
  export async function loadPyodide(config) {
242
+ if (globalThis.__pyodide_module) {
243
+ throw new Error("Pyodide is already loading.");
244
+ }
245
+ if (!config.indexURL) {
246
+ throw new Error("Please provide indexURL parameter to loadPyodide");
247
+ }
248
+
249
+ loadPyodide.inProgress = true;
250
+ // A global "mount point" for the package loaders to talk to pyodide
251
+ // See "--export-name=__pyodide_module" in buildpkg.py
252
+ globalThis.__pyodide_module = Module;
253
+
188
254
  const default_config = {
189
255
  fullStdLib: true,
190
256
  jsglobals: globalThis,
191
257
  stdin: globalThis.prompt ? globalThis.prompt : undefined,
258
+ homedir: "/home/pyodide",
192
259
  };
193
260
  config = Object.assign(default_config, config);
194
- if (globalThis.__pyodide_module) {
195
- if (globalThis.languagePluginURL) {
196
- throw new Error(
197
- "Pyodide is already loading because languagePluginURL is defined."
198
- );
199
- } else {
200
- throw new Error("Pyodide is already loading.");
201
- }
202
- }
203
- // A global "mount point" for the package loaders to talk to pyodide
204
- // See "--export-name=__pyodide_module" in buildpkg.py
205
- globalThis.__pyodide_module = Module;
206
- loadPyodide.inProgress = true;
207
- if (!config.indexURL) {
208
- throw new Error("Please provide indexURL parameter to loadPyodide");
209
- }
210
- let baseURL = config.indexURL;
211
- if (!baseURL.endsWith("/")) {
212
- baseURL += "/";
261
+
262
+ if (!config.indexURL.endsWith("/")) {
263
+ config.indexURL += "/";
213
264
  }
214
- Module.indexURL = baseURL;
215
- let packageIndexReady = initializePackageIndex(baseURL);
265
+ Module.indexURL = config.indexURL;
266
+ let packageIndexReady = initializePackageIndex(config.indexURL);
267
+ let pyodide_py_tar_promise = _fetchBinaryFile(
268
+ config.indexURL,
269
+ "pyodide_py.tar"
270
+ );
216
271
 
217
272
  setStandardStreams(config.stdin, config.stdout, config.stderr);
273
+ setHomeDirectory(config.homedir);
218
274
 
219
- Module.locateFile = (path) => baseURL + path;
220
275
  let moduleLoaded = new Promise((r) => (Module.postRun = r));
221
276
 
222
- const scriptSrc = `${baseURL}pyodide.asm.js`;
277
+ const scriptSrc = `${config.indexURL}pyodide.asm.js`;
223
278
  await loadScript(scriptSrc);
224
279
 
225
280
  // _createPyodideModule is specified in the Makefile by the linker flag:
@@ -230,85 +285,18 @@ export async function loadPyodide(config) {
230
285
  // being called.
231
286
  await moduleLoaded;
232
287
 
233
- fixRecursionLimit();
234
- let pyodide = makePublicAPI();
235
-
236
- // Bootstrap steps:
237
- //
238
- // 1. _pyodide_core is ready now so we can call _pyodide.register_js_finder
239
- // 2. Use the jsfinder to register the js and pyodide_js packages
240
- // 3. Import pyodide, this requires _pyodide_core, js and pyodide_js to be
241
- // ready.
242
- // 4. Add the pyodide_py and Python __main__.__dict__ objects to pyodide_js
243
- Module.runPythonSimple(`
244
- def temp(pyodide_js, Module, jsglobals):
245
- from _pyodide._importhook import register_js_finder
246
- jsfinder = register_js_finder()
247
- jsfinder.register_js_module("js", jsglobals)
248
- jsfinder.register_js_module("pyodide_js", pyodide_js)
249
-
250
- import pyodide
251
- import __main__
252
- import builtins
253
-
254
- globals = __main__.__dict__
255
- globals.update(builtins.__dict__)
256
-
257
- Module.version = pyodide.__version__
258
- Module.globals = globals
259
- Module.builtins = builtins.__dict__
260
- Module.pyodide_py = pyodide
261
- print("Python initialization complete")
262
- `);
263
-
264
- Module.init_dict.get("temp")(pyodide, Module, config.jsglobals);
265
- // Module.runPython works starting from here!
266
-
267
- // Wrap "globals" in a special Proxy that allows `pyodide.globals.x` access.
268
- // TODO: Should we have this?
269
- Module.globals = wrapNamespace(Module.globals);
288
+ const pyodide_py_tar = await pyodide_py_tar_promise;
289
+ unpackPyodidePy(pyodide_py_tar);
290
+ Module._pyodide_init();
270
291
 
271
- pyodide.globals = Module.globals;
272
- pyodide.pyodide_py = Module.pyodide_py;
273
- pyodide.version = Module.version;
292
+ let pyodide = finalizeBootstrap(config);
293
+ // Module.runPython works starting here.
274
294
 
275
295
  await packageIndexReady;
276
296
  if (config.fullStdLib) {
277
297
  await loadPackage(["distutils"]);
278
298
  }
279
-
299
+ pyodide.runPython("print('Python initialization complete')");
280
300
  return pyodide;
281
301
  }
282
302
  globalThis.loadPyodide = loadPyodide;
283
-
284
- if (globalThis.languagePluginUrl) {
285
- console.warn(
286
- "languagePluginUrl is deprecated and will be removed in version 0.18.0, " +
287
- "instead use loadPyodide({ indexURL : <some_url>})"
288
- );
289
-
290
- /**
291
- * A deprecated parameter that specifies the Pyodide ``indexURL``. If present,
292
- * Pyodide will automatically invoke
293
- * ``loadPyodide({indexURL : languagePluginUrl})``
294
- * and will store the resulting promise in
295
- * :any:`globalThis.languagePluginLoader`. Use :any:`loadPyodide`
296
- * directly instead of defining this.
297
- *
298
- * @type String
299
- * @deprecated Will be removed in version 0.18.0
300
- */
301
- globalThis.languagePluginUrl;
302
-
303
- /**
304
- * A deprecated promise that resolves to ``undefined`` when Pyodide is
305
- * finished loading. Only created if :any:`languagePluginUrl` is
306
- * defined. Instead use :any:`loadPyodide`.
307
- *
308
- * @type Promise
309
- * @deprecated Will be removed in version 0.18.0
310
- */
311
- globalThis.languagePluginLoader = loadPyodide({
312
- indexURL: globalThis.languagePluginUrl,
313
- }).then((pyodide) => (self.pyodide = pyodide));
314
- }