pyodide 0.19.0 → 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.
@@ -0,0 +1,414 @@
1
+ import { Module, API, Tests } from "./module.js";
2
+ import { IN_NODE, nodeFsPromisesMod, _loadBinaryFile } from "./compat.js";
3
+ import { PyProxy, isPyProxy } from "./pyproxy.gen";
4
+
5
+ /** @private */
6
+ let baseURL: string;
7
+ /**
8
+ * Initialize the packages index. This is called as early as possible in
9
+ * loadPyodide so that fetching packages.json can occur in parallel with other
10
+ * operations.
11
+ * @param indexURL
12
+ * @private
13
+ */
14
+ export async function initializePackageIndex(indexURL: string) {
15
+ baseURL = indexURL;
16
+ let package_json;
17
+ if (IN_NODE) {
18
+ const package_string = await nodeFsPromisesMod.readFile(
19
+ `${indexURL}packages.json`
20
+ );
21
+ package_json = JSON.parse(package_string);
22
+ } else {
23
+ let response = await fetch(`${indexURL}packages.json`);
24
+ package_json = await response.json();
25
+ }
26
+ if (!package_json.packages) {
27
+ throw new Error(
28
+ "Loaded packages.json does not contain the expected key 'packages'."
29
+ );
30
+ }
31
+ API.packages = package_json.packages;
32
+
33
+ // compute the inverted index for imports to package names
34
+ API._import_name_to_package_name = new Map();
35
+ for (let name of Object.keys(API.packages)) {
36
+ for (let import_name of API.packages[name].imports) {
37
+ API._import_name_to_package_name.set(import_name, name);
38
+ }
39
+ }
40
+ }
41
+
42
+ //
43
+ // Dependency resolution
44
+ //
45
+ const DEFAULT_CHANNEL = "default channel";
46
+ // Regexp for validating package name and URI
47
+ const package_uri_regexp = /^.*?([^\/]*)\.whl$/;
48
+
49
+ function _uri_to_package_name(package_uri: string): string | undefined {
50
+ let match = package_uri_regexp.exec(package_uri);
51
+ if (match) {
52
+ let wheel_name = match[1].toLowerCase();
53
+ return wheel_name.split("-").slice(0, -4).join("-");
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Recursively add a package and its dependencies to toLoad and toLoadShared.
59
+ * A helper function for recursiveDependencies.
60
+ * @param name The package to add
61
+ * @param toLoad The set of names of packages to load
62
+ * @param toLoadShared The set of names of shared libraries to load
63
+ * @private
64
+ */
65
+ function addPackageToLoad(
66
+ name: string,
67
+ toLoad: Map<string, string>,
68
+ toLoadShared: Map<string, string>
69
+ ) {
70
+ name = name.toLowerCase();
71
+ if (toLoad.has(name)) {
72
+ return;
73
+ }
74
+ const pkg_info = API.packages[name];
75
+ if (!pkg_info) {
76
+ throw new Error(`No known package with name '${name}'`);
77
+ }
78
+ if (pkg_info.shared_library) {
79
+ toLoadShared.set(name, DEFAULT_CHANNEL);
80
+ } else {
81
+ toLoad.set(name, DEFAULT_CHANNEL);
82
+ }
83
+ // If the package is already loaded, we don't add dependencies, but warn
84
+ // the user later. This is especially important if the loaded package is
85
+ // from a custom url, in which case adding dependencies is wrong.
86
+ if (loadedPackages[name] !== undefined) {
87
+ return;
88
+ }
89
+
90
+ for (let dep_name of pkg_info.depends) {
91
+ addPackageToLoad(dep_name, toLoad, toLoadShared);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Calculate the dependencies of a set of packages
97
+ * @param names The list of names whose dependencies we need to calculate.
98
+ * @returns Two sets, the set of normal dependencies and the set of shared
99
+ * dependencies
100
+ * @private
101
+ */
102
+ function recursiveDependencies(
103
+ names: string[],
104
+ errorCallback: (err: string) => void
105
+ ) {
106
+ const toLoad = new Map();
107
+ const toLoadShared = new Map();
108
+ for (let name of names) {
109
+ const pkgname = _uri_to_package_name(name);
110
+ if (pkgname === undefined) {
111
+ addPackageToLoad(name, toLoad, toLoadShared);
112
+ continue;
113
+ }
114
+ if (toLoad.has(pkgname) && toLoad.get(pkgname) !== name) {
115
+ errorCallback(
116
+ `Loading same package ${pkgname} from ${name} and ${toLoad.get(
117
+ pkgname
118
+ )}`
119
+ );
120
+ continue;
121
+ }
122
+ toLoad.set(pkgname, name);
123
+ }
124
+ return [toLoad, toLoadShared];
125
+ }
126
+
127
+ //
128
+ // Dependency download and install
129
+ //
130
+
131
+ /**
132
+ * Download a package. If `channel` is `DEFAULT_CHANNEL`, look up the wheel URL
133
+ * relative to baseURL from `packages.json`, otherwise use the URL specified by
134
+ * `channel`.
135
+ * @param name The name of the package
136
+ * @param channel Either `DEFAULT_CHANNEL` or the absolute URL to the
137
+ * wheel or the path to the wheel relative to baseURL.
138
+ * @returns The binary data for the package
139
+ * @private
140
+ */
141
+ async function downloadPackage(
142
+ name: string,
143
+ channel: string
144
+ ): Promise<Uint8Array> {
145
+ let file_name;
146
+ if (channel === DEFAULT_CHANNEL) {
147
+ if (!(name in API.packages)) {
148
+ throw new Error(`Internal error: no entry for package named ${name}`);
149
+ }
150
+ file_name = API.packages[name].file_name;
151
+ } else {
152
+ file_name = channel;
153
+ }
154
+ return await _loadBinaryFile(baseURL, file_name);
155
+ }
156
+
157
+ /**
158
+ * Install the package into the file system.
159
+ * @param name The name of the package
160
+ * @param buffer The binary data returned by downloadPkgBuffer
161
+ * @private
162
+ */
163
+ async function installPackage(name: string, buffer: Uint8Array) {
164
+ let pkg = API.packages[name];
165
+ if (!pkg) {
166
+ pkg = {
167
+ file_name: ".whl",
168
+ install_dir: "site",
169
+ shared_library: false,
170
+ depends: [],
171
+ imports: [] as string[],
172
+ };
173
+ }
174
+ const filename = pkg.file_name;
175
+ // This Python helper function unpacks the buffer and lists out any so files therein.
176
+ const dynlibs = API.package_loader.unpack_buffer.callKwargs({
177
+ buffer,
178
+ filename,
179
+ target: pkg.install_dir,
180
+ calculate_dynlibs: true,
181
+ });
182
+ for (const dynlib of dynlibs) {
183
+ await loadDynlib(dynlib, pkg.shared_library);
184
+ }
185
+ loadedPackages[name] = pkg;
186
+ }
187
+
188
+ /**
189
+ * @returns A new asynchronous lock
190
+ * @private
191
+ */
192
+ function createLock() {
193
+ // This is a promise that is resolved when the lock is open, not resolved when lock is held.
194
+ let _lock = Promise.resolve();
195
+
196
+ /**
197
+ * Acquire the async lock
198
+ * @returns A zero argument function that releases the lock.
199
+ * @private
200
+ */
201
+ async function acquireLock() {
202
+ const old_lock = _lock;
203
+ let releaseLock: () => void;
204
+ _lock = new Promise((resolve) => (releaseLock = resolve));
205
+ await old_lock;
206
+ // @ts-ignore
207
+ return releaseLock;
208
+ }
209
+ return acquireLock;
210
+ }
211
+
212
+ // Emscripten has a lock in the corresponding code in library_browser.js. I
213
+ // don't know why we need it, but quite possibly bad stuff will happen without
214
+ // it.
215
+ const acquireDynlibLock = createLock();
216
+
217
+ /**
218
+ * Load a dynamic library. This is an async operation and Python imports are
219
+ * synchronous so we have to do it ahead of time. When we add more support for
220
+ * synchronous I/O, we could consider doing this later as a part of a Python
221
+ * import hook.
222
+ *
223
+ * @param lib The file system path to the library.
224
+ * @param shared Is this a shared library or not?
225
+ * @private
226
+ */
227
+ async function loadDynlib(lib: string, shared: boolean) {
228
+ const node = Module.FS.lookupPath(lib).node;
229
+ let byteArray;
230
+ if (node.mount.type == Module.FS.filesystems.MEMFS) {
231
+ byteArray = Module.FS.filesystems.MEMFS.getFileDataAsTypedArray(
232
+ Module.FS.lookupPath(lib).node
233
+ );
234
+ } else {
235
+ byteArray = Module.FS.readFile(lib);
236
+ }
237
+ const releaseDynlibLock = await acquireDynlibLock();
238
+ try {
239
+ const module = await Module.loadWebAssemblyModule(byteArray, {
240
+ loadAsync: true,
241
+ nodelete: true,
242
+ allowUndefined: true,
243
+ });
244
+ Module.preloadedWasm[lib] = module;
245
+ Module.preloadedWasm[lib.split("/").pop()!] = module;
246
+ if (shared) {
247
+ Module.loadDynamicLibrary(lib, {
248
+ global: true,
249
+ nodelete: true,
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;
260
+ } finally {
261
+ releaseDynlibLock();
262
+ }
263
+ }
264
+ Tests.loadDynlib = loadDynlib;
265
+
266
+ const acquirePackageLock = createLock();
267
+
268
+ /**
269
+ * Load a package or a list of packages over the network. This installs the
270
+ * package in the virtual filesystem. The package needs to be imported from
271
+ * Python before it can be used.
272
+ *
273
+ * @param names Either a single package name or
274
+ * URL or a list of them. URLs can be absolute or relative. The URLs must have
275
+ * file name ``<package-name>.js`` and there must be a file called
276
+ * ``<package-name>.data`` in the same directory. The argument can be a
277
+ * ``PyProxy`` of a list, in which case the list will be converted to JavaScript
278
+ * and the ``PyProxy`` will be destroyed.
279
+ * @param messageCallback A callback, called with progress messages
280
+ * (optional)
281
+ * @param errorCallback A callback, called with error/warning messages
282
+ * (optional)
283
+ * @async
284
+ */
285
+ export async function loadPackage(
286
+ names: string | PyProxy | Array<string>,
287
+ messageCallback?: (msg: string) => void,
288
+ errorCallback?: (msg: string) => void
289
+ ) {
290
+ messageCallback = messageCallback || console.log;
291
+ errorCallback = errorCallback || console.error;
292
+ if (isPyProxy(names)) {
293
+ names = names.toJs();
294
+ }
295
+ if (!Array.isArray(names)) {
296
+ names = [names as string];
297
+ }
298
+
299
+ const [toLoad, toLoadShared] = recursiveDependencies(names, errorCallback);
300
+
301
+ for (const [pkg, uri] of [...toLoad, ...toLoadShared]) {
302
+ const loaded = loadedPackages[pkg];
303
+ if (loaded === undefined) {
304
+ continue;
305
+ }
306
+ toLoad.delete(pkg);
307
+ toLoadShared.delete(pkg);
308
+ // If uri is from the DEFAULT_CHANNEL, we assume it was added as a
309
+ // dependency, which was previously overridden.
310
+ if (loaded === uri || uri === DEFAULT_CHANNEL) {
311
+ messageCallback(`${pkg} already loaded from ${loaded}`);
312
+ } else {
313
+ errorCallback(
314
+ `URI mismatch, attempting to load package ${pkg} from ${uri} ` +
315
+ `while it is already loaded from ${loaded}. To override a dependency, ` +
316
+ `load the custom package first.`
317
+ );
318
+ }
319
+ }
320
+
321
+ if (toLoad.size === 0 && toLoadShared.size === 0) {
322
+ messageCallback("No new packages to load");
323
+ return;
324
+ }
325
+
326
+ const packageNames = [...toLoad.keys(), ...toLoadShared.keys()].join(", ");
327
+ const releaseLock = await acquirePackageLock();
328
+ try {
329
+ messageCallback(`Loading ${packageNames}`);
330
+ const sharedLibraryLoadPromises: { [name: string]: Promise<Uint8Array> } =
331
+ {};
332
+ const packageLoadPromises: { [name: string]: Promise<Uint8Array> } = {};
333
+ for (const [name, channel] of toLoadShared) {
334
+ if (loadedPackages[name]) {
335
+ // Handle the race condition where the package was loaded between when
336
+ // we did dependency resolution and when we acquired the lock.
337
+ toLoadShared.delete(name);
338
+ continue;
339
+ }
340
+ sharedLibraryLoadPromises[name] = downloadPackage(name, channel);
341
+ }
342
+ for (const [name, channel] of toLoad) {
343
+ if (loadedPackages[name]) {
344
+ // Handle the race condition where the package was loaded between when
345
+ // we did dependency resolution and when we acquired the lock.
346
+ toLoad.delete(name);
347
+ continue;
348
+ }
349
+ packageLoadPromises[name] = downloadPackage(name, channel);
350
+ }
351
+
352
+ const loaded: string[] = [];
353
+ const failed: { [name: string]: any } = {};
354
+ // TODO: add support for prefetching modules by awaiting on a promise right
355
+ // here which resolves in loadPyodide when the bootstrap is done.
356
+ const sharedLibraryInstallPromises: { [name: string]: Promise<void> } = {};
357
+ const packageInstallPromises: { [name: string]: Promise<void> } = {};
358
+ for (const [name, channel] of toLoadShared) {
359
+ sharedLibraryInstallPromises[name] = sharedLibraryLoadPromises[name]
360
+ .then(async (buffer) => {
361
+ await installPackage(name, buffer);
362
+ loaded.push(name);
363
+ loadedPackages[name] = channel;
364
+ })
365
+ .catch((err) => {
366
+ console.warn(err);
367
+ failed[name] = err;
368
+ });
369
+ }
370
+
371
+ await Promise.all(Object.values(sharedLibraryInstallPromises));
372
+ for (const [name, channel] of toLoad) {
373
+ packageInstallPromises[name] = packageLoadPromises[name]
374
+ .then(async (buffer) => {
375
+ await installPackage(name, buffer);
376
+ loaded.push(name);
377
+ loadedPackages[name] = channel;
378
+ })
379
+ .catch((err) => {
380
+ console.warn(err);
381
+ failed[name] = err;
382
+ });
383
+ }
384
+ await Promise.all(Object.values(packageInstallPromises));
385
+
386
+ Module.reportUndefinedSymbols();
387
+ if (loaded.length > 0) {
388
+ const successNames = loaded.join(", ");
389
+ messageCallback(`Loaded ${successNames}`);
390
+ }
391
+ if (Object.keys(failed).length > 0) {
392
+ const failedNames = Object.keys(failed).join(", ");
393
+ messageCallback(`Failed to load ${failedNames}`);
394
+ for (const [name, err] of Object.entries(failed)) {
395
+ console.warn(`The following error occurred while loading ${name}:`);
396
+ console.error(err);
397
+ }
398
+ }
399
+
400
+ // We have to invalidate Python's import caches, or it won't
401
+ // see the new files.
402
+ API.importlib.invalidate_caches();
403
+ } finally {
404
+ releaseLock();
405
+ }
406
+ }
407
+
408
+ /**
409
+ * The list of packages that Pyodide has loaded.
410
+ * Use ``Object.keys(pyodide.loadedPackages)`` to get the list of names of
411
+ * loaded packages, and ``pyodide.loadedPackages[package_name]`` to access
412
+ * install location for a particular ``package_name``.
413
+ */
414
+ export let loadedPackages: { [key: string]: string } = {};
@@ -1,28 +1,36 @@
1
- /**
2
- * @typedef {import('emscripten').Module} Module
3
- */
4
-
5
1
  /**
6
2
  * The Emscripten Module.
7
3
  *
8
4
  * @private
9
- * @type {Module}
10
5
  */
11
- export let Module = {};
6
+ export let Module: any = {};
12
7
  Module.noImageDecoding = true;
13
8
  Module.noAudioDecoding = true;
14
9
  Module.noWasmDecoding = false; // we preload wasm using the built in plugin now
15
10
  Module.preloadedWasm = {};
16
11
  Module.preRun = [];
17
12
 
13
+ export let API: any = {};
14
+ Module.API = API;
15
+ export let Hiwire: any = {};
16
+ Module.hiwire = Hiwire;
17
+
18
+ // Put things that are exposed only for testing purposes here.
19
+ export let Tests: any = {};
20
+ API.tests = Tests;
21
+
18
22
  /**
19
23
  *
20
- * @param {undefined | function(): string} stdin
21
- * @param {undefined | function(string)} stdout
22
- * @param {undefined | function(string)} stderr
24
+ * @param stdin
25
+ * @param stdout
26
+ * @param stderr
23
27
  * @private
24
28
  */
25
- export function setStandardStreams(stdin, stdout, stderr) {
29
+ export function setStandardStreams(
30
+ stdin?: () => string,
31
+ stdout?: (a: string) => void,
32
+ stderr?: (a: string) => void
33
+ ) {
26
34
  // For stdout and stderr, emscripten provides convenient wrappers that save us the trouble of converting the bytes into a string
27
35
  if (stdout) {
28
36
  Module.print = stdout;
@@ -40,7 +48,7 @@ export function setStandardStreams(stdin, stdout, stderr) {
40
48
  }
41
49
  }
42
50
 
43
- function createStdinWrapper(stdin) {
51
+ function createStdinWrapper(stdin: () => string) {
44
52
  // When called, it asks the user for one whole line of input (stdin)
45
53
  // Then, it passes the individual bytes of the input to emscripten, one after another.
46
54
  // And finally, it terminates it with null.
@@ -89,10 +97,10 @@ function createStdinWrapper(stdin) {
89
97
  * Make the home directory inside the virtual file system,
90
98
  * then change the working directory to it.
91
99
  *
92
- * @param {string} path
100
+ * @param path
93
101
  * @private
94
102
  */
95
- export function setHomeDirectory(path) {
103
+ export function setHomeDirectory(path: string) {
96
104
  Module.preRun.push(function () {
97
105
  const fallbackPath = "/";
98
106
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyodide",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "The Pyodide JavaScript package",
5
5
  "keywords": [
6
6
  "python",
@@ -16,22 +16,29 @@
16
16
  },
17
17
  "license": "Apache-2.0",
18
18
  "devDependencies": {
19
+ "@rollup/plugin-commonjs": "^21.0.1",
20
+ "@rollup/plugin-node-resolve": "^13.1.3",
21
+ "@types/emscripten": "^1.39.5",
22
+ "@types/node-fetch": "^3.0.3",
23
+ "error-stack-parser": "^2.0.6",
24
+ "mocha": "^9.0.2",
19
25
  "prettier": "^2.2.1",
20
- "terser": "^5.7.0",
21
26
  "rollup": "^2.48.0",
22
27
  "rollup-plugin-terser": "^7.0.2",
28
+ "rollup-plugin-ts": "^2.0.5",
29
+ "terser": "^5.7.0",
30
+ "ts-node": "^10.4.0",
23
31
  "tsd": "^0.15.1",
24
32
  "typescript": "^4.2.4",
25
- "mocha": "^9.0.2",
26
- "@types/emscripten": "^1.39.5"
33
+ "typedoc": "^0.22.11"
27
34
  },
28
35
  "type": "module",
29
36
  "scripts": {
30
- "test": "mocha"
37
+ "test": "mocha --loader node_modules/ts-node/dist/esm.js"
31
38
  },
32
39
  "mocha": {
33
40
  "timeout": 30000,
34
- "file": "./test/conftest.js"
41
+ "file": "./test/conftest.mjs"
35
42
  },
36
43
  "tsd": {
37
44
  "compilerOptions": {