pyodide-pool 0.1.2__tar.gz → 0.2.0__tar.gz

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.
@@ -6,7 +6,7 @@ _output/
6
6
  bench/results/node-smoke*
7
7
  playwright-report/
8
8
  .jupyterlite.doit.db
9
- demos/jupyterlite/files/assets/
9
+ python/pyodide_pool/pyodide-pool.browser.js
10
10
  demos/jupyterlite/files/wheels/
11
11
  demos/jupyterlite/pypi/
12
12
  demos/jupyterlite/.cache/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyodide-pool
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Run Python callables and dask graphs on a pool of Pyodide Web Workers (driver-side API)
5
5
  Project-URL: Homepage, https://github.com/fideus-labs/python-wasm-parallelism
6
6
  Project-URL: Demo, https://fideus-labs.github.io/python-wasm-parallelism/
@@ -60,8 +60,8 @@ value = await pyodide_pool.submit(fn, *args, **kwargs) # one remote call
60
60
  total = await pyodide_pool.compute(dask.delayed(sum)([...])) # dask collections
61
61
  ```
62
62
 
63
- `create_pool` loads the self-contained JS bundle from the JupyterLite site by
64
- default; pass `js_url=` to load it from elsewhere. A web app that embeds
63
+ `create_pool` imports the self-contained JS bundle that ships inside the
64
+ wheel; pass `js_url=` to load a hosted copy instead. A web app that embeds
65
65
  Pyodide can instead create the JS `PyodidePool` itself and register it with
66
66
  `pyodide.registerJsModule('js_pyodide_pool', { pool })`.
67
67
 
@@ -36,8 +36,8 @@ value = await pyodide_pool.submit(fn, *args, **kwargs) # one remote call
36
36
  total = await pyodide_pool.compute(dask.delayed(sum)([...])) # dask collections
37
37
  ```
38
38
 
39
- `create_pool` loads the self-contained JS bundle from the JupyterLite site by
40
- default; pass `js_url=` to load it from elsewhere. A web app that embeds
39
+ `create_pool` imports the self-contained JS bundle that ships inside the
40
+ wheel; pass `js_url=` to load a hosted copy instead. A web app that embeds
41
41
  Pyodide can instead create the JS `PyodidePool` itself and register it with
42
42
  `pyodide.registerJsModule('js_pyodide_pool', { pool })`.
43
43
 
@@ -40,7 +40,7 @@ from ._bridge import (
40
40
  )
41
41
  from ._packages import EXCLUDED_FROM_MIRROR, PackageSnapshot, snapshot_packages
42
42
 
43
- __version__ = "0.1.2"
43
+ __version__ = "0.2.0"
44
44
 
