From 725ae239e457485672cefdc0d72c9abb988e8b59 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 2 Nov 2026 13:45:16 -0500 Subject: [PATCH 1/4] feat: upgrade Pyodide from 0.27.7 to 314.0.7 Pyodide 314 ships Python 3.14, pydantic 2.12.5 and orjson 3.11.8, so shinychat can move to 0.7.x. The lockfile is regenerated with the #233 fix, so the pure-Python pins catch up with PyPI too. - toJs() now turns dicts into plain objects, which Object.fromEntries() can't take. Pass `dict_converter: Object.fromEntries` instead. - Pyodide's own starlette lists no dependencies but imports anyio. Add it through EXTRA_DEPENDENCIES. - Pyodide's altair 6.0.0 sends NaN from pandas 3 string columns as invalid JSON. Take altair 6.3.0 from PyPI, and let our lockfile replace a Pyodide package (the check meant to stop this never ran). - Set JOBLIB_MULTIPROCESSING=0. joblib now finds `multiprocessing` and warns that it will run in serial mode. - plotnine 0.15 turns the colour "#666" into the invalid "#666FF"; use "#666666" in the plot_interact_exclude example. - Pyodide 0.28 dropped `info.version` from pyodide-lock.json, so compare the whole `info` block to tell when it changed. - Skip the new `*-tests.zip` files when unpacking Pyodide. - The test servers queue more connections. Pyodide now asks for every wheel at once, and macOS resets anything past `http.server`'s queue of 5, so packages failed to load at random. Closes #254 --- Makefile | 6 +- examples/python/plot_interact_exclude/app.py | 2 +- scripts/create_typeshed.py | 2 +- scripts/pyodide_packages.py | 5 +- shinylive_lock.json | 464 ++++------ shinylive_requirements.json | 6 + src/hooks/usePyodide.tsx | 4 + src/messageporthttp.ts | 2 +- src/messageportwebsocket-channel.ts | 2 +- src/pyodide/ffi.d.ts | 56 +- src/pyodide/pyodide.d.ts | 837 +++++++++++++++---- src/pyodide/pyodide.js | 6 +- tests/conftest.py | 30 +- 13 files changed, 935 insertions(+), 487 deletions(-) diff --git a/Makefile b/Makefile index 714a983b..cbff8a4c 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ SHINYLIVE_VERSION = $(shell node -p "require('./package.json').version") -PYODIDE_VERSION = 0.27.7 +PYODIDE_VERSION = 314.0.7 PYODIDE_DIST_FILENAME = pyodide-$(PYODIDE_VERSION).tar.bz2 DOWNLOAD_DIR = ./downloads R_SHINY_VERSION = 1.13.0.8000 @@ -45,7 +45,7 @@ FAICONS_WHEEL = faicons-$(FAICONS_VERSION)-py3-none-any.whl # libsass is built in gadenbuie/libsass-python # NOTE: Update https://github.com/gadenbuie/libsass-python/blob/dev/.github/workflows/pyodide.yml # Pyodide, Emscripten, or Python versions change here. -LIBSASS_WHEEL=libsass-0.23.0-cp312-abi3-pyodide_2024_0_wasm32.whl +LIBSASS_WHEEL=libsass-0.23.0-cp314-abi3-pyemscripten_2026_0_wasm32.whl VENV = venv PYBIN = $(VENV)/bin @@ -142,7 +142,7 @@ $(DOWNLOAD_DIR)/$(PYODIDE_DIST_FILENAME): $(BUILD_DIR)/shinylive/pyodide: $(DOWNLOAD_DIR)/$(PYODIDE_DIST_FILENAME) mkdir -p $(BUILD_DIR)/shinylive/pyodide - tar --exclude "*test*.tar" --exclude "node_modules" \ + tar --exclude "*test*.tar" --exclude "*-tests.zip" --exclude "node_modules" \ -xvjf $(DOWNLOAD_DIR)/$(PYODIDE_DIST_FILENAME) \ -C $(BUILD_DIR)/shinylive diff --git a/examples/python/plot_interact_exclude/app.py b/examples/python/plot_interact_exclude/app.py index d4f5af6f..ccbb9259 100644 --- a/examples/python/plot_interact_exclude/app.py +++ b/examples/python/plot_interact_exclude/app.py @@ -75,7 +75,7 @@ def plot1(): return ( ggplot(df_keep, aes("wt", "mpg")) + geom_point() - + geom_point(data=df_exclude, color="#666", fill="white") + + geom_point(data=df_exclude, color="#666666", fill="white") + geom_smooth(method="lm", fullrange=True) ) diff --git a/scripts/create_typeshed.py b/scripts/create_typeshed.py index 11e5ad72..339cc03b 100755 --- a/scripts/create_typeshed.py +++ b/scripts/create_typeshed.py @@ -9,7 +9,7 @@ import pyright # TODO: Automate version detection -PYODIDE_PYTHON_VERSION = "3.12" +PYODIDE_PYTHON_VERSION = "3.14" # Packages that we'll create type stubs for. PACKAGES = ("htmltools", "shiny", "shinywidgets", "shinyswatch") diff --git a/scripts/pyodide_packages.py b/scripts/pyodide_packages.py index 4337d3c4..55c43100 100755 --- a/scripts/pyodide_packages.py +++ b/scripts/pyodide_packages.py @@ -99,6 +99,8 @@ "anyio": ["exceptiongroup"], # palmerpenguins imports pkg_resources, which is part of setuptools. "palmerpenguins": ["setuptools"], + # Pyodide 314's own starlette entry lists no dependencies, but it imports anyio. + "starlette": ["anyio"], } # Some packages may have a circular dependency. For example, shiny (v1.5.0) depends on @@ -703,7 +705,8 @@ def orig_pyodide_lock() -> PyodidePackagesFile: with open(pyodide_lock_json_file, "r") as f_new, open(orig_pyodide_lock_json_file, "r") as f_orig: new_info = json.load(f_new).get("info", {}) orig_info = json.load(f_orig).get("info", {}) - if new_info.get("version") != orig_info.get("version"): + # Pyodide 0.28 dropped `info.version`, so compare the whole block. + if new_info != orig_info: shutil.copy(pyodide_lock_json_file, orig_pyodide_lock_json_file) except Exception: shutil.copy(pyodide_lock_json_file, orig_pyodide_lock_json_file) diff --git a/shinylive_lock.json b/shinylive_lock.json index b32cf371..16fc96d6 100644 --- a/shinylive_lock.json +++ b/shinylive_lock.json @@ -86,12 +86,12 @@ }, "mdit-py-plugins": { "name": "mdit-py-plugins", - "version": "0.4.1", - "filename": "mdit_py_plugins-0.4.1-py3-none-any.whl", - "sha256": "1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a", - "url": "https://files.pythonhosted.org/packages/ef/f7/8a4dcea720a581e69ac8c5a38524baf0e3e2bb5f3819a9ff661464fe7d10/mdit_py_plugins-0.4.1-py3-none-any.whl", + "version": "0.6.1", + "filename": "mdit_py_plugins-0.6.1-py3-none-any.whl", + "sha256": "214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", + "url": "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", "depends": [ - {"name": "markdown-it-py", "specs": [["<", "4.0.0"], [">=", "1.0.0"]]} + {"name": "markdown-it-py", "specs": [["<", "5.0.0"], [">=", "2.0.0"]]} ], "imports": [ "mdit_py_plugins" @@ -99,17 +99,16 @@ }, "plotnine": { "name": "plotnine", - "version": "0.14.5", - "filename": "plotnine-0.14.5-py3-none-any.whl", - "sha256": "4a8bc4360732dd69a0263def4abab285ed8f0f4386186f1e44c642f2cea79b88", - "url": "https://files.pythonhosted.org/packages/4d/c5/7cfda7ba9fa02243367fbfb4880b6de8039266f22c47c2dbbd39b6adc46f/plotnine-0.14.5-py3-none-any.whl", + "version": "0.15.8", + "filename": "plotnine-0.15.8-py3-none-any.whl", + "sha256": "b8a17fbccdf331f0474ff59ea03186c07d96586eb0ebe70121b03fbf131881a9", + "url": "https://files.pythonhosted.org/packages/40/37/7a0275ea8143f0f0a2b27db73656d7ea3459e9cf8f869e292e450b357d1e/plotnine-0.15.8-py3-none-any.whl", "depends": [ {"name": "matplotlib", "specs": [[">=", "3.8.0"]]}, {"name": "pandas", "specs": [[">=", "2.2.0"]]}, - {"name": "mizani", "specs": [["~=", "0.13.0"]]}, + {"name": "mizani", "specs": [["~=", "0.14.0"]]}, {"name": "numpy", "specs": [[">=", "1.23.5"]]}, - {"name": "scipy", "specs": [[">=", "1.8.0"]]}, - {"name": "statsmodels", "specs": [[">=", "0.14.0"]]} + {"name": "scipy", "specs": [[">=", "1.8.0"]]} ], "imports": [ "plotnine" @@ -117,18 +116,34 @@ }, "plotly": { "name": "plotly", - "version": "5.23.0", - "filename": "plotly-5.23.0-py3-none-any.whl", - "sha256": "76cbe78f75eddc10c56f5a4ee3e7ccaade7c0a57465546f02098c0caed6c2d1a", - "url": "https://files.pythonhosted.org/packages/b8/f0/bcf716a8e070370d6598c92fcd328bd9ef8a9bda2c5562da5a835c66700b/plotly-5.23.0-py3-none-any.whl", + "version": "7.1.0", + "filename": "plotly-7.1.0-py3-none-any.whl", + "sha256": "dbb7fa18afce40d0a8e80d1bf162eceb3faa0ce5a77fe741ad09a74cf78f53f3", + "url": "https://files.pythonhosted.org/packages/e5/7d/905a3a3d51087515719058c94cfbda2ff0fc14417c20d557ae3e82d8b250/plotly-7.1.0-py3-none-any.whl", "depends": [ - {"name": "tenacity", "specs": [[">=", "6.2.0"]]}, + {"name": "narwhals", "specs": [[">=", "1.15.1"]]}, {"name": "packaging", "specs": []} ], "imports": [ "plotly" ] }, + "altair": { + "name": "altair", + "version": "6.3.0", + "filename": "altair-6.3.0-py3-none-any.whl", + "sha256": "7defb6ca730676dfc99a299768e2769f51585fcb3dc960ea71aacc368929d65e", + "url": "https://files.pythonhosted.org/packages/ca/b9/10a8bb13a0462e0bc4e02c0b7b8237dc24ab590f5bb4be94bec5e8f1d532/altair-6.3.0-py3-none-any.whl", + "depends": [ + {"name": "jinja2", "specs": []}, + {"name": "jsonschema", "specs": [[">=", "3.0"]]}, + {"name": "narwhals", "specs": [[">=", "2.4.0"]]}, + {"name": "packaging", "specs": []} + ], + "imports": [ + "altair" + ] + }, "seaborn": { "name": "seaborn", "version": "0.13.2", @@ -146,15 +161,15 @@ }, "ipywidgets": { "name": "ipywidgets", - "version": "8.1.3", - "filename": "ipywidgets-8.1.3-py3-none-any.whl", - "sha256": "efafd18f7a142248f7cb0ba890a68b96abd4d6e88ddbda483c9130d12667eaf2", - "url": "https://files.pythonhosted.org/packages/d4/17/8b2ce5765dd423433d2e0727712629c46152fb0bc706b0977f847480f262/ipywidgets-8.1.3-py3-none-any.whl", + "version": "8.1.9", + "filename": "ipywidgets-8.1.9-py3-none-any.whl", + "sha256": "f2b8cbcaae10252b809fbe4d7470db75c09b769a32cbf816d20e5ca6d3c5a79d", + "url": "https://files.pythonhosted.org/packages/c3/55/298e9b3b864a198234997e87a1471c1b17d7f3546ace6d18fb5cf1ce24b2/ipywidgets-8.1.9-py3-none-any.whl", "depends": [ {"name": "comm", "specs": [[">=", "0.1.3"]]}, {"name": "ipython", "specs": [[">=", "6.1.0"]]}, {"name": "traitlets", "specs": [[">=", "4.3.1"]]}, - {"name": "jupyterlab-widgets", "specs": [["~=", "3.0.11"]]} + {"name": "jupyterlab_widgets", "specs": [["~=", "3.0.17"]]} ], "imports": [ "ipywidgets" @@ -162,14 +177,14 @@ }, "ipyleaflet": { "name": "ipyleaflet", - "version": "0.19.2", - "filename": "ipyleaflet-0.19.2-py3-none-any.whl", - "sha256": "7cc9157848baca2e1793b96e79f8bdb1aa7340521d2b7d8a62aa8bc30eab5278", - "url": "https://files.pythonhosted.org/packages/56/6f/00d60e93a316a178ae04457ceea5bcbb4e2d7e7e469882ac59ec4cccfb8c/ipyleaflet-0.19.2-py3-none-any.whl", + "version": "0.20.0", + "filename": "ipyleaflet-0.20.0-py3-none-any.whl", + "sha256": "b4c20ddc0b17d68e226cd3367ca2215a4db7e2b14374468c0eeaa54b53e4d173", + "url": "https://files.pythonhosted.org/packages/49/69/e9858f2c0b99bf9f036348d1c84b8026f438bb6875effe6a9bcd9883dada/ipyleaflet-0.20.0-py3-none-any.whl", "depends": [ {"name": "branca", "specs": [[">=", "0.5.0"]]}, {"name": "ipywidgets", "specs": [["<", "9"], [">=", "7.6.0"]]}, - {"name": "jupyter-leaflet", "specs": [["<", "0.20"], [">=", "0.19"]]}, + {"name": "jupyter-leaflet", "specs": [["<", "0.21"], [">=", "0.20"]]}, {"name": "traittypes", "specs": [["<", "3"], [">=", "0.2.1"]]}, {"name": "xyzservices", "specs": [[">=", "2021.8.1"]]} ], @@ -195,10 +210,10 @@ }, "palmerpenguins": { "name": "palmerpenguins", - "version": "0.1.4", - "filename": "palmerpenguins-0.1.4-py3-none-any.whl", - "sha256": "63c904db3dcef10e4ff1372f9b7f50a8f44c7364cc03f27f9a487fe5762d159c", - "url": "https://files.pythonhosted.org/packages/23/78/d81191d1faf9c41e8707eb0c08dc6d489157bfa42faad62e0ab90c37ebbc/palmerpenguins-0.1.4-py3-none-any.whl", + "version": "0.1.6", + "filename": "palmerpenguins-0.1.6-py3-none-any.whl", + "sha256": "5c3be9af84dd703a25f1aaeadeac6ac770cfd38c1620d00089f581c023129ee0", + "url": "https://files.pythonhosted.org/packages/af/9e/7579e7f1ace559be76984107d9e056377993a787eb1fd557a098c32967bc/palmerpenguins-0.1.6-py3-none-any.whl", "depends": [ {"name": "pandas", "specs": []}, {"name": "numpy", "specs": []} @@ -209,16 +224,17 @@ }, "black": { "name": "black", - "version": "24.8.0", - "filename": "black-24.8.0-py3-none-any.whl", - "sha256": "972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", - "url": "https://files.pythonhosted.org/packages/27/1e/83fa8a787180e1632c3d831f7e58994d7aaf23a0961320d21e84f922f919/black-24.8.0-py3-none-any.whl", + "version": "26.10.1", + "filename": "black-26.10.1-py3-none-any.whl", + "sha256": "28842f9a8207cc1df6eb983a35a14c5a0dfcd603d214fe82d84bef552afd2e3a", + "url": "https://files.pythonhosted.org/packages/b1/28/9dd29175c1db777e6189e2a0bb5f37101adf19c3e509b236c8920f778d0b/black-26.10.1-py3-none-any.whl", "depends": [ {"name": "click", "specs": [[">=", "8.0.0"]]}, {"name": "mypy-extensions", "specs": [[">=", "0.4.3"]]}, {"name": "packaging", "specs": [[">=", "22.0"]]}, - {"name": "pathspec", "specs": [[">=", "0.9.0"]]}, - {"name": "platformdirs", "specs": [[">=", "2"]]} + {"name": "pathspec", "specs": [[">=", "1.0.0"]]}, + {"name": "platformdirs", "specs": [[">=", "2"]]}, + {"name": "pytokens", "specs": [["~=", "0.4.0"]]} ], "imports": [ "black" @@ -226,48 +242,32 @@ }, "qrcode": { "name": "qrcode", - "version": "7.4.2", - "filename": "qrcode-7.4.2-py3-none-any.whl", - "sha256": "581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a", - "url": "https://files.pythonhosted.org/packages/24/79/aaf0c1c7214f2632badb2771d770b1500d3d7cbdf2590ae62e721ec50584/qrcode-7.4.2-py3-none-any.whl", - "depends": [ - {"name": "typing-extensions", "specs": []}, - {"name": "pypng", "specs": []} - ], + "version": "8.2", + "filename": "qrcode-8.2-py3-none-any.whl", + "sha256": "16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", + "url": "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", + "depends": [], "imports": [ "qrcode" ] }, - "starlette": { - "name": "starlette", - "version": "0.38.1", - "filename": "starlette-0.38.1-py3-none-any.whl", - "sha256": "42688a287165bd6acc53068457b5cbf28a3204110b704930411728a633f0f7b8", - "url": "https://files.pythonhosted.org/packages/e8/3b/ccd19be31966e79ed88127f418e27bfacd7887992129fdd25543e7c27e46/starlette-0.38.1-py3-none-any.whl", - "depends": [ - {"name": "anyio", "specs": [["<", "5"], [">=", "3.4.0"]]} - ], - "imports": [ - "starlette" - ] - }, - "python-multipart": { - "name": "python-multipart", - "version": "0.0.9", - "filename": "python_multipart-0.0.9-py3-none-any.whl", - "sha256": "97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215", - "url": "https://files.pythonhosted.org/packages/3d/47/444768600d9e0ebc82f8e347775d24aef8f6348cf00e9fa0e81910814e6d/python_multipart-0.0.9-py3-none-any.whl", + "libsass": { + "name": "libsass", + "version": "0.23.0", + "filename": "libsass-0.23.0-cp314-abi3-pyemscripten_2026_0_wasm32.whl", + "sha256": null, + "url": null, "depends": [], "imports": [ - "multipart" + "libsass" ] }, "markdown-it-py": { "name": "markdown-it-py", - "version": "3.0.0", - "filename": "markdown_it_py-3.0.0-py3-none-any.whl", - "sha256": "355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", - "url": "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", + "version": "4.2.0", + "filename": "markdown_it_py-4.2.0-py3-none-any.whl", + "sha256": "9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", + "url": "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", "depends": [ {"name": "mdurl", "specs": [["~=", "0.1"]]} ], @@ -277,45 +277,76 @@ }, "linkify-it-py": { "name": "linkify-it-py", - "version": "2.0.3", - "filename": "linkify_it_py-2.0.3-py3-none-any.whl", - "sha256": "6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", - "url": "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", - "depends": [ - {"name": "uc-micro-py", "specs": []} - ], - "imports": [ - "linkify_it" - ] - }, - "appdirs": { - "name": "appdirs", - "version": "1.4.4", - "filename": "appdirs-1.4.4-py2.py3-none-any.whl", - "sha256": "a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", - "url": "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", + "version": "2.2.0", + "filename": "linkify_it_py-2.2.0-py3-none-any.whl", + "sha256": "3adc40eb5af300b2605fcfdb968c24e1d780a90f1f2221af7c15e5111e94d443", + "url": "https://files.pythonhosted.org/packages/13/d4/1152d1c7ab42d8b908be64fd200ddc870dc9d4925e951198702084aa1a7d/linkify_it_py-2.2.0-py3-none-any.whl", "depends": [], "imports": [ - "appdirs" + "linkify_it" ] }, "asgiref": { "name": "asgiref", - "version": "3.8.1", - "filename": "asgiref-3.8.1-py3-none-any.whl", - "sha256": "3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", - "url": "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", + "version": "3.13.0", + "filename": "asgiref-3.13.0-py3-none-any.whl", + "sha256": "f9596a2434512686473d50f96ff6207fbba49ae1a785dfac614df4aeb085674d", + "url": "https://files.pythonhosted.org/packages/2e/b0/ec211e1f7966247cb6469158d856bac9aa4c2fe13621bd6a352e4f850453/asgiref-3.13.0-py3-none-any.whl", "depends": [], "imports": [ "asgiref" ] }, + "shinychat": { + "name": "shinychat", + "version": "0.7.2", + "filename": "shinychat-0.7.2-py3-none-any.whl", + "sha256": "a063b4cb789019f7dd1e897cf67f351915c00e2fc34b957a3824fcaab1c702b0", + "url": "https://files.pythonhosted.org/packages/7d/92/8976398c152b6984723abed71e315ca1b572b791cc8f05cdf1b17157e8f0/shinychat-0.7.2-py3-none-any.whl", + "depends": [ + {"name": "htmltools", "specs": [[">=", "0.7.0"]]}, + {"name": "libsass", "specs": [[">=", "0.23.0"]]}, + {"name": "pydantic", "specs": [[">=", "2.11"]]}, + {"name": "shiny", "specs": [[">=", "1.7.0"]]} + ], + "imports": [ + "shinychat" + ] + }, + "opentelemetry-api": { + "name": "opentelemetry-api", + "version": "1.45.2", + "filename": "opentelemetry_api-1.45.2-py3-none-any.whl", + "sha256": "2cbd737377e710ee740d261c94e93fa0f08e6d037c18f17513cc0a2d94479a34", + "url": "https://files.pythonhosted.org/packages/a9/2d/610db864d7513158cebb8f36cc88e67e73bd6c7aa1905ef18d89e1281c81/opentelemetry_api-1.45.2-py3-none-any.whl", + "depends": [ + {"name": "typing-extensions", "specs": [[">=", "4.5.0"]]} + ], + "imports": [ + "opentelemetry_api" + ] + }, + "anywidget": { + "name": "anywidget", + "version": "0.11.0", + "filename": "anywidget-0.11.0-py3-none-any.whl", + "sha256": "c574d9acc6503ad27b37a9acea48f957a8ba7c9c9876cfcb37898931c098ce9d", + "url": "https://files.pythonhosted.org/packages/8e/c2/8fec8e8e2eb920cc2280f569144080cd58622a2eda83bfa4c0c354a63264/anywidget-0.11.0-py3-none-any.whl", + "depends": [ + {"name": "ipywidgets", "specs": [[">=", "7.6.0"]]}, + {"name": "psygnal", "specs": [[">=", "0.8.1"]]}, + {"name": "typing-extensions", "specs": [[">=", "4.2.0"]]} + ], + "imports": [ + "anywidget" + ] + }, "jupyter-core": { "name": "jupyter-core", - "version": "5.7.2", - "filename": "jupyter_core-5.7.2-py3-none-any.whl", - "sha256": "4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409", - "url": "https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl", + "version": "5.9.1", + "filename": "jupyter_core-5.9.1-py3-none-any.whl", + "sha256": "ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", + "url": "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", "depends": [ {"name": "platformdirs", "specs": [[">=", "2.5"]]}, {"name": "traitlets", "specs": [[">=", "5.3"]]} @@ -326,10 +357,10 @@ }, "mizani": { "name": "mizani", - "version": "0.13.1", - "filename": "mizani-0.13.1-py3-none-any.whl", - "sha256": "7da0dcacd43fbcc01c279ea06a76f1f064ae90dbb387c4a985ba24a92d3c7d7a", - "url": "https://files.pythonhosted.org/packages/29/85/16e17e75831ec01808c5f07e578f1552df87a4f5c827caa8be28f97b4c19/mizani-0.13.1-py3-none-any.whl", + "version": "0.14.6", + "filename": "mizani-0.14.6-py3-none-any.whl", + "sha256": "a3d3ec364018efaa0e259d530303576156bac7741faa66167c9b50aaf19c3d73", + "url": "https://files.pythonhosted.org/packages/d9/a8/4463130becffe3e091feed0f459008aeebc806998dff53166cc12063f6b0/mizani-0.14.6-py3-none-any.whl", "depends": [ {"name": "numpy", "specs": [[">=", "1.23.5"]]}, {"name": "scipy", "specs": [[">=", "1.8.0"]]}, @@ -340,36 +371,23 @@ "mizani" ] }, - "tenacity": { - "name": "tenacity", - "version": "8.5.0", - "filename": "tenacity-8.5.0-py3-none-any.whl", - "sha256": "b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", - "url": "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", - "depends": [], - "imports": [ - "tenacity" - ] - }, "comm": { "name": "comm", - "version": "0.2.2", - "filename": "comm-0.2.2-py3-none-any.whl", - "sha256": "e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", - "url": "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", - "depends": [ - {"name": "traitlets", "specs": [[">=", "4"]]} - ], + "version": "0.2.3", + "filename": "comm-0.2.3-py3-none-any.whl", + "sha256": "c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", + "url": "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", + "depends": [], "imports": [ "comm" ] }, "jupyterlab-widgets": { "name": "jupyterlab-widgets", - "version": "3.0.11", - "filename": "jupyterlab_widgets-3.0.11-py3-none-any.whl", - "sha256": "78287fd86d20744ace330a61625024cf5521e1c012a352ddc0a3cdc2348becd0", - "url": "https://files.pythonhosted.org/packages/65/f6/659ca44182c86f57977e946047c339c717745fda9f43b7ac47f274e86553/jupyterlab_widgets-3.0.11-py3-none-any.whl", + "version": "3.0.17", + "filename": "jupyterlab_widgets-3.0.17-py3-none-any.whl", + "sha256": "40ac1e9955acf116c4d995d9bfa082d86ad9ec6d91c4f134827cf5e0a5eb75e0", + "url": "https://files.pythonhosted.org/packages/33/ef/6d27fc118f58cb24886da413545a7efb0853d405fddbfd8b2d9ac09fbed4/jupyterlab_widgets-3.0.17-py3-none-any.whl", "depends": [], "imports": [ "jupyterlab_widgets" @@ -377,10 +395,10 @@ }, "branca": { "name": "branca", - "version": "0.7.2", - "filename": "branca-0.7.2-py3-none-any.whl", - "sha256": "853a359c34d08fd06498be762d8be9932750db4049cac11e25dd6f23562e25c2", - "url": "https://files.pythonhosted.org/packages/75/ca/6074ab4a04dd1a503201c18091b3426f3709670115fae316907a97f98d75/branca-0.7.2-py3-none-any.whl", + "version": "0.8.2", + "filename": "branca-0.8.2-py3-none-any.whl", + "sha256": "2ebaef3983e3312733c1ae2b793b0a8ba3e1c4edeb7598e10328505280cf2f7c", + "url": "https://files.pythonhosted.org/packages/7e/50/fc9680058e63161f2f63165b84c957a0df1415431104c408e8104a3a18ef/branca-0.8.2-py3-none-any.whl", "depends": [ {"name": "jinja2", "specs": [[">=", "3"]]} ], @@ -390,10 +408,10 @@ }, "jupyter-leaflet": { "name": "jupyter-leaflet", - "version": "0.19.2", - "filename": "jupyter_leaflet-0.19.2-py3-none-any.whl", - "sha256": "0d57e15e80c08a4360f0cde0b4c490beddc5d422bb0e9bc1c0b4479d3fb725a6", - "url": "https://files.pythonhosted.org/packages/81/7c/3ade59500a358b9018a996d00c26eaede79f76ed362b7051bc5038d4a0d4/jupyter_leaflet-0.19.2-py3-none-any.whl", + "version": "0.20.0", + "filename": "jupyter_leaflet-0.20.0-py3-none-any.whl", + "sha256": "2e27ce83647316424f04845e3a6af35e1ee44c177c318a145646b11f4afe0764", + "url": "https://files.pythonhosted.org/packages/16/95/ffe543060eb3b1570d78c3f2c1948c640a6758ff5c6479c27e474819115b/jupyter_leaflet-0.20.0-py3-none-any.whl", "depends": [], "imports": [ "jupyter_leaflet" @@ -401,10 +419,10 @@ }, "traittypes": { "name": "traittypes", - "version": "0.2.1", - "filename": "traittypes-0.2.1-py2.py3-none-any.whl", - "sha256": "1340af133810b6eee1a2eb2e988f862b0d12b6c2d16f282aaf3207b782134c2e", - "url": "https://files.pythonhosted.org/packages/9c/d1/8d5bd662703cc1764d986f6908a608777305946fa634d34c470cd4a1e729/traittypes-0.2.1-py2.py3-none-any.whl", + "version": "0.2.3", + "filename": "traittypes-0.2.3-py2.py3-none-any.whl", + "sha256": "49016082ce740d6556d9bb4672ee2d899cd14f9365f17cbb79d5d96b47096d4e", + "url": "https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl", "depends": [ {"name": "traitlets", "specs": [[">=", "4.2.2"]]} ], @@ -414,10 +432,10 @@ }, "mypy-extensions": { "name": "mypy-extensions", - "version": "1.0.0", - "filename": "mypy_extensions-1.0.0-py3-none-any.whl", - "sha256": "4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", - "url": "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", + "version": "1.1.0", + "filename": "mypy_extensions-1.1.0-py3-none-any.whl", + "sha256": "1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", + "url": "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", "depends": [], "imports": [ "mypy_extensions" @@ -425,35 +443,24 @@ }, "pathspec": { "name": "pathspec", - "version": "0.12.1", - "filename": "pathspec-0.12.1-py3-none-any.whl", - "sha256": "a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", - "url": "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", + "version": "1.1.1", + "filename": "pathspec-1.1.1-py3-none-any.whl", + "sha256": "a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", + "url": "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", "depends": [], "imports": [ "pathspec" ] }, - "platformdirs": { - "name": "platformdirs", - "version": "4.2.2", - "filename": "platformdirs-4.2.2-py3-none-any.whl", - "sha256": "2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", - "url": "https://files.pythonhosted.org/packages/68/13/2aa1f0e1364feb2c9ef45302f387ac0bd81484e9c9a4c5688a322fbdfd08/platformdirs-4.2.2-py3-none-any.whl", - "depends": [], - "imports": [ - "platformdirs" - ] - }, - "pypng": { - "name": "pypng", - "version": "0.20220715.0", - "filename": "pypng-0.20220715.0-py3-none-any.whl", - "sha256": "4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c", - "url": "https://files.pythonhosted.org/packages/3e/b9/3766cc361d93edb2ce81e2e1f87dd98f314d7d513877a342d31b30741680/pypng-0.20220715.0-py3-none-any.whl", + "pytokens": { + "name": "pytokens", + "version": "0.4.1", + "filename": "pytokens-0.4.1-py3-none-any.whl", + "sha256": "26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", + "url": "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", "depends": [], "imports": [ - "pypng" + "pytokens" ] }, "mdurl": { @@ -467,146 +474,15 @@ "mdurl" ] }, - "uc-micro-py": { - "name": "uc-micro-py", - "version": "1.0.3", - "filename": "uc_micro_py-1.0.3-py3-none-any.whl", - "sha256": "db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", - "url": "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", - "depends": [], - "imports": [ - "uc_micro" - ] - }, - "altair": { - "name": "altair", - "version": "5.5.0", - "filename": "altair-5.5.0-py3-none-any.whl", - "sha256": "91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", - "url": "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", - "depends": [ - {"name": "jinja2", "specs": []}, - {"name": "jsonschema", "specs": [[">=", "3.0"]]}, - {"name": "narwhals", "specs": [[">=", "1.14.2"]]}, - {"name": "packaging", "specs": []} - ], - "imports": [ - "altair" - ] - }, - "narwhals": { - "name": "narwhals", - "version": "2.28.0", - "filename": "narwhals-2.28.0-py3-none-any.whl", - "sha256": "1f69d6385ec30e2fa6e1fdeb37c562ecabd0f713f6f3e3b58a668c5c9df9567f", - "url": "https://files.pythonhosted.org/packages/cf/82/8ef9c7e0f50493307a49c09cfa7f00c5a778b6d1921f7b2dabe1d3a5809e/narwhals-2.28.0-py3-none-any.whl", - "depends": [], - "imports": [ - "narwhals" - ] - }, - "libsass": { - "name": "libsass", - "version": "0.23.0", - "filename": "libsass-0.23.0-cp312-abi3-pyodide_2024_0_wasm32.whl", - "sha256": null, - "url": null, - "depends": [], - "imports": [ - "libsass" - ] - }, - "jupyter_core": { - "name": "jupyter_core", - "version": "5.7.2", - "filename": "jupyter_core-5.7.2-py3-none-any.whl", - "sha256": "4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409", - "url": "https://files.pythonhosted.org/packages/c9/fb/108ecd1fe961941959ad0ee4e12ee7b8b1477247f30b1fdfd83ceaf017f0/jupyter_core-5.7.2-py3-none-any.whl", - "depends": [ - {"name": "platformdirs", "specs": [[">=", "2.5"]]}, - {"name": "traitlets", "specs": [[">=", "5.3"]]} - ], - "imports": [ - "jupyter_core" - ] - }, - "anywidget": { - "name": "anywidget", - "version": "0.9.15", - "filename": "anywidget-0.9.15-py3-none-any.whl", - "sha256": "fd7876332e47f380e0428f552f26b7227f5694d4e0a257bbc23354d9b9e9a73c", - "url": "https://files.pythonhosted.org/packages/3a/cf/4f2797204895846d2a33a5f8e0cffcc3e23493c6daa10d500f9929da6644/anywidget-0.9.15-py3-none-any.whl", - "depends": [ - {"name": "ipywidgets", "specs": [[">=", "7.6.0"]]}, - {"name": "psygnal", "specs": [[">=", "0.8.1"]]}, - {"name": "typing-extensions", "specs": [[">=", "4.2.0"]]} - ], - "imports": [ - "anywidget" - ] - }, "psygnal": { "name": "psygnal", - "version": "0.12.0", - "filename": "psygnal-0.12.0-py3-none-any.whl", - "sha256": "15f39abd8bee2926e79da76bec31a258d03dbe3e61d22d6251f65caefbae5d54", - "url": "https://files.pythonhosted.org/packages/eb/fa/84fc30ad391081cb119099aae5491ba4a9ebd34ce5139bf05b31813abb84/psygnal-0.12.0-py3-none-any.whl", + "version": "0.16.1", + "filename": "psygnal-0.16.1-py3-none-any.whl", + "sha256": "93b96894d8c46f0a3c0bfaaf8abe73f0d6db2c20cc6d303624cf08f85abc3c92", + "url": "https://files.pythonhosted.org/packages/be/f2/973c1280f035cd825af70582bbee776c69545e24534b324cf135eabef57e/psygnal-0.16.1-py3-none-any.whl", "depends": [], "imports": [ "psygnal" ] - }, - "shinychat": { - "name": "shinychat", - "version": "0.6.1", - "filename": "shinychat-0.6.1-py3-none-any.whl", - "sha256": "dc4c742bfec391e933ceaa470c12c7c5f204ba6bb53f97cc5ba3567720b0e525", - "url": "https://files.pythonhosted.org/packages/1b/a6/03d47e4257a9dce48f07a4c5934de02e300749c9a58f873294fc92652f8a/shinychat-0.6.1-py3-none-any.whl", - "depends": [ - {"name": "htmltools", "specs": [[">=", "0.7.0"]]}, - {"name": "pydantic", "specs": []}, - {"name": "shiny", "specs": [[">=", "1.4.0"]]} - ], - "imports": [ - "shinychat" - ] - }, - "opentelemetry-api": { - "name": "opentelemetry-api", - "version": "1.40.0", - "filename": "opentelemetry_api-1.40.0-py3-none-any.whl", - "sha256": "82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", - "url": "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", - "depends": [ - {"name": "importlib-metadata", "specs": [["<", "8.8.0"], [">=", "6.0"]]}, - {"name": "typing-extensions", "specs": [[">=", "4.5.0"]]} - ], - "imports": [ - "opentelemetry_api" - ] - }, - "importlib-metadata": { - "name": "importlib-metadata", - "version": "9.0.0", - "filename": "importlib_metadata-9.0.0-py3-none-any.whl", - "sha256": "2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", - "url": "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", - "depends": [ - {"name": "zipp", "specs": [[">=", "3.20"]]} - ], - "imports": [ - "importlib_metadata" - ] - }, - "zipp": { - "name": "zipp", - "version": "3.23.0", - "filename": "zipp-3.23.0-py3-none-any.whl", - "sha256": "071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", - "url": "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", - "depends": [], - "imports": [ - "zipp" - ] } } \ No newline at end of file diff --git a/shinylive_requirements.json b/shinylive_requirements.json index f6f2846e..7be362f8 100644 --- a/shinylive_requirements.json +++ b/shinylive_requirements.json @@ -12,6 +12,12 @@ }, { "name": "plotnine", "source": "pypi", "version": "latest" }, { "name": "plotly", "source": "pypi", "version": "latest" }, + { + "name": "altair", + "source": "pypi", + "version": "latest", + "comment": "Pyodide 314 ships altair 6.0.0, which sends NaN from pandas 3 string columns as invalid JSON. Fixed in 6.3.0." + }, { "name": "seaborn", "source": "pypi", "version": "latest" }, { "name": "ipywidgets", "source": "pypi", "version": "latest" }, { "name": "ipyleaflet", "source": "pypi", "version": "latest" }, diff --git a/src/hooks/usePyodide.tsx b/src/hooks/usePyodide.tsx index a9d4a533..95f9613a 100644 --- a/src/hooks/usePyodide.tsx +++ b/src/hooks/usePyodide.tsx @@ -204,6 +204,10 @@ def _pyodide_env_init(): # the default Canvas one. os.environ["MPLBACKEND"] = "AGG" + # There are no processes to fork in Pyodide. Without this, joblib (used by + # scikit-learn) warns at import that it will "operate in serial mode". + os.environ["JOBLIB_MULTIPROCESSING"] = "0" + # Add current directory to Python path. sys.path.insert(0, "") diff --git a/src/messageporthttp.ts b/src/messageporthttp.ts index 93981277..c7b8cf59 100644 --- a/src/messageporthttp.ts +++ b/src/messageporthttp.ts @@ -132,7 +132,7 @@ async function connect( } async function toClient(event: Record): Promise { - event = Object.fromEntries(event.toJs()); + event = event.toJs({ dict_converter: Object.fromEntries }); if (event.type === "http.response.start") { clientPort.postMessage({ type: event.type, diff --git a/src/messageportwebsocket-channel.ts b/src/messageportwebsocket-channel.ts index faafa0c5..d5fe1489 100644 --- a/src/messageportwebsocket-channel.ts +++ b/src/messageportwebsocket-channel.ts @@ -62,7 +62,7 @@ async function connect( // A function to be called by the ASGI app to send a message to the client. async function toClient(event: Record): Promise { - event = Object.fromEntries(event.toJs()); + event = event.toJs({ dict_converter: Object.fromEntries }); if (event.type === "websocket.accept") { // TODO: Also pass along event.subprotocol, event.headers conn.accept(); diff --git a/src/pyodide/ffi.d.ts b/src/pyodide/ffi.d.ts index d734a5b4..aa982e2d 100644 --- a/src/pyodide/ffi.d.ts +++ b/src/pyodide/ffi.d.ts @@ -1,14 +1,23 @@ // Generated by dts-bundle-generator v8.1.2 +/** + * @docgroup pyodide.ffi + */ export type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array; interface PyProxy { [x: string]: any; } +declare const dispose: symbol; /** * A :js:class:`~pyodide.ffi.PyProxy` is an object that allows idiomatic use of a Python object from * JavaScript. See :ref:`type-translations-pyproxy`. */ declare class PyProxy { + /** + * JavaScript resource management + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management#the_using_and_await_using_declarations + */ + [Symbol.dispose]: () => void; /** @private */ $$flags: number; /** @private */ @@ -37,7 +46,7 @@ declare class PyProxy { get type(): string; /** * Returns `str(o)` (unless `pyproxyToStringRepr: true` was passed to - * :js:func:`~globalThis.loadPyodide` in which case it will return `repr(o)`) + * :js:func:`~exports.loadPyodide` in which case it will return `repr(o)`) */ toString(): string; /** @@ -123,7 +132,7 @@ declare class PyProxy { */ declare class PyProxyWithLength extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyProxyWithLength; } interface PyProxyWithLength extends PyLengthMethods { } @@ -139,7 +148,7 @@ declare class PyLengthMethods { */ declare class PyProxyWithGet extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyProxyWithGet; } interface PyProxyWithGet extends PyGetItemMethods { } @@ -173,7 +182,7 @@ declare class PyGetItemMethods { */ declare class PyProxyWithSet extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyProxyWithSet; } interface PyProxyWithSet extends PySetItemMethods { } @@ -198,7 +207,7 @@ declare class PySetItemMethods { */ declare class PyProxyWithHas extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyProxyWithHas; } interface PyProxyWithHas extends PyContainsMethods { } @@ -217,7 +226,7 @@ declare class PyContainsMethods { */ declare class PyIterable extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyIterable; } interface PyIterable extends PyIterableMethods { } @@ -237,7 +246,7 @@ declare class PyIterableMethods { */ declare class PyAsyncIterable extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyAsyncIterable; } interface PyAsyncIterable extends PyAsyncIterableMethods { } @@ -256,7 +265,7 @@ declare class PyAsyncIterableMethods { */ declare class PyIterator extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyIterator; } interface PyIterator extends PyIteratorMethods { } @@ -285,7 +294,7 @@ declare class PyIteratorMethods { */ declare class PyGenerator extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyGenerator; } interface PyGenerator extends PyGeneratorMethods { } @@ -328,7 +337,7 @@ declare class PyGeneratorMethods { */ declare class PyAsyncIterator extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyAsyncIterator; } interface PyAsyncIterator extends PyAsyncIteratorMethods { } @@ -358,7 +367,7 @@ declare class PyAsyncIteratorMethods { */ declare class PyAsyncGenerator extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyAsyncGenerator; } interface PyAsyncGenerator extends PyAsyncGeneratorMethods { } @@ -400,7 +409,7 @@ declare class PyAsyncGeneratorMethods { */ declare class PySequence extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PySequence; } interface PySequence extends PySequenceMethods { } @@ -600,7 +609,7 @@ declare class PySequenceMethods { */ declare class PyMutableSequence extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyMutableSequence; } interface PyMutableSequence extends PyMutableSequenceMethods { } @@ -687,7 +696,7 @@ declare class PyMutableSequenceMethods { */ declare class PyAwaitable extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyAwaitable; } interface PyAwaitable extends Promise { } @@ -889,6 +898,11 @@ declare class PyBufferMethods { * data, so you might want to pass ``'dataview'`` as the type argument in that * case. * + * When you are done with the buffer view, you have to call + * :js:func:`~PyBufferView.release`. Alternatively, if you declare the buffer + * with `using pybuf = proxy.getBuffer()`, JavaScript will automatically + * release the buffer at the end of the current scope. + * * @param type The type of the :js:attr:`~pyodide.ffi.PyBufferView.data` field * in the output. Should be one of: ``"i8"``, ``"u8"``, ``"u8clamped"``, * ``"i16"``, ``"u16"``, ``"i32"``, ``"u32"``, ``"i32"``, ``"u32"``, @@ -904,16 +918,19 @@ declare class PyBufferMethods { */ declare class PyDict extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyDict; } interface PyDict extends PyProxyWithGet, PyProxyWithSet, PyProxyWithHas, PyProxyWithLength, PyIterable { } /** * A class to allow access to Python data buffers from JavaScript. These are - * produced by :js:meth:`~pyodide.ffi.PyBuffer.getBuffer` and cannot be constructed directly. - * When you are done, release it with the :js:func:`~PyBufferView.release` method. - * See the Python :external:doc:`c-api/buffer` documentation for more - * information. + * produced by :js:meth:`~pyodide.ffi.PyBuffer.getBuffer` and cannot be + * constructed directly. When you are done, release it with the + * :js:func:`~PyBufferView.release` method. It has a `[Symbol.dispose]()` method + * which is identical to the `release` method, so if you create the buffer with + * `using pybuf = proxy.getBuffer();` and JavaScript will automatically release + * it at the end of the scope. See the Python :external:doc:`c-api/buffer` + * documentation for more information. * * To find the element ``x[a_1, ..., a_n]``, you could use the following code: * @@ -961,6 +978,7 @@ interface PyDict extends PyProxyWithGet, PyProxyWithSet, PyProxyWithHas, PyProxy * ); */ declare class PyBufferView { + [Symbol.dispose]: () => void; /** * The offset of the first entry of the array. For instance if our array * is 3d, then you will find ``array[0,0,0]`` at diff --git a/src/pyodide/pyodide.d.ts b/src/pyodide/pyodide.d.ts index 6a1987cf..505381ac 100644 --- a/src/pyodide/pyodide.d.ts +++ b/src/pyodide/pyodide.d.ts @@ -1,20 +1,22 @@ // Generated by dts-bundle-generator v8.1.2 -/** - * - * The Pyodide version. - * - * The version here is a Python version, following :pep:`440`. This is different - * from the version in ``package.json`` which follows the node package manager - * version convention. - */ -export declare const version: string; +import { ReadableStream, WritableStream } from 'node:stream/web'; + interface CanvasInterface { setCanvas2D(canvas: HTMLCanvasElement): void; getCanvas2D(): HTMLCanvasElement | undefined; setCanvas3D(canvas: HTMLCanvasElement): void; getCanvas3D(): HTMLCanvasElement | undefined; } +interface Writer { + isatty?: boolean; + fsync?: () => void; + write(buffer: Uint8Array): number; + getTerminalSize?: () => { + rows: number; + columns: number; + } | undefined; +} type InFuncType = () => null | undefined | string | ArrayBuffer | Uint8Array | number; declare function setStdin(options?: { stdin?: InFuncType; @@ -23,81 +25,414 @@ declare function setStdin(options?: { isatty?: boolean; autoEOF?: boolean; }): void; -declare function setStdout(options?: { - batched?: (output: string) => void; - raw?: (charCode: number) => void; - write?: (buffer: Uint8Array) => number; - isatty?: boolean; -}): void; -declare function setStderr(options?: { - batched?: (output: string) => void; - raw?: (charCode: number) => void; - write?: (buffer: Uint8Array) => number; +interface BatchedWriteHandler { + batched: (output: string) => void; +} +interface RawWriteHandler { + raw: (charCode: number) => void; isatty?: boolean; -}): void; + getTerminalSize?: () => { + rows: number; + columns: number; + } | undefined; +} +type StdWriteOpts = BatchedWriteHandler | RawWriteHandler | Writer; +declare function setStdout(options?: StdWriteOpts | {}): void; +declare function setStderr(options?: StdWriteOpts | {}): void; +interface RuntimeEnv extends BaseRuntimeEnv { + IN_NODE_COMMONJS: boolean; + IN_NODE_ESM: boolean; + IN_BROWSER: boolean; + IN_BROWSER_MAIN_THREAD: boolean; + IN_BROWSER_WEB_WORKER: boolean; +} +interface BaseRuntimeEnv { + IN_NODE: boolean; + IN_BUN: boolean; + IN_DENO: boolean; + IN_SAFARI: boolean; + IN_SHELL: boolean; + IN_WORKERD: boolean; +} +interface SocketOptions { + /** + * Specifies whether or not to use TLS when creating the TCP socket. + * + * `off` + * Do not use TLS. + * `on` + * Use TLS. + * `starttls` + * Do not use TLS initially, but allow the socket to be + * upgraded to use TLS by calling startTls(). + */ + secureTransport?: "off" | "on" | "starttls"; + /** + * Defines whether the writable side of the TCP socket will automatically + * close on end-of-file (EOF). + */ + allowHalfOpen?: boolean; +} +interface SocketAddress { + /** The hostname to connect to. Example: `cloudflare.com`. */ + hostname: string; + /** The port number to connect to. Example: `5432`. */ + port: number; +} +interface SocketInfo { + remoteAddress?: string; + localAddress?: string; +} +type ConnectFunc = (address: SocketAddress | string, options?: SocketOptions) => Socket; +declare class Socket { + readable: ReadableStream; + writable: WritableStream; + opened: Promise; + closed: Promise; + private _socket; + private allowHalfOpen; + private secureTransport; + private openedIsResolved; + private openedResolve; + private openedReject; + private closedResolve; + private closedReject; + private startTlsCalled; + constructor(addressOrSocket: SocketAddress | import("node:net").Socket, options?: SocketOptions); + close(): Promise; + startTls(): Socket; +} +declare function initializeNodeSockFS(connectFunc?: ConnectFunc): Promise; +type SerializedHiwireValue = { + path: string[]; +} | { + serialized: any; +} | { + API: true; +} | { + abortSignalAny: true; +} | null; +type SnapshotConfig = { + hiwireKeys: SerializedHiwireValue[]; + immortalKeys: string[]; +}; +declare class PackageManager { + #private; + /** + * Only used in Node. If we can't find a package in node_modules, we'll use this + * to fetch the package from the cdn (and we'll store it into node_modules so + * subsequent loads don't require a web request). + * + * exported for testing purposes. + */ + cdnURL: string; + /** + * The set of loaded packages. + * This is exposed as a global variable and can be modified by micropip + * + * TODO: Make this private and expose a setter + */ + loadedPackages: LoadedPackages; + private _lock; + installBaseUrl?: string; + /** + * The function to use for stdout and stderr, defaults to console.log and console.error + */ + private stdout; + private stderr; + /** + * Buffers for store stdout and stderr messages temporarily. + * These are used to store the messages that are printed before the + * stdout and stderr functions are set. + */ + private streamReady; + private stdoutBuffer; + private stderrBuffer; + private defaultChannel; + constructor(api: PackageManagerAPI, pyodideModule: PackageManagerModule); + /** + * Load packages from the Pyodide distribution or Python wheels by URL. + * + * This installs packages in the virtual filesystem. Packages + * needs to be imported from Python before it can be used. + * + * This function can only install packages included in the Pyodide distribution, + * or Python wheels by URL, without dependency resolution. It is significantly + * more limited in terms of functionality as compared to :mod:`micropip`, + * however it has less overhead and can be faster. + * + * When installing binary wheels by URLs it is user's responsibility to check + * that the installed binary wheel is compatible in terms of Python and + * Emscripten versions. Compatibility is not checked during installation time + * (unlike with micropip). If a wheel for the wrong Python/Emscripten version + * is installed it would fail at import time. + * + * + * @param names Either a single package name or URL or a list of them. URLs can + * be absolute or relative. The URLs must correspond to Python wheels: + * either pure Python wheels, with a file name ending with ``none-any.whl`` + * or Emscripten/WASM 32 wheels, with a file name ending with + * ``cp_emscripten__wasm32.whl``. + * The argument can be a :js:class:`~pyodide.ffi.PyProxy` of a list, in + * which case the list will be converted to JavaScript and the + * :js:class:`~pyodide.ffi.PyProxy` will be destroyed. + * @param options + * @param options.messageCallback A callback, called with progress messages + * (optional) + * @param options.errorCallback A callback, called with error/warning messages + * (optional) + * @param options.checkIntegrity If true, check the integrity of the downloaded + * packages (default: true) + * @returns The loaded package data. + */ + loadPackage(names: string | PyProxy | Array, options?: { + messageCallback?: (message: string) => void; + errorCallback?: (message: string) => void; + checkIntegrity?: boolean; + }): Promise; + loadPackageInner(names: string | PyProxy | string[], options?: { + messageCallback?: (message: string) => void; + errorCallback?: (message: string) => void; + checkIntegrity?: boolean; + }): Promise>; + /** + * Recursively add a package and its dependencies to toLoad. + * A helper function for recursiveDependencies. + * @param name The package to add + * @param toLoad The set of names of packages to load + * @private + */ + private addPackageToLoad; + /** + * Calculate the dependencies of a set of packages + * @param names The list of names whose dependencies we need to calculate. + * @returns The map of package names to PackageLoadMetadata + * @private + */ + private recursiveDependencies; + /** + * Download a package. If `channel` is `DEFAULT_CHANNEL`, look up the wheel URL + * relative to packageCacheDir (when IN_NODE), or to lockfileURL, otherwise use the URL specified by + * `channel`. + * @param pkg The package to download + * @param channel Either `DEFAULT_CHANNEL` or the absolute URL to the + * wheel or the path to the wheel relative to packageCacheDir (when IN_NODE), or lockfileURL. + * @param checkIntegrity Whether to check the integrity of the downloaded + * package. + * @returns The binary data for the package + * @private + */ + private downloadPackage; + /** + * Install the package into the file system. + * @param metadata The package metadata + * @param buffer The binary data returned by downloadPackage + * @private + */ + private installPackage; + /** + * Download and install the package. + * Downloads can be done in parallel, but installs must be done for dependencies first. + * @param pkg The package to load + * @param toLoad The map of package names to PackageLoadMetadata + * @param loaded The set of loaded package metadata, this will be updated by this function. + * @param failed The map of , this will be updated by this function. + * @param checkIntegrity Whether to check the integrity of the downloaded + * package. + * @private + */ + private downloadAndInstall; + /** + * Flushes the stdout and stderr buffers, that were collected before the + * stdout and stderr functions were set. + */ + flushBuffers(): void; + /** + * getLoadedPackageChannel returns the channel from which a package was loaded. + * if the package is not loaded, it returns null. + * @param pkg package name + */ + getLoadedPackageChannel(pkg: string): string | null; + setCallbacks(stdout?: (message: string) => void, stderr?: (message: string) => void): any>(fn: T) => (...args: Parameters) => ReturnType; + logStdout(message: string): void; + logStderr(message: string): void; +} +/** + * @docgroup pyodide.ffi + */ /** @deprecated Use `import type { TypedArray } from "pyodide/ffi"` instead */ export type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array; -type FSNode = { - timestamp: number; - rdev: number; - contents: Uint8Array; - mode: number; -}; -type FSStream = { - tty?: { - ops: object; +type FSType = typeof FS; +type PreRunFunc = (Module: PyodideModule) => void; +type DSO = any; +interface LDSO { + loadedLibsByName: { + [key: string]: DSO; }; - seekable?: boolean; - stream_ops: FSStreamOps; - node: FSNode; -}; -type FSStreamOps = FSStreamOpsGen; -type FSStreamOpsGen = { - open: (a: T) => void; - close: (a: T) => void; - fsync: (a: T) => void; - read: (a: T, b: Uint8Array, offset: number, length: number, pos: number) => number; - write: (a: T, b: Uint8Array, offset: number, length: number, pos: number) => number; -}; -interface FSType { - unlink: (path: string) => void; - mkdirTree: (path: string, mode?: number) => void; - chdir: (path: string) => void; - symlink: (target: string, src: string) => FSNode; - createDevice: ((parent: string, name: string, input?: (() => number | null) | null, output?: ((code: number) => void) | null) => FSNode) & { - major: number; +} +interface EmscriptenModule { + locateFile: (file: string) => string; + exited?: { + toThrow: any; + }; + ENV: { + [key: string]: string; + }; + PATH: { + join2(a: string, b: string): string; + dirname(path: string): string; + basename(path: string): string; + normalize(path: string): string; }; - closeStream: (fd: number) => void; - open: (path: string, flags: string | number, mode?: number) => FSStream; - makedev: (major: number, minor: number) => number; - mkdev: (path: string, dev: number) => FSNode; - filesystems: any; - stat: (path: string, dontFollow?: boolean) => any; - readdir: (path: string) => string[]; - isDir: (mode: number) => boolean; - isMountpoint: (mode: FSNode) => boolean; - lookupPath: (path: string, options?: { - follow_mount?: boolean; - }) => { - node: FSNode; + TTY: any; + FS: FSType; + LDSO: LDSO; + canvas?: HTMLCanvasElement; + addRunDependency(id: string): void; + removeRunDependency(id: string): void; + getDylinkMetadata(binary: Uint8Array | WebAssembly.Module): { + neededDynlibs: string[]; }; - isFile: (mode: number) => boolean; - writeFile: (path: string, contents: any, o?: { - canOwn?: boolean; - }) => void; - chmod: (path: string, mode: number) => void; - utime: (path: string, atime: number, mtime: number) => void; - rmdir: (path: string) => void; - mount: (type: any, opts: any, mountpoint: string) => any; - write: (stream: FSStream, buffer: any, offset: number, length: number, position?: number) => number; - close: (stream: FSStream) => void; - ErrnoError: { - new (errno: number): Error; + ERRNO_CODES: { + [k: string]: number; }; - registerDevice(dev: number, ops: FSStreamOpsGen): void; - syncfs(dir: boolean, oncomplete: (val: void) => void): void; - findObject(a: string, dontResolveLastLink?: boolean): any; - readFile(a: string): Uint8Array; + stringToNewUTF8(x: string): number; + stringToUTF8OnStack: (str: string) => number; + HEAP8: Uint8Array; + HEAPU8: Uint8Array; + HEAP32: Int32Array; + HEAPU32: Uint32Array; + HEAP16: Int16Array; + SOCKFS: any; + DNS: { + lookup_addr: (addr: string) => string | null; + }; + getSocketAddress: (addr: number, addrlen: number) => any; + getExceptionMessage(e: number): [ + string, + string + ]; + exitCode: number | undefined; + ExitStatus: { + new (exitCode: number): Error; + }; + _free: (ptr: number) => void; + stackSave: () => number; + stackRestore: (ptr: number) => void; + promiseMap: { + free(id: number): void; + }; + _emscripten_dlopen_promise(lib: number, flags: number): number; + _dlerror(): number; + UTF8ToString: (ptr: number, maxBytesToRead: number, ignoreNul?: boolean) => string; +} +/** @deprecated Use `import type { PythonModule } from "pyodide/ffi"` instead */ +interface PythonModule extends EmscriptenModule { + _Py_EMSCRIPTEN_SIGNAL_HANDLING: number; + Py_EmscriptenSignalBuffer: TypedArray; + _Py_Version: number; +} +/** @deprecated Use `import type { PyodideModule } from "pyodide/ffi"` instead */ +interface PyodideModule extends PythonModule { + API: API; + _compat_to_string_repr: number; + _compat_null_to_none: number; + _compat_dict_to_literalmap: number; + js2python_convert: (obj: any, options: { + depth?: number; + defaultConverter?: (value: any, converter: (value: any) => any, cacheConversion: (input: any, output: any) => void) => any; + }) => any; + _PropagatePythonError: typeof Error; + __hiwire_get(a: number): any; + __hiwire_set(a: number, b: any): void; + __hiwire_immortal_add(a: any): void; + _jslib_init(): number; + _init_pyodide_proxy(): number; + handle_js_error(e: any): void; + _print_stdout: (ptr: number) => void; + _print_stderr: (ptr: number) => void; + getPromise(p: number): Promise; +} +/** + * The lockfile platform info. The ``abi_version`` field is used to check if the + * lockfile is compatible with the interpreter. The remaining fields are + * informational. + */ +interface LockfileInfo { + /** + * Machine architecture. At present, only can be wasm32. Pyodide has no wasm64 + * build. + */ + arch: "wasm32"; + /** + * The ABI version is structured as ``yyyy_patch``. For the lockfile to be + * compatible with the current interpreter this field must match exactly with + * the ABI version of the interpreter. + */ + abi_version: string; + /** + * The Emscripten versions for instance, `emscripten_4_0_9`. Different + * Emscripten versions have different ABIs so if this changes ``abi_version`` + * must also change. + */ + platform: string; + /** + * The Pyodide version the lockfile was made with. Informational only, has no + * compatibility implications. May be removed in the future. + */ + version: string; + /** + * The Python version this lock file was made with. If the minor version + * changes (e.g, 3.12 to 3.13) this changes the ABI and the ``abi_version`` + * must change too. Patch versions do not imply a change to the + * ``abi_version``. + */ + python: string; +} +/** + * A package entry in the lock file. + */ +interface LockfilePackage { + /** + * The unnormalized name of the package. + */ + name: string; + version: string; + /** + * The file name or url of the package wheel. If it's relative, it will be + * resolved with respect to ``packageBaseUrl``. If there is no + * ``packageBaseUrl``, attempting to install a package with a relative + * ``file_name`` will fail. + */ + file_name: string; + package_type: PackageType; + /** + * The installation directory. Will be ``site`` except for certain system + * dynamic libraries that need to go on the global LD_LIBRARY_PATH. + */ + install_dir: "site" | "dynlib"; + /** + * Integrity. Must be present unless ``checkIntegrity: false`` is passed to + * ``loadPyodide``. + */ + sha256: string; + /** + * The set of imports provided by this package as best we can tell. Used by + * :js:func:`pyodide.loadPackagesFromImports` to work out what packages to + * install. + */ + imports: string[]; + /** + * The set of dependencies of this package. + */ + depends: string[]; +} +/** + * The type of a package lockfile. + */ +interface Lockfile { + info: LockfileInfo; + packages: Record; } type PackageType = "package" | "cpython_module" | "shared_library" | "static_library"; interface PackageData { @@ -108,11 +443,107 @@ interface PackageData { packageType: PackageType; } type LoadedPackages = Record; +interface API { + runtimeEnv: RuntimeEnv; + fatal_error: (e: any) => never; + isPyProxy: (e: any) => e is PyProxy; + debug_ffi: boolean; + maybe_fatal_error: (e: any) => void; + public_api: PyodideAPI; + config: PyodideConfigWithDefaults; + packageIndexReady: Promise; + bootstrapFinalizedPromise: Promise; + typedArrayAsUint8Array: (buffer: TypedArray | ArrayBuffer) => Uint8Array; + initializeStreams: (stdin?: InFuncType | undefined, stdout?: ((a: string) => void) | undefined, stderr?: ((a: string) => void) | undefined) => void; + getTypeTag: (o: any) => string; + inTestHoist?: boolean; + on_fatal?: (e: any) => void; + _skip_unwind_fatal_error?: boolean; + capture_stderr: () => void; + restore_stderr: () => string; + fatal_loading_error: (...args: string[]) => never; + PythonError: any; + NoGilError: any; + errorConstructors: Map; + deserializeError: (name: string, message: string, stack: string) => Error; + setPyProxyToStringMethod: (useRepr: boolean) => void; + setCompatNullToNone: (compat: boolean) => void; + setCompatToJsLiteralMap: (compat: boolean) => void; + _pyodide: any; + pyodide_py: any; + pyodide_code: any; + pyodide_ffi: any; + pyodide_base: any; + globals: PyProxy; + rawRun: (code: string) => [ + number, + string + ]; + runPythonInternal: (code: string) => any; + runPythonInternal_dict: any; + saveState: () => any; + restoreState: (state: any) => void; + scheduleCallback: (callback: () => void, timeout: number) => void; + package_loader: any; + importlib: any; + _import_name_to_package_name: Map; + lockFilePromise: Promise; + lockfile_unvendored_stdlibs: string[]; + lockfile_unvendored_stdlibs_and_test: string[]; + lockfile: Lockfile; + lockfile_info: LockfileInfo; + lockfile_packages: Record; + packageManager: PackageManager; + flushPackageManagerBuffers: () => void; + defaultLdLibraryPath: string[]; + sitepackages: string; + loadBinaryFile: (path: string, file_sub_resource_hash?: string | undefined) => Promise; + loadDynlib: (lib: string, global: boolean, searchDirs?: string[] | undefined, readFileFunc?: (path: string) => Uint8Array) => Promise; + install: (buffer: Uint8Array, filename: string, installDir: string, metadata?: ReadonlyMap) => Promise; + _Comlink: any; + dsodir: string; + sys: PyProxy; + os: PyProxy; + restoreSnapshot(snapshot: Uint8Array): SnapshotConfig; + serializeHiwireState(serializer?: (obj: any) => any): SnapshotConfig; + makeSnapshot(serializer?: (obj: any) => any): Uint8Array; + saveSnapshot(): Uint8Array; + getExpectedKeys(): any[]; + finalizeBootstrap: (fromSnapshot?: SnapshotConfig, snapshotDeserializer?: (obj: any) => any) => PyodideAPI; + syncUpSnapshotLoad3(conf: SnapshotConfig): void; + abortSignalAny: (signals: AbortSignal[]) => AbortSignal; + version: string; + abiVersion: string; + pyVersionTuple: [ + number, + number, + number + ]; + LiteralMap: any; + sitePackages: string; + initializeNodeSockFS: typeof initializeNodeSockFS; + _nodeSock: { + connect: (fd: number, host: string, port: number) => Promise; + recv: (fd: number, nbytes: number) => Promise; + send: (fd: number, data: any) => Promise; + startTls: (fd: number) => Promise; + }; +} +type PackageManagerAPI = Pick & { + config: Pick; +}; +type PackageManagerModule = Pick; /** @deprecated Use `import type { PyProxy } from "pyodide/ffi"` instead */ interface PyProxy { [x: string]: any; } +declare const dispose: symbol; declare class PyProxy { + /** + * JavaScript resource management + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management#the_using_and_await_using_declarations + */ + [Symbol.dispose]: () => void; /** @private */ $$flags: number; /** @private */ @@ -141,7 +572,7 @@ declare class PyProxy { get type(): string; /** * Returns `str(o)` (unless `pyproxyToStringRepr: true` was passed to - * :js:func:`~globalThis.loadPyodide` in which case it will return `repr(o)`) + * :js:func:`~exports.loadPyodide` in which case it will return `repr(o)`) */ toString(): string; /** @@ -223,7 +654,7 @@ declare class PyProxy { } declare class PyProxyWithLength extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyProxyWithLength; } /** @deprecated Use `import type { PyProxyWithLength } from "pyodide/ffi"` instead */ interface PyProxyWithLength extends PyLengthMethods { @@ -236,7 +667,7 @@ declare class PyLengthMethods { } declare class PyProxyWithGet extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyProxyWithGet; } /** @deprecated Use `import type { PyProxyWithGet } from "pyodide/ffi"` instead */ interface PyProxyWithGet extends PyGetItemMethods { @@ -267,7 +698,7 @@ declare class PyGetItemMethods { } declare class PyProxyWithSet extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyProxyWithSet; } /** @deprecated Use `import type { PyProxyWithSet } from "pyodide/ffi"` instead */ interface PyProxyWithSet extends PySetItemMethods { @@ -289,7 +720,7 @@ declare class PySetItemMethods { } declare class PyProxyWithHas extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyProxyWithHas; } /** @deprecated Use `import type { PyProxyWithHas } from "pyodide/ffi"` instead */ interface PyProxyWithHas extends PyContainsMethods { @@ -305,7 +736,7 @@ declare class PyContainsMethods { } declare class PyIterable extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyIterable; } /** @deprecated Use `import type { PyIterable } from "pyodide/ffi"` instead */ interface PyIterable extends PyIterableMethods { @@ -322,7 +753,7 @@ declare class PyIterableMethods { } declare class PyAsyncIterable extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyAsyncIterable; } /** @deprecated Use `import type { PyAsyncIterable } from "pyodide/ffi"` instead */ interface PyAsyncIterable extends PyAsyncIterableMethods { @@ -338,7 +769,7 @@ declare class PyAsyncIterableMethods { } declare class PyIterator extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyIterator; } /** @deprecated Use `import type { PyIterator } from "pyodide/ffi"` instead */ interface PyIterator extends PyIteratorMethods { @@ -364,7 +795,7 @@ declare class PyIteratorMethods { } declare class PyGenerator extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyGenerator; } /** @deprecated Use `import type { PyGenerator } from "pyodide/ffi"` instead */ interface PyGenerator extends PyGeneratorMethods { @@ -404,7 +835,7 @@ declare class PyGeneratorMethods { } declare class PyAsyncIterator extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyAsyncIterator; } /** @deprecated Use `import type { PyAsyncIterator } from "pyodide/ffi"` instead */ interface PyAsyncIterator extends PyAsyncIteratorMethods { @@ -430,7 +861,7 @@ declare class PyAsyncIteratorMethods { } declare class PyAsyncGenerator extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyAsyncGenerator; } /** @deprecated Use `import type { PyAsyncGenerator } from "pyodide/ffi"` instead */ interface PyAsyncGenerator extends PyAsyncGeneratorMethods { @@ -469,7 +900,7 @@ declare class PyAsyncGeneratorMethods { } declare class PySequence extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PySequence; } /** @deprecated Use `import type { PySequence } from "pyodide/ffi"` instead */ interface PySequence extends PySequenceMethods { @@ -666,7 +1097,7 @@ declare class PySequenceMethods { } declare class PyMutableSequence extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyMutableSequence; } /** @deprecated Use `import type { PyMutableSequence } from "pyodide/ffi"` instead */ interface PyMutableSequence extends PyMutableSequenceMethods { @@ -750,7 +1181,7 @@ declare class PyMutableSequenceMethods { } declare class PyAwaitable extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyAwaitable; } /** @deprecated Use `import type { PyAwaitable } from "pyodide/ffi"` instead */ interface PyAwaitable extends Promise { @@ -944,6 +1375,11 @@ declare class PyBufferMethods { * data, so you might want to pass ``'dataview'`` as the type argument in that * case. * + * When you are done with the buffer view, you have to call + * :js:func:`~PyBufferView.release`. Alternatively, if you declare the buffer + * with `using pybuf = proxy.getBuffer()`, JavaScript will automatically + * release the buffer at the end of the current scope. + * * @param type The type of the :js:attr:`~pyodide.ffi.PyBufferView.data` field * in the output. Should be one of: ``"i8"``, ``"u8"``, ``"u8clamped"``, * ``"i16"``, ``"u16"``, ``"i32"``, ``"u32"``, ``"i32"``, ``"u32"``, @@ -956,13 +1392,14 @@ declare class PyBufferMethods { } declare class PyDict extends PyProxy { /** @private */ - static [Symbol.hasInstance](obj: any): obj is PyProxy; + static [Symbol.hasInstance](obj: any): obj is PyDict; } /** @deprecated Use `import type { PyDict } from "pyodide/ffi"` instead */ interface PyDict extends PyProxyWithGet, PyProxyWithSet, PyProxyWithHas, PyProxyWithLength, PyIterable { } /** @deprecated Use `import type { PyBufferView } from "pyodide/ffi"` instead */ declare class PyBufferView { + [Symbol.dispose]: () => void; /** * The offset of the first entry of the array. For instance if our array * is 3d, then you will find ``array[0,0,0]`` at @@ -1076,7 +1513,7 @@ declare class PythonError extends Error { type NativeFS = { syncfs: () => Promise; }; -declare class PyodideAPI { +declare class PyodideAPI_ { /** @hidden */ static version: string; /** @hidden */ @@ -1084,7 +1521,7 @@ declare class PyodideAPI { messageCallback?: (message: string) => void; errorCallback?: (message: string) => void; checkIntegrity?: boolean; - }) => Promise>; + }) => Promise; /** @hidden */ static loadedPackages: LoadedPackages; /** @hidden */ @@ -1211,6 +1648,8 @@ declare class PyodideAPI { * Defaults to ``""``. If a custom file name is given, the * traceback for any exception that is thrown will show source lines * (unless the given file name starts with ``<`` and ends with ``>``). + * @param options.dedent An optional boolean indicating whether ``code`` + * should be dedented before being run. Defaults to ``true``. * @returns The result of the Python code translated to JavaScript. See the * documentation for :py:func:`~pyodide.code.eval_code` for more info. * @example @@ -1233,6 +1672,7 @@ declare class PyodideAPI { globals?: PyProxy; locals?: PyProxy; filename?: string; + dedent?: boolean; }): any; /** * Run a Python code string with top level await using @@ -1271,12 +1711,15 @@ declare class PyodideAPI { * Defaults to ``""``. If a custom file name is given, the * traceback for any exception that is thrown will show source lines * (unless the given file name starts with ``<`` and ends with ``>``). + * @param options.dedent An optional boolean indicating whether ``code`` + * should be dedented before being run. Defaults to ``true``. * @returns The result of the Python code translated to JavaScript. */ static runPythonAsync(code: string, options?: { globals?: PyProxy; locals?: PyProxy; filename?: string; + dedent?: boolean; }): Promise; /** * Registers the JavaScript object ``module`` as a JavaScript module named @@ -1412,6 +1855,17 @@ declare class PyodideAPI { * @param hostPath The host path to mount. It must be a directory that exists. */ static mountNodeFS(emscriptenPath: string, hostPath: string): void; + /** + * Use Node.js native socket filesystem instead of Emscripten's SOCKFS. + * @experimental + */ + static useNodeSockFS( + /** + * @hidden + */ + options?: { + connect?: ConnectFunc; + }): Promise; /** * Tell Pyodide about Comlink. * Necessary to enable importing Comlink proxies into Python. @@ -1454,59 +1908,63 @@ declare class PyodideAPI { */ static setDebug(debug: boolean): boolean; /** - * - * @param param0 - * @returns + * @private */ static makeMemorySnapshot({ serializer, }?: { serializer?: (obj: any) => any; }): Uint8Array; + /** + * Returns the pyodide lockfile used to load the current Pyodide instance. + * The format of the lockfile is defined in the `pyodide/pyodide-lock + * `_ repository. + */ + static get lockfile(): Lockfile; + /** + * Returns the URL or path with respect to which relative paths in the lock + * file are resolved, or undefined. + */ + static get lockfileBaseUrl(): string | undefined; } -/** @hidden */ -export type PyodideInterface = typeof PyodideAPI; /** - * See documentation for loadPyodide. - * @hidden + * The return type of :js:func:`~exports.loadPyodide`. See + * :ref:`the pyodide api docs ` for more information. + * @hidetype + * @docgroup exports */ -type ConfigType = { - indexURL: string; - packageCacheDir: string; - lockFileURL: string; - fullStdLib?: boolean; - stdLibURL?: string; - stdin?: () => string; - stdout?: (msg: string) => void; - stderr?: (msg: string) => void; - jsglobals?: object; - _sysExecutable?: string; - args: string[]; - fsInit?: (FS: FSType, info: { - sitePackages: string; - }) => Promise; - env: { - [key: string]: string; - }; - packages: string[]; - _makeSnapshot: boolean; - enableRunUntilComplete: boolean; - checkAPIVersion: boolean; - BUILD_ID: string; -}; +export type PyodideAPI = typeof PyodideAPI_; +interface EmscriptenSettings { + readonly noImageDecoding?: boolean; + readonly noAudioDecoding?: boolean; + readonly noWasmDecoding?: boolean; + readonly preRun: readonly PreRunFunc[]; + readonly print?: (a: string) => void; + readonly printErr?: (a: string) => void; + readonly onExit?: (code: number) => void; + readonly thisProgram?: string; + readonly arguments: readonly string[]; + readonly instantiateWasm?: (imports: { + [key: string]: any; + }, successCallback: (instance: WebAssembly.Instance, module: WebAssembly.Module) => void) => void; + readonly API: API; + readonly locateFile: (file: string) => string; + noInitialRun?: boolean; + INITIAL_MEMORY?: number; + exitCode?: number; +} /** - * Load the main Pyodide wasm module and initialize it. + * The Pyodide version. * - * @returns The :ref:`js-api-pyodide` module. - * @example - * async function main() { - * const pyodide = await loadPyodide({ - * fullStdLib: true, - * stdout: (msg) => console.log(`Pyodide: ${msg}`), - * }); - * console.log("Loaded Pyodide"); - * } - * main(); + * The version here is a Python version, following :pep:`440`. This is different + * from the version in ``package.json`` which follows the node package manager + * version convention. + */ +export declare const version: string; +type CreatePyodideModuleFn = (settings: EmscriptenSettings) => Promise; +/** + * The configuration options for loading Pyodide. */ -export declare function loadPyodide(options?: { +/** @deprecated Use `import type { PyodideConfig } from "pyodide/ffi"` instead */ +interface PyodideConfig { /** * The URL from which Pyodide will load the main Pyodide runtime and * packages. It is recommended that you leave this unchanged, providing an @@ -1532,16 +1990,32 @@ export declare function loadPyodide(options?: { */ lockFileURL?: string; /** - * Load the full Python standard library. Setting this to false excludes - * unvendored modules from the standard library. - * Default: ``false`` + * The contents of a lockfile. If a string, it should be valid json and + * ``JSON.parse()`` should return a ``Lockfile`` instance. See + * :js:interface:`~pyodide.Lockfile` for the schema. + */ + lockFileContents?: Lockfile | string | Promise; + /** + * The base url relative to which a relative value of + * :js:attr:`~pyodide.LockfilePackage.file_name` is interpreted. If + * ``lockfileContents`` is provided, then ``lockFileContents`` must be + * provided explicitly in order to install packages with relative paths. + * + * Otherwise, the default is calculated as follows: + * + * 1. If `lockFileURL` contains a ``/``, the default is everything before the last + * ``/`` in ``lockFileURL``. + * 2. If in the browser, the default is ``location.toString()``. + * 3. Otherwise, the default is `'.'`. + */ + packageBaseUrl?: string; + /** + * Deprecated: This option has no effect. */ fullStdLib?: boolean; /** * The URL from which to load the standard library ``python_stdlib.zip`` - * file. This URL includes the most of the Python standard library. Some - * stdlib modules were unvendored, and can be loaded separately - * with ``fullStdLib: true`` option or by their package name. + * file. This URL includes the most of the Python standard library. * Default: ```${indexURL}/python_stdlib.zip``` */ stdLibURL?: string; @@ -1550,7 +2024,7 @@ export declare function loadPyodide(options?: { * input. The :js:func:`pyodide.setStdin` function is more flexible and * should be preferred. */ - stdin?: () => string; + stdin?: () => string | null; /** * Override the standard output callback. The :js:func:`pyodide.setStdout` * function is more flexible and should be preferred in most cases, but @@ -1572,11 +2046,6 @@ export declare function loadPyodide(options?: { * Default: ``globalThis`` */ jsglobals?: object; - /** - * Determine the value of ``sys.executable``. - * @ignore - */ - _sysExecutable?: string; /** * Command line arguments to pass to Python on startup. See `Python command * line interface options @@ -1605,12 +2074,6 @@ export declare function loadPyodide(options?: { * while Pyodide bootstraps itself. */ packages?: string[]; - /** - * Opt into the old behavior where :js:func:`PyProxy.toString() ` - * calls :py:func:`repr` and not :py:class:`str() `. - * @deprecated - */ - pyproxyToStringRepr?: boolean; /** * Make loop.run_until_complete() function correctly using stack switching. * Default: ``true``. @@ -1630,13 +2093,73 @@ export declare function loadPyodide(options?: { fsInit?: (FS: FSType, info: { sitePackages: string; }) => Promise; + /** + * Opt into the old behavior where :js:func:`PyProxy.toString() ` + * calls :py:func:`repr` and not :py:class:`str() `. Deprecated. + * @deprecated + */ + pyproxyToStringRepr?: boolean; + /** + * Opt into the old behavior where JavaScript `null` is converted to `None` + * instead of `jsnull`. Deprecated. + * @deprecated + */ + convertNullToNone?: boolean; + /** + * Opt into the old behavior where Python dictionaries are converted to + * LiteralMap instead of Object. + * @deprecated + */ + toJsLiteralMap?: boolean; + /** + * Determine the value of ``sys.executable``. + * @ignore + */ + _sysExecutable?: string; /** @ignore */ _makeSnapshot?: boolean; /** @ignore */ _loadSnapshot?: Uint8Array | ArrayBuffer | PromiseLike; /** @ignore */ _snapshotDeserializer?: (obj: any) => any; -}): Promise; + /** + * @experimental + * The constructor function to use to create the Pyodide module. + * This function can be imported from `pyodide.asm.mjs` + * and passed to `loadPyodide` as `createPyodideModule` option. + * This is used to work around service workers forbid dynamic import(), + * and not intended to be used in other cases. + * + * Warning: This is an experimental feature and may change in the future. + */ + createPyodideModule?: CreatePyodideModuleFn; + /** @ignore */ + BUILD_ID?: string; + /** @ignore */ + cdnUrl?: string; +} +/** + * @hidden + */ +export type PyodideConfigWithDefaults = Required; +/** + * Load the main Pyodide wasm module and initialize it. + * + * @returns The :ref:`js-api-pyodide` module. + * @example + * async function main() { + * const pyodide = await loadPyodide({ + * stdout: (msg) => console.log(`Pyodide: ${msg}`), + * }); + * console.log("Loaded Pyodide"); + * } + * main(); + */ +export declare function loadPyodide(options?: PyodideConfig): Promise; + +export type { + PyodideAPI as PyodideInterface, +}; export type {}; -export type {PackageData}; +export type {Lockfile, LockfileInfo, LockfilePackage, PackageData, PyodideConfig}; diff --git a/src/pyodide/pyodide.js b/src/pyodide/pyodide.js index 5ecef60d..1d26ad25 100644 --- a/src/pyodide/pyodide.js +++ b/src/pyodide/pyodide.js @@ -1,4 +1,4 @@ -var Q=Object.defineProperty;var s=(e,t)=>Q(e,"name",{value:t,configurable:!0}),R=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,o)=>(typeof require<"u"?require:t)[o]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+e+'" is not supported')});function Z(e){return!isNaN(parseFloat(e))&&isFinite(e)}s(Z,"_isNumber");function E(e){return e.charAt(0).toUpperCase()+e.substring(1)}s(E,"_capitalize");function O(e){return function(){return this[e]}}s(O,"_getter");var w=["isConstructor","isEval","isNative","isToplevel"],N=["columnNumber","lineNumber"],_=["fileName","functionName","source"],ee=["args"],te=["evalOrigin"],P=w.concat(N,_,ee,te);function p(e){if(e)for(var t=0;t-1&&(n=n.replace(/eval code/g,"eval").replace(/(\(eval at [^()]*)|(,.*$)/g,""));var i=n.replace(/^\s+/,"").replace(/\(eval code/g,"(").replace(/^.*?\s+/,""),c=i.match(/ (\(.+\)$)/);i=c?i.replace(c[0],""):i;var l=this.extractLocation(c?c[1]:i),d=c&&i||void 0,u=["eval",""].indexOf(l[0])>-1?void 0:l[0];return new k({functionName:d,fileName:u,lineNumber:l[1],columnNumber:l[2],source:n})},this)},"ErrorStackParser$$parseV8OrIE"),parseFFOrSafari:s(function(r){var a=r.stack.split(` -`).filter(function(n){return!n.match(t)},this);return a.map(function(n){if(n.indexOf(" > eval")>-1&&(n=n.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g,":$1")),n.indexOf("@")===-1&&n.indexOf(":")===-1)return new k({functionName:n});var i=/((.*".+"[^@]*)?[^@]*)(?:@)/,c=n.match(i),l=c&&c[1]?c[1]:void 0,d=this.extractLocation(n.replace(i,""));return new k({functionName:l,fileName:d[0],lineNumber:d[1],columnNumber:d[2],source:n})},this)},"ErrorStackParser$$parseFFOrSafari")}}s(ne,"ErrorStackParser");var re=new ne;var M=re;var g=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&!process.browser,A=g&&typeof module<"u"&&typeof module.exports<"u"&&typeof R<"u"&&typeof __dirname<"u",W=g&&!A,Ne=typeof globalThis.Bun<"u",ie=typeof Deno<"u",B=!g&&!ie,$=B&&typeof window=="object"&&typeof document=="object"&&typeof document.createElement=="function"&&"sessionStorage"in window&&typeof importScripts!="function",j=B&&typeof importScripts=="function"&&typeof self=="object",_e=typeof navigator=="object"&&typeof navigator.userAgent=="string"&&navigator.userAgent.indexOf("Chrome")==-1&&navigator.userAgent.indexOf("Safari")>-1;var z,D,V,H,L;async function T(){if(!g||(z=(await import("node:url")).default,H=await import("node:fs"),L=await import("node:fs/promises"),V=(await import("node:vm")).default,D=await import("node:path"),U=D.sep,typeof R<"u"))return;let e=H,t=await import("node:crypto"),o=await import("ws"),r=await import("node:child_process"),a={fs:e,crypto:t,ws:o,child_process:r};globalThis.require=function(n){return a[n]}}s(T,"initNodeModules");function oe(e,t){return D.resolve(t||".",e)}s(oe,"node_resolvePath");function ae(e,t){return t===void 0&&(t=location),new URL(e,t).toString()}s(ae,"browser_resolvePath");var x;g?x=oe:x=ae;var U;g||(U="/");function se(e,t){return e.startsWith("file://")&&(e=e.slice(7)),e.includes("://")?{response:fetch(e)}:{binary:L.readFile(e).then(o=>new Uint8Array(o.buffer,o.byteOffset,o.byteLength))}}s(se,"node_getBinaryResponse");function ce(e,t){let o=new URL(e,location);return{response:fetch(o,t?{integrity:t}:{})}}s(ce,"browser_getBinaryResponse");var F;g?F=se:F=ce;async function q(e,t){let{response:o,binary:r}=F(e,t);if(r)return r;let a=await o;if(!a.ok)throw new Error(`Failed to load '${e}': request failed.`);return new Uint8Array(await a.arrayBuffer())}s(q,"loadBinaryFile");var I;if($)I=s(async e=>await import(e),"loadScript");else if(j)I=s(async e=>{try{globalThis.importScripts(e)}catch(t){if(t instanceof TypeError)await import(e);else throw t}},"loadScript");else if(g)I=le;else throw new Error("Cannot determine runtime environment");async function le(e){e.startsWith("file://")&&(e=e.slice(7)),e.includes("://")?V.runInThisContext(await(await fetch(e)).text()):await import(z.pathToFileURL(e).href)}s(le,"nodeLoadScript");async function J(e){if(g){await T();let t=await L.readFile(e,{encoding:"utf8"});return JSON.parse(t)}else return await(await fetch(e)).json()}s(J,"loadLockFile");async function K(){if(A)return __dirname;let e;try{throw new Error}catch(r){e=r}let t=M.parse(e)[0].fileName;if(g&&!t.startsWith("file://")&&(t=`file://${t}`),W){let r=await import("node:path");return(await import("node:url")).fileURLToPath(r.dirname(t))}let o=t.lastIndexOf(U);if(o===-1)throw new Error("Could not extract indexURL path from pyodide module location");return t.slice(0,o)}s(K,"calculateDirname");function Y(e){let t=e.FS,o=e.FS.filesystems.MEMFS,r=e.PATH,a={DIR_MODE:16895,FILE_MODE:33279,mount:function(n){if(!n.opts.fileSystemHandle)throw new Error("opts.fileSystemHandle is required");return o.mount.apply(null,arguments)},syncfs:async(n,i,c)=>{try{let l=a.getLocalSet(n),d=await a.getRemoteSet(n),u=i?d:l,m=i?l:d;await a.reconcile(n,u,m),c(null)}catch(l){c(l)}},getLocalSet:n=>{let i=Object.create(null);function c(u){return u!=="."&&u!==".."}s(c,"isRealDir");function l(u){return m=>r.join2(u,m)}s(l,"toAbsolute");let d=t.readdir(n.mountpoint).filter(c).map(l(n.mountpoint));for(;d.length;){let u=d.pop(),m=t.stat(u);t.isDir(m.mode)&&d.push.apply(d,t.readdir(u).filter(c).map(l(u))),i[u]={timestamp:m.mtime,mode:m.mode}}return{type:"local",entries:i}},getRemoteSet:async n=>{let i=Object.create(null),c=await de(n.opts.fileSystemHandle);for(let[l,d]of c)l!=="."&&(i[r.join2(n.mountpoint,l)]={timestamp:d.kind==="file"?new Date((await d.getFile()).lastModified):new Date,mode:d.kind==="file"?a.FILE_MODE:a.DIR_MODE});return{type:"remote",entries:i,handles:c}},loadLocalEntry:n=>{let c=t.lookupPath(n).node,l=t.stat(n);if(t.isDir(l.mode))return{timestamp:l.mtime,mode:l.mode};if(t.isFile(l.mode))return c.contents=o.getFileDataAsTypedArray(c),{timestamp:l.mtime,mode:l.mode,contents:c.contents};throw new Error("node type not supported")},storeLocalEntry:(n,i)=>{if(t.isDir(i.mode))t.mkdirTree(n,i.mode);else if(t.isFile(i.mode))t.writeFile(n,i.contents,{canOwn:!0});else throw new Error("node type not supported");t.chmod(n,i.mode),t.utime(n,i.timestamp,i.timestamp)},removeLocalEntry:n=>{var i=t.stat(n);t.isDir(i.mode)?t.rmdir(n):t.isFile(i.mode)&&t.unlink(n)},loadRemoteEntry:async n=>{if(n.kind==="file"){let i=await n.getFile();return{contents:new Uint8Array(await i.arrayBuffer()),mode:a.FILE_MODE,timestamp:new Date(i.lastModified)}}else{if(n.kind==="directory")return{mode:a.DIR_MODE,timestamp:new Date};throw new Error("unknown kind: "+n.kind)}},storeRemoteEntry:async(n,i,c)=>{let l=n.get(r.dirname(i)),d=t.isFile(c.mode)?await l.getFileHandle(r.basename(i),{create:!0}):await l.getDirectoryHandle(r.basename(i),{create:!0});if(d.kind==="file"){let u=await d.createWritable();await u.write(c.contents),await u.close()}n.set(i,d)},removeRemoteEntry:async(n,i)=>{await n.get(r.dirname(i)).removeEntry(r.basename(i)),n.delete(i)},reconcile:async(n,i,c)=>{let l=0,d=[];Object.keys(i.entries).forEach(function(f){let y=i.entries[f],S=c.entries[f];(!S||t.isFile(y.mode)&&y.timestamp.getTime()>S.timestamp.getTime())&&(d.push(f),l++)}),d.sort();let u=[];if(Object.keys(c.entries).forEach(function(f){i.entries[f]||(u.push(f),l++)}),u.sort().reverse(),!l)return;let m=i.type==="remote"?i.handles:c.handles;for(let f of d){let y=r.normalize(f.replace(n.mountpoint,"/")).substring(1);if(c.type==="local"){let S=m.get(y),X=await a.loadRemoteEntry(S);a.storeLocalEntry(f,X)}else{let S=a.loadLocalEntry(f);await a.storeRemoteEntry(m,y,S)}}for(let f of u)if(c.type==="local")a.removeLocalEntry(f);else{let y=r.normalize(f.replace(n.mountpoint,"/")).substring(1);await a.removeRemoteEntry(m,y)}}};e.FS.filesystems.NATIVEFS_ASYNC=a}s(Y,"initializeNativeFS");var de=s(async e=>{let t=[];async function o(a){for await(let n of a.values())t.push(n),n.kind==="directory"&&await o(n)}s(o,"collect"),await o(e);let r=new Map;r.set(".",e);for(let a of t){let n=(await e.resolve(a)).join("/");r.set(n,a)}return r},"getFsHandles");function G(e){let t={noImageDecoding:!0,noAudioDecoding:!0,noWasmDecoding:!1,preRun:ge(e),quit(o,r){throw t.exited={status:o,toThrow:r},r},print:e.stdout,printErr:e.stderr,thisProgram:e._sysExecutable,arguments:e.args,API:{config:e},locateFile:o=>e.indexURL+o,instantiateWasm:ye(e.indexURL)};return t}s(G,"createSettings");function ue(e){return function(t){let o="/";try{t.FS.mkdirTree(e)}catch(r){console.error(`Error occurred while making a home directory '${e}':`),console.error(r),console.error(`Using '${o}' for a home directory instead`),e=o}t.FS.chdir(e)}}s(ue,"createHomeDirectory");function fe(e){return function(t){Object.assign(t.ENV,e)}}s(fe,"setEnvironment");function me(e){return e?[async t=>{t.addRunDependency("fsInitHook");try{await e(t.FS,{sitePackages:t.API.sitePackages})}finally{t.removeRunDependency("fsInitHook")}}]:[]}s(me,"callFsInitHook");function pe(e){let t=q(e);return async o=>{let r=o._py_version_major(),a=o._py_version_minor();o.FS.mkdirTree("/lib"),o.API.sitePackages=`/lib/python${r}.${a}/site-packages`,o.FS.mkdirTree(o.API.sitePackages),o.addRunDependency("install-stdlib");try{let n=await t;o.FS.writeFile(`/lib/python${r}${a}.zip`,n)}catch(n){console.error("Error occurred while installing the standard library:"),console.error(n)}finally{o.removeRunDependency("install-stdlib")}}}s(pe,"installStdlib");function ge(e){let t;return e.stdLibURL!=null?t=e.stdLibURL:t=e.indexURL+"python_stdlib.zip",[...me(e.fsInit),pe(t),ue(e.env.HOME),fe(e.env),Y]}s(ge,"getFileSystemInitializationFuncs");function ye(e){if(typeof WasmOffsetConverter<"u")return;let{binary:t,response:o}=F(e+"pyodide.asm.wasm");return function(r,a){return async function(){try{let n;o?n=await WebAssembly.instantiateStreaming(o,r):n=await WebAssembly.instantiate(await t,r);let{instance:i,module:c}=n;a(i,c)}catch(n){console.warn("wasm instantiation failed!"),console.warn(n)}}(),{}}}s(ye,"getInstantiateWasmFunc");var C="0.27.7";async function $e(e={}){var u,m;await T();let t=e.indexURL||await K();t=x(t),t.endsWith("/")||(t+="/"),e.indexURL=t;let o={fullStdLib:!1,jsglobals:globalThis,stdin:globalThis.prompt?globalThis.prompt:void 0,lockFileURL:t+"pyodide-lock.json",args:[],env:{},packageCacheDir:t,packages:[],enableRunUntilComplete:!0,checkAPIVersion:!0,BUILD_ID:"e94377f5ce7dcf67e0417b69a0016733c2cfb6b4622ee8c490a6f17eb58e863b"},r=Object.assign(o,e);(u=r.env).HOME??(u.HOME="/home/pyodide"),(m=r.env).PYTHONINSPECT??(m.PYTHONINSPECT="1");let a=G(r),n=a.API;if(n.lockFilePromise=J(r.lockFileURL),typeof _createPyodideModule!="function"){let f=`${r.indexURL}pyodide.asm.js`;await I(f)}let i;if(e._loadSnapshot){let f=await e._loadSnapshot;ArrayBuffer.isView(f)?i=f:i=new Uint8Array(f),a.noInitialRun=!0,a.INITIAL_MEMORY=i.length}let c=await _createPyodideModule(a);if(a.exited)throw a.exited.toThrow;if(e.pyproxyToStringRepr&&n.setPyProxyToStringMethod(!0),n.version!==C&&r.checkAPIVersion)throw new Error(`Pyodide version does not match: '${C}' <==> '${n.version}'. If you updated the Pyodide version, make sure you also updated the 'indexURL' parameter passed to loadPyodide.`);c.locateFile=f=>{throw new Error("Didn't expect to load any more file_packager files!")};let l;i&&(l=n.restoreSnapshot(i));let d=n.finalizeBootstrap(l,e._snapshotDeserializer);return n.sys.path.insert(0,""),d.version.includes("dev")||n.setCdnUrl(`https://cdn.jsdelivr.net/pyodide/v${d.version}/full/`),n._pyodide.set_excepthook(),await n.packageIndexReady,n.initializeStreams(r.stdin,r.stdout,r.stderr),d}s($e,"loadPyodide");export{$e as loadPyodide,C as version}; +var Q=Object.defineProperty;var o=(e,t)=>Q(e,"name",{value:t,configurable:!0}),R=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(typeof require<"u"?require:t)[n]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var W=(()=>{for(var e=new Uint8Array(128),t=0;t<64;t++)e[t<26?t+65:t<52?t+71:t<62?t-4:t*4-205]=t;return n=>{for(var i=n.length,s=new Uint8Array((i-(n[i-1]=="=")-(n[i-2]=="="))*3/4|0),r=0,a=0;r>4,s[a++]=l<<4|d>>2,s[a++]=d<<6|u}return s}})();function Z(e){return!isNaN(parseFloat(e))&&isFinite(e)}o(Z,"_isNumber");function P(e){return e.charAt(0).toUpperCase()+e.substring(1)}o(P,"_capitalize");function D(e){return function(){return this[e]}}o(D,"_getter");var N=["isConstructor","isEval","isNative","isToplevel"],I=["columnNumber","lineNumber"],S=["fileName","functionName","source"],ee=["args"],te=["evalOrigin"],F=N.concat(I,S,ee,te);function p(e){if(e)for(var t=0;t-1&&(r=r.replace(/eval code/g,"eval").replace(/(\(eval at [^()]*)|(,.*$)/g,""));var a=r.replace(/^\s+/,"").replace(/\(eval code/g,"(").replace(/^.*?\s+/,""),c=a.match(/ (\(.+\)$)/);a=c?a.replace(c[0],""):a;var l=this.extractLocation(c?c[1]:a),d=c&&a||void 0,u=["eval",""].indexOf(l[0])>-1?void 0:l[0];return new A({functionName:d,fileName:u,lineNumber:l[1],columnNumber:l[2],source:r})},this)},"ErrorStackParser$$parseV8OrIE"),parseFFOrSafari:o(function(i){var s=i.stack.split(` +`).filter(function(r){return!r.match(t)},this);return s.map(function(r){if(r.indexOf(" > eval")>-1&&(r=r.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g,":$1")),r.indexOf("@")===-1&&r.indexOf(":")===-1)return new A({functionName:r});var a=/((.*".+"[^@]*)?[^@]*)(?:@)/,c=r.match(a),l=c&&c[1]?c[1]:void 0,d=this.extractLocation(r.replace(a,""));return new A({functionName:l,fileName:d[0],lineNumber:d[1],columnNumber:d[2],source:r})},this)},"ErrorStackParser$$parseFFOrSafari")}}o(ne,"ErrorStackParser");var re=new ne;var M=re;function ie(){if(typeof API<"u"&&API!==globalThis.API)return API.runtimeEnv;let e=typeof Bun<"u",t=typeof Deno<"u",n=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&!process.browser,i=typeof navigator=="object"&&typeof navigator.userAgent=="string"&&navigator.userAgent.indexOf("Chrome")===-1&&navigator.userAgent.indexOf("Safari")>-1,s=typeof read=="function"&&typeof load=="function",r=typeof navigator=="object"&&navigator.userAgent?.includes("Cloudflare-Workers");return oe({IN_BUN:e,IN_DENO:t,IN_NODE:n,IN_SAFARI:i,IN_SHELL:s,IN_WORKERD:r})}o(ie,"getGlobalRuntimeEnv");var f=ie();function oe(e){let t=e.IN_NODE&&typeof module<"u"&&module.exports&&typeof R=="function"&&typeof __dirname=="string",n=e.IN_NODE&&!t,i=!e.IN_NODE&&!e.IN_DENO&&!e.IN_BUN,s=i&&typeof window<"u"&&typeof window.document<"u"&&typeof document.createElement=="function"&&"sessionStorage"in window&&typeof globalThis.importScripts!="function",r=i&&typeof globalThis.WorkerGlobalScope<"u"&&typeof globalThis.self<"u"&&globalThis.self instanceof globalThis.WorkerGlobalScope;if(r&&ae())throw new Error("Classic web workers are not supported");let a={...e,IN_BROWSER:i,IN_BROWSER_MAIN_THREAD:s,IN_BROWSER_WEB_WORKER:r,IN_NODE_COMMONJS:t,IN_NODE_ESM:n};if(!(a.IN_BROWSER_MAIN_THREAD||a.IN_BROWSER_WEB_WORKER||a.IN_NODE||a.IN_SHELL||a.IN_WORKERD))throw new Error(`Cannot determine runtime environment: ${JSON.stringify(a)}`);return a}o(oe,"calculateDerivedFlags");function ae(){try{return globalThis.importScripts("data:text/javascript,"),!0}catch{return!1}}o(ae,"isClassicWorker");var $,x,se,B,L;async function C(){if(!f.IN_NODE||($=(await import("node:url")).default,B=await import("node:fs"),L=await import("node:fs/promises"),se=(await import("node:vm")).default,x=await import("node:path"),T=x.sep,typeof R<"u"))return;let e=B,t=await import("node:crypto"),n=await import("ws"),i=await import("node:child_process"),s={fs:e,crypto:t,ws:n,child_process:i};globalThis.require=function(r){return s[r]}}o(C,"initNodeModules");function le(e,t){return x.resolve(t||".",e)}o(le,"node_resolvePath");function ce(e,t){return t===void 0&&(t=location),new URL(e,t).toString()}o(ce,"browser_resolvePath");var w;f.IN_NODE?w=le:f.IN_SHELL?w=o(e=>e,"resolvePath"):w=ce;var T;f.IN_NODE||(T="/");function de(e,t){return e.startsWith("file://")&&(e=e.slice(7)),e.includes("://")?{response:fetch(e)}:{binary:L.readFile(e).then(n=>new Uint8Array(n.buffer,n.byteOffset,n.byteLength))}}o(de,"node_getBinaryResponse");function ue(e,t){if(e.startsWith("file://")&&(e=e.slice(7)),e.includes("://"))throw new Error("Shell cannot fetch urls");return{binary:Promise.resolve(new Uint8Array(readbuffer(e)))}}o(ue,"shell_getBinaryResponse");function fe(e,t){let n=new URL(e,location);return{response:fetch(n,t?{integrity:t}:{})}}o(fe,"browser_getBinaryResponse");var _;f.IN_NODE?_=de:f.IN_SHELL?_=ue:_=fe;async function j(e,t){let{response:n,binary:i}=_(e,t);if(i)return i;let s=await n;if(!s.ok)throw new Error(`Failed to load '${e}': request failed.`);return new Uint8Array(await s.arrayBuffer())}o(j,"loadBinaryFile");var O;f.IN_NODE?O=me:O=o(async e=>await import(e),"loadScript");async function me(e){return e.startsWith("file://")&&(e=e.slice(7)),e.includes("://")?await import(e):await import($.pathToFileURL(e).href)}o(me,"nodeLoadScript");async function H(e){if(f.IN_NODE){await C();let t=await L.readFile(e,{encoding:"utf8"});return JSON.parse(t)}else if(f.IN_SHELL){let t=read(e);return JSON.parse(t)}else return await(await fetch(e)).json()}o(H,"loadLockFile");async function J(){if(f.IN_NODE_COMMONJS)return __dirname;let e;try{throw new Error}catch(i){e=i}let t=M.parse(e)[0].fileName;if(f.IN_NODE&&!t.startsWith("file://")&&(t=`file://${t}`),f.IN_NODE_ESM){let i=await import("node:path");return(await import("node:url")).fileURLToPath(i.dirname(t))}let n=t.lastIndexOf(T);if(n===-1)throw new Error("Could not extract indexURL path from pyodide module location. Please pass the indexURL explicitly to loadPyodide.");return t.slice(0,n)}o(J,"calculateDirname");function V(e){return e.substring(0,e.lastIndexOf("/")+1)||globalThis.location?.toString()||"."}o(V,"calculateInstallBaseUrl");function z(e){let t=e.FS,n=e.FS.filesystems.MEMFS,i=e.PATH,s={DIR_MODE:16895,FILE_MODE:33279,mount:o(function(r){if(!r.opts.fileSystemHandle)throw new Error("opts.fileSystemHandle is required");return n.mount.apply(null,arguments)},"mount"),syncfs:o(async(r,a,c)=>{try{let l=s.getLocalSet(r),d=await s.getRemoteSet(r),u=a?d:l,y=a?l:d;await s.reconcile(r,u,y),c(null)}catch(l){c(l)}},"syncfs"),getLocalSet:o(r=>{let a=Object.create(null);function c(u){return u!=="."&&u!==".."}o(c,"isRealDir");function l(u){return y=>i.join2(u,y)}o(l,"toAbsolute");let d=t.readdir(r.mountpoint).filter(c).map(l(r.mountpoint));for(;d.length;){let u=d.pop(),y=t.stat(u);t.isDir(y.mode)&&d.push.apply(d,t.readdir(u).filter(c).map(l(u))),a[u]={timestamp:y.mtime,mode:y.mode}}return{type:"local",entries:a}},"getLocalSet"),getRemoteSet:o(async r=>{let a=Object.create(null),c=await pe(r.opts.fileSystemHandle);for(let[l,d]of c)l!=="."&&(a[i.join2(r.mountpoint,l)]={timestamp:d.kind==="file"?new Date((await d.getFile()).lastModified):new Date,mode:d.kind==="file"?s.FILE_MODE:s.DIR_MODE});return{type:"remote",entries:a,handles:c}},"getRemoteSet"),loadLocalEntry:o(r=>{let c=t.lookupPath(r,{}).node,l=t.stat(r);if(t.isDir(l.mode))return{timestamp:l.mtime,mode:l.mode};if(t.isFile(l.mode))return c.contents=n.getFileDataAsTypedArray(c),{timestamp:l.mtime,mode:l.mode,contents:c.contents};throw new Error("node type not supported")},"loadLocalEntry"),storeLocalEntry:o((r,a)=>{if(t.isDir(a.mode))t.mkdirTree(r,a.mode);else if(t.isFile(a.mode))t.writeFile(r,a.contents,{canOwn:!0});else throw new Error("node type not supported");t.chmod(r,a.mode),t.utime(r,a.timestamp,a.timestamp)},"storeLocalEntry"),removeLocalEntry:o(r=>{var a=t.stat(r);t.isDir(a.mode)?t.rmdir(r):t.isFile(a.mode)&&t.unlink(r)},"removeLocalEntry"),loadRemoteEntry:o(async r=>{if(r.kind==="file"){let a=await r.getFile();return{contents:new Uint8Array(await a.arrayBuffer()),mode:s.FILE_MODE,timestamp:new Date(a.lastModified)}}else{if(r.kind==="directory")return{mode:s.DIR_MODE,timestamp:new Date};throw new Error("unknown kind: "+r.kind)}},"loadRemoteEntry"),storeRemoteEntry:o(async(r,a,c)=>{let l=r.get(i.dirname(a)),d=t.isFile(c.mode)?await l.getFileHandle(i.basename(a),{create:!0}):await l.getDirectoryHandle(i.basename(a),{create:!0});if(d.kind==="file"){let u=await d.createWritable();await u.write(c.contents),await u.close()}r.set(a,d)},"storeRemoteEntry"),removeRemoteEntry:o(async(r,a)=>{await r.get(i.dirname(a)).removeEntry(i.basename(a)),r.delete(a)},"removeRemoteEntry"),reconcile:o(async(r,a,c)=>{let l=0,d=[];Object.keys(a.entries).forEach(function(m){let g=a.entries[m],h=c.entries[m];(!h||t.isFile(g.mode)&&g.timestamp.getTime()>h.timestamp.getTime())&&(d.push(m),l++)}),d.sort();let u=[];if(Object.keys(c.entries).forEach(function(m){a.entries[m]||(u.push(m),l++)}),u.sort().reverse(),!l)return;let y=a.type==="remote"?a.handles:c.handles;for(let m of d){let g=i.normalize(m.replace(r.mountpoint,"/")).substring(1);if(c.type==="local"){let h=y.get(g),X=await s.loadRemoteEntry(h);s.storeLocalEntry(m,X)}else{let h=s.loadLocalEntry(m);await s.storeRemoteEntry(y,g,h)}}for(let m of u)if(c.type==="local")s.removeLocalEntry(m);else{let g=i.normalize(m.replace(r.mountpoint,"/")).substring(1);await s.removeRemoteEntry(y,g)}},"reconcile")};e.FS.filesystems.NATIVEFS_ASYNC=s}o(z,"initializeNativeFS");var pe=o(async e=>{let t=[];async function n(s){for await(let r of s.values())t.push(r),r.kind==="directory"&&await n(r)}o(n,"collect"),await n(e);let i=new Map;i.set(".",e);for(let s of t){let r=(await e.resolve(s)).join("/");i.set(r,s)}return i},"getFsHandles");var G=W("AGFzbQEAAAABDANfAGAAAW9gAW8BfwMDAgECBygCE0pzdl9HZXRFcnJvcl9pbXBvcnQAAA5Kc3ZFcnJvcl9DaGVjawABChMCBwD7AQD7GwsJACAA+xr7FAAL");var ge=async function(){if(!(globalThis.navigator&&(/iPad|iPhone|iPod/.test(navigator.userAgent)||navigator.platform==="MacIntel"&&typeof navigator.maxTouchPoints<"u"&&navigator.maxTouchPoints>1)))try{let t=await WebAssembly.compile(G);return await WebAssembly.instantiate(t)}catch(t){if(t instanceof WebAssembly.CompileError)return;throw t}}();async function K(){let e=await ge;if(e)return e.exports;let t=Symbol("error marker");return{Jsv_GetError_import:o(()=>t,"Jsv_GetError_import"),JsvError_Check:o(n=>n===t,"JsvError_Check")}}o(K,"getJsvErrorImport");function q(e){let t={config:e,runtimeEnv:f},n={noImageDecoding:!0,noAudioDecoding:!0,noWasmDecoding:!1,preRun:Ne(e),print:e.stdout,printErr:e.stderr,onExit(i){n.exitCode=i},thisProgram:e._sysExecutable,arguments:e.args,API:t,locateFile:o(i=>e.indexURL+i,"locateFile"),instantiateWasm:Ie(e.indexURL)};return n}o(q,"createSettings");function ve(e){return function(t){let n="/";try{t.FS.mkdirTree(e)}catch(i){console.error(`Error occurred while making a home directory '${e}':`),console.error(i),console.error(`Using '${n}' for a home directory instead`),e=n}t.FS.chdir(e)}}o(ve,"createHomeDirectory");function be(e){return function(t){Object.assign(t.ENV,e)}}o(be,"setEnvironment");function Ee(e){return e?[async t=>{t.addRunDependency("fsInitHook");try{await e(t.FS,{sitePackages:t.API.sitePackages})}finally{t.removeRunDependency("fsInitHook")}}]:[]}o(Ee,"callFsInitHook");function Pe(e){let t=e.HEAPU32[e._Py_Version>>>2],n=t>>>24&255,i=t>>>16&255,s=t>>>8&255;return[n,i,s]}o(Pe,"computeVersionTuple");function he(e){let t=j(e);return async n=>{n.API.pyVersionTuple=Pe(n);let[i,s]=n.API.pyVersionTuple;n.FS.mkdirTree("/lib"),n.API.sitePackages=`/lib/python${i}.${s}/site-packages`,n.FS.mkdirTree(n.API.sitePackages),n.FS.mkdirTree(`/lib/python${i}.${s}/lib-dynload`),n.addRunDependency("install-stdlib");try{let r=await t;n.FS.writeFile(`/lib/python${i}${s}.zip`,r)}catch(r){console.error("Error occurred while installing the standard library:"),console.error(r)}finally{n.removeRunDependency("install-stdlib")}}}o(he,"installStdlib");function Ne(e){let t;return e.stdLibURL!=null?t=e.stdLibURL:t=e.indexURL+"python_stdlib.zip",[he(t),ve(e.env.HOME),be(e.env),z,...Ee(e.fsInit)]}o(Ne,"getFileSystemInitializationFuncs");function Ie(e){if(typeof WasmOffsetConverter<"u")return;let{binary:t,response:n}=_(e+"pyodide.asm.wasm"),i=K();return function(s,r){return async function(){let{Jsv_GetError_import:a,JsvError_Check:c}=await i;s.env.Jsv_GetError_import=a,s.env.JsvError_Check=c;try{let l;n?l=await WebAssembly.instantiateStreaming(n,s):l=await WebAssembly.instantiate(await t,s);let{instance:d,module:u}=l;r(d,u)}catch(l){console.warn("wasm instantiation failed!"),console.warn(l)}}(),{}}}o(Ie,"getInstantiateWasmFunc");var Y="314.0.7";function k(e){return e===void 0||e.endsWith("/")?e:e+"/"}o(k,"withTrailingSlash");var U=Y;async function Se(e={}){if(await C(),e.lockFileContents&&e.lockFileURL)throw new Error("Can't pass both lockFileContents and lockFileURL");let t=e.indexURL||await J();if(t=k(w(t)),e.packageBaseUrl=k(e.packageBaseUrl),e.cdnUrl=k(e.packageBaseUrl??`https://cdn.jsdelivr.net/pyodide/v${U}/full/`),!e.lockFileContents){let s=e.lockFileURL??t+"pyodide-lock.json";e.lockFileContents=H(s),e.packageBaseUrl??=V(s)}e.indexURL=t,e.packageCacheDir&&(e.packageCacheDir=k(w(e.packageCacheDir)));let n={jsglobals:globalThis,stdin:globalThis.prompt?()=>globalThis.prompt():void 0,args:[],env:{},packages:[],packageCacheDir:e.packageBaseUrl,enableRunUntilComplete:!0,checkAPIVersion:!0,BUILD_ID:"990c3b51a9722d62434de2bbc9643732658d488c1b6d467f22c1b6beba80d497"},i=Object.assign(n,e);return i.env.HOME??="/home/pyodide",i.env.PYTHONINSPECT??="1",i}o(Se,"initializeConfiguration");function we(e){let t=q(e),n=t.API;return n.lockFilePromise=Promise.resolve(e.lockFileContents),t}o(we,"createEmscriptenSettings");async function _e(e){if(e.createPyodideModule)return e.createPyodideModule;let t=`${e.indexURL}pyodide.asm.mjs`;return(await O(t)).default}o(_e,"loadWasmScript");async function ke(e,t){if(!e._loadSnapshot)return;let n=await e._loadSnapshot,i=ArrayBuffer.isView(n)?n:new Uint8Array(n);return t.noInitialRun=!0,t.INITIAL_MEMORY=i.length,i}o(ke,"prepareSnapshot");async function Re(e,t){let n=await e(t);if(t.exitCode!==void 0)throw new n.ExitStatus(t.exitCode);return n}o(Re,"instantiatePyodideModule");function Fe(e,t){let n=e.API;if(t.pyproxyToStringRepr&&n.setPyProxyToStringMethod(!0),t.convertNullToNone&&n.setCompatNullToNone(!0),t.toJsLiteralMap&&n.setCompatToJsLiteralMap(!0),n.version!==U&&t.checkAPIVersion)throw new Error(`Pyodide version does not match: '${U}' <==> '${n.version}'. If you updated the Pyodide version, make sure you also updated the 'indexURL' parameter passed to loadPyodide.`);e.locateFile=i=>{throw i.endsWith(".so")?new Error(`Failed to find dynamic library "${i}"`):new Error(`Unexpected call to locateFile("${i}")`)}}o(Fe,"configureAPI");function Ae(e,t,n){let i=e.API,s;return t&&(s=i.restoreSnapshot(t)),i.finalizeBootstrap(s,n._snapshotDeserializer)}o(Ae,"bootstrapPyodide");async function Oe(e,t){let n=e._api;return n.sys.path.insert(0,""),n._pyodide.set_excepthook(),await n.packageIndexReady,n.initializeStreams(t.stdin,t.stdout,t.stderr),e}o(Oe,"finalizeSetup");async function dt(e={}){let t=await Se(e),n=we(t),i=await _e(t),s=await ke(t,n),r=await Re(i,n);Fe(r,t);let a=Ae(r,s,t);return await Oe(a,t)}o(dt,"loadPyodide");export{dt as loadPyodide,U as version}; //# sourceMappingURL=pyodide.mjs.map diff --git a/tests/conftest.py b/tests/conftest.py index abe71dbc..2bcd2995 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -186,6 +186,24 @@ def pytest_collection_modifyitems( items[:] = keep +# Pyodide requests every wheel an app needs at once. `http.server` queues only 5 +# connections waiting to be accepted, and macOS resets the rest, so packages +# fail to load at random. Both servers below queue more. +_REQUEST_QUEUE_SIZE = 128 + +# `python -m http.server` with a longer queue. Run as a separate process so that +# xdist workers can share it. +_SERVE_STATIC = f""" +import sys +from functools import partial +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer + +ThreadingHTTPServer.request_queue_size = {_REQUEST_QUEUE_SIZE} +handler = partial(SimpleHTTPRequestHandler, directory=sys.argv[2]) +ThreadingHTTPServer(("127.0.0.1", int(sys.argv[1])), handler).serve_forever() +""" + + @pytest.fixture(scope="session", autouse=True) def static_server() -> Iterator[None]: """Serve the built `_shinylive/` for the session. @@ -210,12 +228,9 @@ def static_server() -> Iterator[None]: process = subprocess.Popen( [ sys.executable, - "-m", - "http.server", + "-c", + _SERVE_STATIC, str(STATIC_PORT), - "--bind", - "127.0.0.1", - "--directory", str(SHINYLIVE_DIR), ], stdout=subprocess.DEVNULL, @@ -252,7 +267,10 @@ class _QuietHandler(SimpleHTTPRequestHandler): def log_message(self, format: str, *args: Any) -> None: pass - server = ThreadingHTTPServer( + class _Server(ThreadingHTTPServer): + request_queue_size = _REQUEST_QUEUE_SIZE + + server = _Server( ("127.0.0.1", 0), partial(_QuietHandler, directory=str(export_root)) ) threading.Thread(target=server.serve_forever, daemon=True).start() From 2cf98d316977d4bbe45f91ad7bbb2c3c7410ad1e Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Mon, 9 Nov 2026 13:22:56 -0500 Subject: [PATCH 2/4] refactor: share one static test server, detect a fresh pyodide-lock.json by shiny pyodide-lock.json's info block no longer has a Pyodide version, and what it has is the same across patch releases, so comparing it misses an upgrade such as 314.0.6 to 314.0.7. A lock without shiny in it is always newly unpacked. Both test servers now come from tests/static_server.py. --- scripts/pyodide_packages.py | 13 +++++++------ tests/conftest.py | 36 +++--------------------------------- tests/static_server.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 39 deletions(-) create mode 100644 tests/static_server.py diff --git a/scripts/pyodide_packages.py b/scripts/pyodide_packages.py index 55c43100..05688746 100755 --- a/scripts/pyodide_packages.py +++ b/scripts/pyodide_packages.py @@ -702,12 +702,13 @@ def orig_pyodide_lock() -> PyodidePackagesFile: shutil.copy(pyodide_lock_json_file, orig_pyodide_lock_json_file) else: try: - with open(pyodide_lock_json_file, "r") as f_new, open(orig_pyodide_lock_json_file, "r") as f_orig: - new_info = json.load(f_new).get("info", {}) - orig_info = json.load(f_orig).get("info", {}) - # Pyodide 0.28 dropped `info.version`, so compare the whole block. - if new_info != orig_info: - shutil.copy(pyodide_lock_json_file, orig_pyodide_lock_json_file) + # Pyodide 0.28 dropped `info.version`, and what is left is the same + # across patch releases. A pyodide-lock.json without shiny in it is a + # fresh one from a newly unpacked Pyodide, so it replaces the saved copy. + with open(pyodide_lock_json_file, "r") as f_new: + new_packages = json.load(f_new).get("packages", {}) + if "shiny" not in new_packages: + shutil.copy(pyodide_lock_json_file, orig_pyodide_lock_json_file) except Exception: shutil.copy(pyodide_lock_json_file, orig_pyodide_lock_json_file) diff --git a/tests/conftest.py b/tests/conftest.py index 2bcd2995..7804df94 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,8 +8,6 @@ import threading import time from collections.abc import Callable, Iterator, Mapping -from functools import partial -from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from typing import Any @@ -25,6 +23,7 @@ suspect_terminal_lines, terminal_text, ) +from static_server import static_server as make_static_server # Assertions get their own timeout, well short of the per-test one: an app that # is going to answer at all answers long before this. @@ -186,24 +185,6 @@ def pytest_collection_modifyitems( items[:] = keep -# Pyodide requests every wheel an app needs at once. `http.server` queues only 5 -# connections waiting to be accepted, and macOS resets the rest, so packages -# fail to load at random. Both servers below queue more. -_REQUEST_QUEUE_SIZE = 128 - -# `python -m http.server` with a longer queue. Run as a separate process so that -# xdist workers can share it. -_SERVE_STATIC = f""" -import sys -from functools import partial -from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer - -ThreadingHTTPServer.request_queue_size = {_REQUEST_QUEUE_SIZE} -handler = partial(SimpleHTTPRequestHandler, directory=sys.argv[2]) -ThreadingHTTPServer(("127.0.0.1", int(sys.argv[1])), handler).serve_forever() -""" - - @pytest.fixture(scope="session", autouse=True) def static_server() -> Iterator[None]: """Serve the built `_shinylive/` for the session. @@ -228,8 +209,7 @@ def static_server() -> Iterator[None]: process = subprocess.Popen( [ sys.executable, - "-c", - _SERVE_STATIC, + str(Path(__file__).parent / "static_server.py"), str(STATIC_PORT), str(SHINYLIVE_DIR), ], @@ -262,17 +242,7 @@ def export_server(export_root: Path) -> Iterator[str]: reusing a server for, but an export root is made fresh per session, so a server left over from another run would quietly serve the wrong files. """ - - class _QuietHandler(SimpleHTTPRequestHandler): - def log_message(self, format: str, *args: Any) -> None: - pass - - class _Server(ThreadingHTTPServer): - request_queue_size = _REQUEST_QUEUE_SIZE - - server = _Server( - ("127.0.0.1", 0), partial(_QuietHandler, directory=str(export_root)) - ) + server = make_static_server(0, str(export_root)) threading.Thread(target=server.serve_forever, daemon=True).start() try: yield f"http://127.0.0.1:{server.server_address[1]}" diff --git a/tests/static_server.py b/tests/static_server.py new file mode 100644 index 00000000..c92915c8 --- /dev/null +++ b/tests/static_server.py @@ -0,0 +1,37 @@ +"""A static file server that can take Pyodide's burst of package requests. + +Pyodide fetches every wheel an app needs at once -- 40-odd for a Shiny app. +`python -m http.server` queues only 5 pending connections; past that, macOS +resets the connection, and Pyodide skips any package it could not fetch rather +than failing, so the app dies later on an import of a package that "is not +installed". + +Run as a script to serve a directory on a port, as `static_server` in +conftest.py does: + + python tests/static_server.py 8008 _shinylive +""" + +from __future__ import annotations + +import sys +from functools import partial +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from typing import Any + + +class StaticServer(ThreadingHTTPServer): + request_queue_size = 128 + + +class QuietHandler(SimpleHTTPRequestHandler): + def log_message(self, format: str, *args: Any) -> None: + pass + + +def static_server(port: int, directory: str) -> StaticServer: + return StaticServer(("127.0.0.1", port), partial(QuietHandler, directory=directory)) + + +if __name__ == "__main__": + static_server(int(sys.argv[1]), sys.argv[2]).serve_forever() From 14efa69b5a1a120f022f675671c831bf2177580f Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 10 Nov 2026 13:59:07 -0500 Subject: [PATCH 3/4] fix: drop the anyio backport workaround, build local wheels before locking, and stop overwriting a clean saved Pyodide lock - Python 3.14 has ExceptionGroup built in, so anyio no longer needs exceptiongroup. - update_packages_lock and update_packages_lock_local read the local wheels in packages/ and expect one per package. They now depend on those wheels, whose rules remove older copies, so a leftover cp312 libsass wheel no longer breaks them. - Remove the libsass wheel rule's dependency on itself. - orig_pyodide_lock no longer copies an unreadable pyodide-lock.json over the saved original. --- Makefile | 12 +++++++++--- scripts/pyodide_packages.py | 19 ++++++++----------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index cbff8a4c..cee82451 100644 --- a/Makefile +++ b/Makefile @@ -294,17 +294,23 @@ $(PACKAGE_DIR)/$(FAICONS_WHEEL): $(PYBIN) $(PACKAGE_DIR)/py-faicons rm -f $(PACKAGE_DIR)/faicons*.whl . $(PYBIN)/activate && cd $(PACKAGE_DIR)/py-faicons && pip wheel --no-deps -w ../ . -$(PACKAGE_DIR)/$(LIBSASS_WHEEL): $(PYBIN) $(PACKAGE_DIR)/$(LIBSASS_WHEEL) +$(PACKAGE_DIR)/$(LIBSASS_WHEEL): $(PYBIN) rm -f $(PACKAGE_DIR)/libsass*.whl curl --fail -L https://pkg.garrickadenbuie.com/libsass-python/$(LIBSASS_WHEEL) -o $(PACKAGE_DIR)/$(LIBSASS_WHEEL) +# The lock file reads the local wheels from $(PACKAGE_DIR), and expects exactly one +# wheel per package there. Building them first clears out older copies. +LOCAL_WHEELS = $(PACKAGE_DIR)/$(HTMLTOOLS_WHEEL) $(PACKAGE_DIR)/$(SHINY_WHEEL) \ + $(PACKAGE_DIR)/$(SHINYWIDGETS_WHEEL) $(PACKAGE_DIR)/$(FAICONS_WHEEL) \ + $(PACKAGE_DIR)/$(LIBSASS_WHEEL) + ## Update the shinylive_lock.json file, based on shinylive_requirements.json -update_packages_lock: $(PYBIN) $(BUILD_DIR)/shinylive/pyodide +update_packages_lock: $(PYBIN) $(BUILD_DIR)/shinylive/pyodide $(LOCAL_WHEELS) $(PYBIN)/pip install -r requirements-dev.txt . $(PYBIN)/activate && scripts/pyodide_packages.py generate_lockfile ## Update the shinylive_lock.json file, but with local packages only -update_packages_lock_local: $(PYBIN) $(BUILD_DIR)/shinylive/pyodide +update_packages_lock_local: $(PYBIN) $(BUILD_DIR)/shinylive/pyodide $(LOCAL_WHEELS) $(PYBIN)/pip install -r requirements-dev.txt . $(PYBIN)/activate && scripts/pyodide_packages.py update_lockfile_local diff --git a/scripts/pyodide_packages.py b/scripts/pyodide_packages.py index 05688746..2c280d57 100755 --- a/scripts/pyodide_packages.py +++ b/scripts/pyodide_packages.py @@ -95,8 +95,6 @@ # doing table styling. "jinja2", ], - # Can be removed when we moved to pyodide w/ python 3.11 - "anyio": ["exceptiongroup"], # palmerpenguins imports pkg_resources, which is part of setuptools. "palmerpenguins": ["setuptools"], # Pyodide 314's own starlette entry lists no dependencies, but it imports anyio. @@ -701,15 +699,14 @@ def orig_pyodide_lock() -> PyodidePackagesFile: ) shutil.copy(pyodide_lock_json_file, orig_pyodide_lock_json_file) else: - try: - # Pyodide 0.28 dropped `info.version`, and what is left is the same - # across patch releases. A pyodide-lock.json without shiny in it is a - # fresh one from a newly unpacked Pyodide, so it replaces the saved copy. - with open(pyodide_lock_json_file, "r") as f_new: - new_packages = json.load(f_new).get("packages", {}) - if "shiny" not in new_packages: - shutil.copy(pyodide_lock_json_file, orig_pyodide_lock_json_file) - except Exception: + # Pyodide 0.28 dropped `info.version`, and what is left is the same across + # patch releases. A pyodide-lock.json without shiny in it is a fresh one from + # a newly unpacked Pyodide, so it replaces the saved copy. This assumes + # Pyodide never ships shiny itself. An unreadable file raises rather than + # replacing the saved copy, which may be the only clean one left. + with open(pyodide_lock_json_file, "r") as f_new: + new_packages = json.load(f_new).get("packages", {}) + if "shiny" not in new_packages: shutil.copy(pyodide_lock_json_file, orig_pyodide_lock_json_file) with open(orig_pyodide_lock_json_file, "r") as f: From 098095f2245b47fb2e4fb3e3553089e2222df8e9 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 10 Nov 2026 14:03:42 -0500 Subject: [PATCH 4/4] docs(memory-bank): update the Python package notes for Pyodide 314 --- memory-bank/pythonPackageVersions.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/memory-bank/pythonPackageVersions.md b/memory-bank/pythonPackageVersions.md index 59d557af..f208962b 100644 --- a/memory-bank/pythonPackageVersions.md +++ b/memory-bank/pythonPackageVersions.md @@ -33,7 +33,8 @@ One subcommand per Makefile target. All run inside `venv/` with `requirements-de - `generate_lockfile` (`make update_packages_lock`) — reads every requirement. Local packages get their info from the one wheel in `packages/` whose name starts with `-` (exactly one must - exist), read with `pkginfo`. PyPI packages come from `https://pypi.org/pypi/[/]/json`; + exist), read with `pkginfo`. Both lock targets depend on those wheels in the Makefile + (`LOCAL_WHEELS`), and each wheel's rule removes older copies before building. PyPI packages come from `https://pypi.org/pypi/[/]/json`; `_find_pypi_meta_with_wheel` takes the first `py3-none-any.whl`, and if the chosen release has none, walks back to the newest release that does. `_recurse_dependencies_lockfile` then adds each dependency that is not already in the lockfile or in the original Pyodide lock, always at @@ -52,12 +53,15 @@ One subcommand per Makefile target. All run inside `venv/` with `requirements-de `update_pyodide_pyodide_lock_json`) — loads the original Pyodide lock via `orig_pyodide_lock()`, adds each lockfile package with `install_dir: "site"`, and computes the SHA256 of local wheels from `packages/`. It then appends `EXTRA_DEPENDENCIES` (jinja2 for pandas, - exceptiongroup for anyio, setuptools for palmerpenguins) and drops `OMIT_DEPENDENCIES` (shiny + setuptools for palmerpenguins, anyio for Pyodide's starlette) and drops `OMIT_DEPENDENCIES` (shiny from shinychat, to break a cycle). The result is written without indentation. -`orig_pyodide_lock()` keeps `pyodide-lock.orig.json` next to `pyodide-lock.json`. It copies the -current file over it when the copy is missing or when the `info.version` fields differ, so the -script can be run again without stacking additions. +`orig_pyodide_lock()` keeps `pyodide-lock.orig.json` next to `pyodide-lock.json`, so the script +can be run again without stacking additions. It copies the current file over it when the copy +is missing or when the current file has no `shiny` entry, which means Pyodide was just unpacked +(Pyodide 0.28 dropped `info.version`, so versions cannot be compared). This assumes Pyodide +never ships shiny. A current file that cannot be read raises an error rather than replacing +the saved copy. Dependency filtering (`_filter_requires`): any requirement with a `;` marker is dropped, except `platform_system == "Emscripten"` (kept for mizani's tzdata). Names in `AVOID_DEPEND_PACKAGES`