45
45
  __all__ = [
46
46
  "EXCLUDED_FROM_MIRROR",
@@ -171,7 +171,10 @@ class WorkerPool:
171
171
 
172
172
  The call triple ships as a cloudpickle payload together with the
173
173
  driver's current package snapshot, so the worker mirrors the
174
- driver's installed packages before executing.
174
+ driver's installed packages before executing. Where the runtime has
175
+ JSPI, the worker runs the call with stack switching: ``func`` may
176
+ block on an awaitable with ``pyodide.ffi.run_sync`` and may be a
177
+ coroutine function, whose awaited value is the result.
175
178
  """
176
179
  payload = _to_js_buffer(cloudpickle.dumps((func, args, kwargs)))
177
180
  packages, wheels = snapshot_packages()
@@ -0,0 +1,70 @@
1
+ """Bootstrap the JS worker pool from inside a Pyodide kernel (JupyterLite).
2
+
3
+ Runs in the MAIN Pyodide instance (the driver), like the rest of the
4
+ package. An embedding web app wires the JS side itself (create a
5
+ ``PyodidePool``, ``pyodide.registerJsModule('js_pyodide_pool', { pool })``),
6
+ but notebook code inside JupyterLite's Pyodide kernel has no JS embedder to
7
+ lean on — the kernel itself runs in a Web Worker. :func:`create_pool` pulls
8
+ the JS side in from Python instead: dynamically ``import()`` the
9
+ self-contained browser bundle (``pyodide-pool.browser.js``, built by
10
+ ``npm run build`` and shipped inside this wheel), construct the JS
11
+ ``PyodidePool`` — its workers spawn as NESTED Web Workers from a Blob URL
12
+ inlined in the bundle — and install the Python :class:`WorkerPool` wrapper
13
+ as the default pool, so ``pyodide_pool.submit``/``compute`` work with no
14
+ further setup.
15
+
16
+ The vendored bundle is imported from a ``data:`` URL, so no host has to
17
+ serve it; a page with a ``script-src`` CSP must allow ``data:`` (the nested
18
+ workers already need ``blob:``).
19
+
20
+ Environment requirements: nested-worker support (all Chromium-based
21
+ browsers; Firefox and Safari in current versions) and network access to
22
+ the Pyodide CDN. The pool is postMessage-based, so it does NOT need a
23
+ cross-origin-isolated page — it runs on plain static hosts like GitHub
24
+ Pages (verified by the Pages deployment of demos/jupyterlite). COOP/COEP
25
+ headers (scripts/serve-lite.mjs sends them) only add SharedArrayBuffer
26
+ extras such as the JupyterLite kernel's coincident transport.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from base64 import b64encode
32
+ from importlib.resources import files
33
+
34
+ from . import _bridge
35
+ from ._bridge import WorkerPool
36
+
37
+ __all__ = ["create_pool"]
38
+
39
+ #: The browser bundle, copied next to this module by scripts/bundles.mjs and
40
+ #: listed as a build artifact in pyproject.toml so the wheel carries it.
41
+ _BUNDLE = "pyodide-pool.browser.js"
42
+
43
+
44
+ async def create_pool(pool_size: int = 4, js_url: str | None = None) -> WorkerPool:
45
+ """Import the JS bundle, start a worker pool, and make it the default.
46
+
47
+ Parameters
48
+ ----------
49
+ pool_size:
50
+ Maximum number of concurrent workers, one Pyodide interpreter each.
51
+ js_url:
52
+ Load the bundle from this URL instead of the vendored copy. Must be
53
+ fully qualified (``https://``, or ``file://`` under Node): inside a
54
+ kernel worker, ``import()`` resolves a relative URL against the CDN
55
+ module evaluating it, not against the site.
56
+ """
57
+ from js import Object
58
+ from pyodide.code import run_js
59
+ from pyodide.ffi import to_js
60
+
61
+ if js_url is None:
62
+ # FileNotFoundError here means the wheel was built before the JS
63
+ # bundle (run `npm run build` first).
64
+ source = files(__package__).joinpath(_BUNDLE).read_bytes()
65
+ js_url = "data:text/javascript;base64," + b64encode(source).decode()
66
+ module = await run_js("(url) => import(url)")(js_url)
67
+ options = to_js({"poolSize": pool_size}, dict_converter=Object.fromEntries)
68
+ pool = WorkerPool(module.createPool(options))
69
+ _bridge.set_default_pool(pool)
70
+ return pool
@@ -40,3 +40,7 @@ path = "pyodide_pool/__init__.py"
40
40
 
41
41
  [tool.hatch.build.targets.wheel]
42
42
  packages = ["pyodide_pool"]
43
+ # Build output written by scripts/bundles.mjs and gitignored at the repo root.
44
+ # Hatchling only reads a .gitignore inside this directory today, so the file
45
+ # is included either way; naming it keeps the wheel whole if that changes.
46
+ artifacts = ["pyodide_pool/pyodide-pool.browser.js"]
@@ -1,99 +0,0 @@
1
- """Bootstrap the JS worker pool from inside a Pyodide kernel (JupyterLite).
2
-
3
- Runs in the MAIN Pyodide instance (the driver), like the rest of the
4
- package. An embedding web app wires the JS side itself (create a
5
- ``PyodidePool``, ``pyodide.registerJsModule('js_pyodide_pool', { pool })``),
6
- but notebook code inside JupyterLite's Pyodide kernel has no JS embedder to
7
- lean on — the kernel itself runs in a Web Worker. :func:`create_pool` pulls
8
- the JS side in from Python instead: dynamically ``import()`` the
9
- self-contained browser bundle (``pyodide-pool.browser.js``, built by
10
- ``npm run build`` and copied into the site's ``files/assets/`` by
11
- ``npm run build:lite``), construct the JS ``PyodidePool`` — its workers
12
- spawn as NESTED Web Workers from a Blob URL inlined in the bundle — and
13
- install the Python :class:`WorkerPool` wrapper as the default pool, so
14
- ``pyodide_pool.submit``/``compute`` work with no further setup.
15
-
16
- Environment requirements: nested-worker support (all Chromium-based
17
- browsers; Firefox and Safari in current versions) and network access to
18
- the Pyodide CDN. The pool is postMessage-based, so it does NOT need a
19
- cross-origin-isolated page — it runs on plain static hosts like GitHub
20
- Pages (verified by the Pages deployment of demos/jupyterlite). COOP/COEP
21
- headers (scripts/serve-lite.mjs sends them) only add SharedArrayBuffer
22
- extras such as the JupyterLite kernel's coincident transport.
23
- """
24
-
25
- from __future__ import annotations
26
-
27
- from . import _bridge
28
- from ._bridge import WorkerPool
29
-
30
- __all__ = ["DEFAULT_JS_URL", "create_pool"]
31
-
32
- #: Where the JupyterLite site serves the bundle, as a path RELATIVE to the
33
- #: deployment's base URL (``files/`` holds the site contents). It is resolved
34
- #: against the base URL derived from the kernel worker's ``location`` (see
35
- #: :data:`_IMPORT_JS`), so the default works whether the site is hosted at the
36
- #: origin root (scripts/serve-lite.mjs) or under a sub-path such as GitHub
37
- #: Pages (``/<repo>/``) — no explicit ``js_url`` needed either way.
38
- DEFAULT_JS_URL = "files/assets/pyodide-pool.browser.js"
39
-
40
- # JS that dynamically ``import()``s the browser bundle. Two wrinkles make a
41
- # plain ``import(url)`` wrong inside JupyterLite's Pyodide kernel:
42
- # * ``import()`` resolves relative/path-absolute URLs against the REFERRER
43
- # module — here pyodide.asm.js, loaded from the jsDelivr CDN — so a
44
- # site-relative path would 404 on the CDN rather than the site.
45
- # * A path-absolute ``/files/...`` resolved against the page origin drops
46
- # any deployment sub-path, so GitHub Pages (``/<repo>/``) 404s on
47
- # ``origin/files/...`` — the original symptom this bootstrap fixes.
48
- # The kernel worker is served from ``<baseUrl>extensions/.../<name>.worker.js``
49
- # (jupyterlite-pyodide-kernel's ``initWorker``), so everything up to the slash
50
- # before ``extensions/`` is the deployment base URL. Resolve the base-relative
51
- # bundle URL against that, falling back to the origin root. A fully-qualified
52
- # URL (http:, https:, file:, blob:, data:) and Node (no global ``location``)
53
- # are imported as given.
54
- _IMPORT_JS = r"""
55
- (async (u) => {
56
- if (typeof location === 'undefined') return import(u);
57
- if (/^[a-z][a-z0-9+.\-]*:/i.test(u)) return import(u);
58
- const here = location.href;
59
- const rel = u.replace(/^\/+/, '');
60
- const ext = here.indexOf('/extensions/');
61
- const base = ext === -1 ? new URL('/', here).href : here.slice(0, ext + 1);
62
- const candidates = [...new Set([new URL(rel, base).href, new URL('/' + rel, here).href])];
63
- let lastErr;
64
- for (const c of candidates) {
65
- try { return await import(c); } catch (e) { lastErr = e; }
66
- }
67
- throw lastErr;
68
- })
69
- """.strip()
70
-
71
-
72
- async def create_pool(pool_size: int = 4, js_url: str | None = None) -> WorkerPool:
73
- """Import the JS bundle, start a worker pool, and make it the default.
74
-
75
- Parameters
76
- ----------
77
- pool_size:
78
- Maximum number of concurrent workers, one Pyodide interpreter each.
79
- js_url:
80
- URL of the self-contained browser bundle, defaulting to
81
- :data:`DEFAULT_JS_URL`. A base-relative path (``files/...``) is
82
- resolved against the JupyterLite deployment base URL; a full URL
83
- (``http(s)://``, or a ``file://`` URL under Node) is used as-is.
84
- """
85
- from js import Object
86
- from pyodide.code import run_js
87
- from pyodide.ffi import to_js
88
-
89
- url = DEFAULT_JS_URL if js_url is None else js_url
90
- # Resolve and import the bundle against the deployment base URL; see
91
- # _IMPORT_JS for why a plain import() / path-absolute URL is wrong here.
92
- # repr(url) yields a JS-safe string literal (matching the old f-string's
93
- # {url!r}); build the call by concatenation so the JS body's braces are
94
- # not read as Python format fields.
95
- module = await run_js(_IMPORT_JS + "(" + repr(url) + ")")
96
- options = to_js({"poolSize": pool_size}, dict_converter=Object.fromEntries)
97
- pool = WorkerPool(module.createPool(options))
98
- _bridge.set_default_pool(pool)
99
- return pool