diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 940c28a1..aed4a69d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,8 +69,10 @@ jobs: - name: Upload test config uses: actions/upload-artifact@v3 with: - name: pyproject-toml - path: pyproject.toml + name: test-assets + path: | + pyproject.toml + examples if-no-files-found: error test: @@ -145,7 +147,7 @@ jobs: - uses: actions/download-artifact@v3 with: - name: pyproject-toml + name: test-assets - name: Install test deps run: |- @@ -157,6 +159,8 @@ jobs: jupyter labextension list 2>&1 | grep -ie "@jupyterlite/pyodide-kernel.*OK" - name: Run the tests + env: + LITE_PYODIDE_KERNEL_DEMO: ./examples run: pytest - name: Upload reports diff --git a/.gitignore b/.gitignore index 19fe2fd9..f762e45e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ node_modules/ *.egg-info/ .ipynb_checkpoints *.tsbuildinfo -jupyterlite_pyodide_kernel/labextension +src/jupyterlite_pyodide_kernel/labextension # Created by https://www.gitignore.io/api/python # Edit at https://www.gitignore.io/?templates=python @@ -114,8 +114,8 @@ dmypy.json # generated packages/pyodide-kernel/pypi/ -packages/pyodide-kernel/pypi/all.json +packages/pyodide-kernel/pypi/*.json _pypi.ts .jupyterlite.doit.db _output -jupyterlite_pyodide_kernel/tests/fixtures/.pyodide*/ +src/jupyterlite_pyodide_kernel/tests/fixtures/.pyodide*/ diff --git a/.prettierignore b/.prettierignore index 7a59b128..eea23f8c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,7 +2,7 @@ node_modules/ **/node_modules **/lib **/package.json -jupyterlite_pyodide_kernel/labextension/ +src/jupyterlite_pyodide_kernel/labextension/ **/.pyodide/ .pytest_cache/ docs/ @@ -13,3 +13,4 @@ CHANGELOG.md # generated _pypi.ts **/pypi/all.json +**/pypi/repodata.json diff --git a/.readthedocs.yml b/.readthedocs.yml index a8baecd0..92311b37 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -7,23 +7,16 @@ build: nodejs: '18' apt_packages: - libarchive-dev - jobs: pre_build: + - python -m pip install -r requirements-dev.txt - jlpm --frozen-lockfile - - jlpm build + - jlpm build:prod + - jlpm dev - jlpm dist # pre-build the lite site to isolate build errors - jlpm docs:lite -python: - install: - - method: pip - path: . - extra_requirements: - - dev - - docs - sphinx: builder: html configuration: docs/conf.py diff --git a/examples/intro.ipynb b/examples/intro.ipynb index 674d8357..67525be8 100644 --- a/examples/intro.ipynb +++ b/examples/intro.ipynb @@ -1,63 +1,234 @@ { - "metadata": { - "kernelspec": { - "name": "python", - "display_name": "Python (Pyodide)", - "language": "python" - }, - "language_info": { - "codemirror_mode": { - "name": "python", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8" - } - }, - "nbformat_minor": 5, - "nbformat": 4, - "cells": [ - { - "cell_type": "markdown", - "source": "# Introduction to `jupyterlite-pyodide-kernel`\n\n`jupyterlite-pyodide-kernel` extends [JupyterLite](https://jupyterlite.rtfd.io) with a Python 3 kernel, powered by [Pyodide](https://pyodide.org/).", - "metadata": {}, - "id": "ab1dc4ef-36ae-4893-8fb4-474440fabc8a" - }, - { - "cell_type": "markdown", - "source": "## Visualizing data in a Notebook ✨\n\nBelow is an example of a code cell. We'll visualize some simple data using two popular\npackages in Python. We'll use [NumPy](https://numpy.org/) to create some random data,\nand [Matplotlib](https://matplotlib.org) to visualize it.\n\nNote how the code and the results of running the code are bundled together.", - "metadata": {}, - "id": "a6372e17-bf46-48c5-90a3-bdd6236e376e" - }, - { - "cell_type": "code", - "source": "from matplotlib import pyplot as plt\nimport numpy as np\n\n# Generate 100 random data points along 3 dimensions\nx, y, scale = np.random.randn(3, 100)\nfig, ax = plt.subplots()\n\n# Map each onto a scatterplot we'll create with Matplotlib\nax.scatter(x=x, y=y, c=scale, s=np.abs(scale) * 500)\nax.set(title=\"Some random data, created with JupyterLab!\")\nplt.show()", - "metadata": { - "trusted": true - }, - "execution_count": null, - "outputs": [], - "id": "fe55883a-6887-43dd-9498-5333a51799e2" - }, - { - "cell_type": "markdown", - "source": "## Interact with Widgets 🎛️\n\n[Jupyter Widgets](https://jupyter.org/widgets) power rich interactivity between users of Jupyter clients and Jupyter kernels. With `jupyterlab_widgets` installed, all of the [core widgets](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html) will work in `jupyterlite-pyodide-kernel`, as do many third-party packages.", - "metadata": {}, - "id": "61bd8fda-3e36-4910-b319-668e8843f322" - }, - { - "cell_type": "code", - "source": "%pip install ipywidgets\nfrom ipywidgets import *\nslider = FloatSlider()\nreadout = FloatText()\njslink((slider, \"value\"), (readout, \"value\"))\nHBox([slider, readout])", - "metadata": { - "trusted": true - }, - "execution_count": null, - "outputs": [], - "id": "b034c439-aea0-4880-9118-75f7a2b1092a" - } - ] -} \ No newline at end of file + "cells": [ + { + "cell_type": "markdown", + "id": "ab1dc4ef-36ae-4893-8fb4-474440fabc8a", + "metadata": {}, + "source": [ + "# Introduction to `jupyterlite-pyodide-kernel`\n", + "\n", + "[`jupyterlite-pyodide-kernel`](https://github.com/jupyterlite/pyodide-kernel) extends [JupyterLite](https://jupyterlite.rtfd.io) with a Python 3 kernel, powered by [Pyodide](https://pyodide.org/) and [IPython](https://ipython.readthedocs.io).\n", + "\n", + "> _ℹ️ Use the ▸ button, or shift+enter to run cells below_" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db046898-d539-444b-a1e2-e95b4b40d989", + "metadata": {}, + "outputs": [], + "source": [ + "import this" + ] + }, + { + "cell_type": "markdown", + "id": "edd8be2c-16a1-47c5-ae5d-7b70ff3938b2", + "metadata": {}, + "source": [ + "### Example: Interactive Widgets\n", + "\n", + "[Jupyter Widgets](https://jupyter.org/widgets) power rich interactivity between users of Jupyter clients and Jupyter kernels. With `jupyterlab_widgets` installed, all of the [core widgets](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html) will work in `jupyterlite-pyodide-kernel`, as do many third-party packages.\n", + "\n", + "> ⚠️ _`ipywidgets` _and_ `jupyterlab_widgets` were included at **build time** of this site, and are **not** available by default when installing `jupyterlite-pyodide-kernel`._" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b034c439-aea0-4880-9118-75f7a2b1092a", + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import *\n", + "slider = FloatSlider()\n", + "readout = FloatText()\n", + "jslink((slider, \"value\"), (readout, \"value\"))\n", + "HBox([slider, readout])" + ] + }, + { + "cell_type": "markdown", + "id": "cd2f8599-4985-4db7-8f75-e447cb4801fc", + "metadata": {}, + "source": [ + "## What is JupyterLite?\n", + "\n", + "JupyterLite is a **limited** implementation of the [Jupyter architecture](https://jupyter.org).\n", + "\n", + "Like the tradtional JupyterLab client/server stack of **tools for interactive computing**, it provides:\n", + "- a subset of the [Jupyter **Server** API](https://jupyter-server.readthedocs.io/en/latest/developers/rest-api.html), implemented in JavaScript\n", + "- browser-based **kernels**, like JupyterLite Pyodide Kernel, which run entirely in the browser\n", + "- a Jupyter browser **client** based on [JupyterLab](https://jupyterlab.readthedocs.io) and many of its [extensions](https://pypi.org/search/?q=&o=&c=Framework+%3A%3A+Jupyter+%3A%3A+JupyterLab+%3A%3A+Extensions+%3A%3A+Prebuilt)\n", + "- the Jupyter [Notebook **format**](https://nbformat.readthedocs.io)" + ] + }, + { + "cell_type": "markdown", + "id": "b559d0e6-03bf-4706-b5f5-4fd1ef5c0a36", + "metadata": {}, + "source": [ + "## What is Pyodide?\n", + "\n", + "Pyodide is a distribution of [CPython](https://github.com/python/cpython), compiled to [WebAssembly](https://webassembly.org) with [Emscripten](https://emscripten.org) which runs entirely within the browser." + ] + }, + { + "cell_type": "markdown", + "id": "ddcc1b90-cbaf-422b-915e-448be20617cd", + "metadata": {}, + "source": [ + "## What _can't_ it do?\n", + "\n", + "While a very broad subset of the CPython standard library, and much of the open source, scientific Python ecosystem are available, there are some limitation.\n", + "\n", + "The best way to find out is to **try it out**! However, some known implementation-level limitations are listed below." + ] + }, + { + "cell_type": "markdown", + "id": "f2a6e1fc-589f-46f5-aef5-1878c476a213", + "metadata": {}, + "source": [ + "### Differences from CPython\n", + "\n", + "A number of aspects of the underlying Python implementation that don't work the same way as their upstream counterparts, if at all. Uses of these features will need to be updated, or put behind some check.\n", + "\n", + "- no threads\n", + "- no subprocesses\n", + "- no custom event asynchronous loops\n", + "- package `data_files` are not installed\n", + "- some blocking features are made asynchronous\n", + " - `input` must be `await`ed\n", + " - the `urllib` family of HTTP features don't work\n", + " - though [pyodide-http](https://github.com/koenvo/pyodide-http) can be explicitly installed, imported and used" + ] + }, + { + "cell_type": "markdown", + "id": "961bb18f-954b-4624-bf85-774d451addec", + "metadata": {}, + "source": [ + "### Differences from `IPython` and `ipykernel`\n", + "\n", + "`jupyterlite-pyodide-kernel` ships a _shim_ of `ipykernel`, and changes a number of features in `ipython`\n", + "\n", + "- many [magics](https://ipython.readthedocs.io/en/stable/interactive/magics.html) don't work" + ] + }, + { + "cell_type": "markdown", + "id": "896da1d0-f51b-4c76-a25a-c448455e4960", + "metadata": {}, + "source": [ + "### Differences from Pyodide\n", + "Because `jupyterlite-pyodide-kernel` runs inside a [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers), a number of features don't work (yet).\n", + "\n", + "- no access to the browser's DOM\n", + " - in particular, the Pyodide [matplotlib](https://github.com/pyodide/matplotlib-pyodide) backend will not work" + ] + }, + { + "cell_type": "markdown", + "id": "75187813-fe26-47d6-84c2-eb5f4ac4bd91", + "metadata": {}, + "source": [ + "## Where are all the Pyodide packages?\n", + "\n", + "This site follows the pattern for an [offline site](https://jupyterlite.readthedocs.io/en/0.1.0-beta/howto/configure/advanced/offline.html) based on a minimal, but fully self-contained, distribution of Pyodide. \n", + "\n", + "This means:\n", + "- no additional resources are loaded from any other sites on the internet\n", + " - serving from a content delivery network (CDN) is often faster, but _could_ disappear or change in the future \n", + "- only the Python 3.10 standard library is available by default\n", + "\n", + "> ℹ️ For a more full-featured site with full Pyodide distributions, hosted from a CDN, see: \n", + ">\n", + "> - the JupyterLite [documentation](https://jupyterlite.readthedocs.io) on [ReadTheDocs](https://jupyterlite.readthedocs.io/en/0.1.0-beta/reference/demo.html)\n", + "> - the JupyterLite [demo](https://jupyterlite.github.io/demo) on [GitHub Pages](https://jupyterlite.readthedocs.io/en/0.1.0-beta/quickstart/deploy.html)" + ] + }, + { + "cell_type": "markdown", + "id": "5aaff9a8-9dfa-4180-8c9f-a2930557b3b2", + "metadata": {}, + "source": [ + "## How do I use more Python packages?\n", + "\n", + "Simple python packages can be installed at runtime through one of the following methods.\n", + "\n", + "> ℹ️ Learn more on the [JupyterLite documentation](https://jupyterlite.readthedocs.io/en/latest/howto/python/packages.html), including how to make more packages available." + ] + }, + { + "cell_type": "markdown", + "id": "4a9ffff0-e38c-47cf-b991-40dd17359b0c", + "metadata": {}, + "source": [ + "### At runtime\n", + "\n", + "Packages not added (or known of) at build time can be imported with the [`%pip`](https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-pip) magic. This gives site visitors the freedom to explore beyond what a site builder even imagined at build time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2154019e-bd81-4313-a88a-445c5cf673f9", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install -q emoji" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68b2607f-035c-48f9-ab21-316228085644", + "metadata": {}, + "outputs": [], + "source": [ + "import emoji\n", + "b, m, s = \":badger:\", \":mushroom:\", \":snake:\"\n", + "emoji.emojize(3 * (b * 12 + m * 2) + s * 3)" + ] + }, + { + "cell_type": "markdown", + "id": "58aa75f7-6f13-4c08-a83d-2b776116db7e", + "metadata": {}, + "source": [ + "## How do I use more client extension packages?\n", + "\n", + "Some python packages, like all Jupyter widgets, will require client extensions, and need to be available when the site loads for a site visitor.\n", + "\n", + "Any number of JupyterLab [extensions]() can be [added at **build time**](https://jupyterlite.readthedocs.io/en/latest/howto/configure/simple_extensions.html), but not all will be compatible:\n", + "\n", + "- some extensions require a [Jupyter Server extension](https://jupyter-server.readthedocs.io/en/latest/developers/extensions.html)\n", + " - it is _possible_ to [extend the JupyterLite server](https://jupyterlite.readthedocs.io/en/latest/howto/extensions/server.html)\n", + "- some extensions require a different version of JupyterLab than the one JupyterLite is based on" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/jupyter_lite_config.json b/examples/jupyter_lite_config.json index c6b92607..a85c9525 100644 --- a/examples/jupyter_lite_config.json +++ b/examples/jupyter_lite_config.json @@ -2,11 +2,36 @@ "LiteBuildConfig": { "contents": ["."], "output_dir": "../build/docs-app", - "ignore_sys_prefix": ["federated_extensions"], "federated_extensions": [ - "https://files.pythonhosted.org/packages/df/82/4576cbc07ebace8c7734fe08b2c2f9123b7ebecd29e932a3b839b6bee2cb/jupyterlab_widgets-3.0.5-py3-none-any.whl", - "../jupyterlite_pyodide_kernel/labextension" + "https://files.pythonhosted.org/packages/df/82/4576cbc07ebace8c7734fe08b2c2f9123b7ebecd29e932a3b839b6bee2cb/jupyterlab_widgets-3.0.5-py3-none-any.whl" ], "cache_dir": "../build/.lite-cache" + }, + "PipliteAddon": { + "piplite_urls": [ + "https://files.pythonhosted.org/packages/0b/42/d9d95cc461f098f204cd20c85642ae40fbff81f74c300341b8d0e0df14e0/Pygments-2.14.0-py3-none-any.whl", + "https://files.pythonhosted.org/packages/20/f4/c0584a25144ce20bfcf1aecd041768b8c762c1eb0aa77502a3f0baa83f11/wcwidth-0.2.6-py2.py3-none-any.whl", + "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", + "https://files.pythonhosted.org/packages/28/3c/bc3819dd8b1a1588c9215a87271b6178cc5498acaa83885211f5d4d9e693/executing-1.2.0-py2.py3-none-any.whl", + "https://files.pythonhosted.org/packages/2b/27/77f9d5684e6bce929f5cfe18d6cfbe5133013c06cb2fbf5933670e60761d/pure_eval-0.2.2-py3-none-any.whl", + "https://files.pythonhosted.org/packages/39/7b/88dbb785881c28a102619d46423cb853b46dbccc70d3ac362d99773a78ce/pexpect-4.8.0-py2.py3-none-any.whl", + "https://files.pythonhosted.org/packages/4c/1c/ff6546b6c12603d8dd1070aa3c3d273ad4c07f5771689a7b69a550e8c951/backcall-0.2.0-py2.py3-none-any.whl", + "https://files.pythonhosted.org/packages/6a/81/aa96c25c27f78cdc444fec27d80f4c05194c591465e491a1358d8a035bc1/stack_data-0.6.2-py3-none-any.whl", + "https://files.pythonhosted.org/packages/6c/10/a7d0fa5baea8fe7b50f448ab742f26f52b80bfca85ac2be9d35cdd9a3246/pyparsing-3.0.9-py3-none-any.whl", + "https://files.pythonhosted.org/packages/77/75/c28e9ef7abec2b7e9ff35aea3e0be6c1aceaf7873c26c95ae1f0d594de71/traitlets-5.9.0-py3-none-any.whl", + "https://files.pythonhosted.org/packages/9a/41/220f49aaea88bc6fa6cba8d05ecf24676326156c23b991e80b3f2fc24c77/pickleshare-0.7.5-py2.py3-none-any.whl", + "https://files.pythonhosted.org/packages/a1/7c/2f5a9b925a66261b39a9c4181c9cc4500ff5547d7b1efc76f5cba40a1d0e/micropip-0.2.0-py3-none-any.whl", + "https://files.pythonhosted.org/packages/ac/91/23e08c442657cf493598b0222008437c9e0aef0709a8fd65a5d5d68ffa21/ipython-8.11.0-py3-none-any.whl", + "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", + "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", + "https://files.pythonhosted.org/packages/ed/35/a31aed2993e398f6b09a790a181a7927eb14610ee8bbf02dc14d31677f1c/packaging-23.0-py3-none-any.whl", + "https://files.pythonhosted.org/packages/f2/51/c34d7a1d528efaae3d8ddb18ef45a41f284eacf9e514523b191b7d0872cc/matplotlib_inline-0.1.6-py3-none-any.whl", + "https://files.pythonhosted.org/packages/f3/e1/64679d9d0759db5b182222c81ff322c2fe2c31e156a59afd6e9208c960e5/asttokens-2.2.1-py2.py3-none-any.whl", + "https://files.pythonhosted.org/packages/f3/fc/bd076538811d63babf8ceea0ff3d8d024171569a47f5dba7757c5fd0462c/ipywidgets-8.0.4-py3-none-any.whl" + ] + }, + "PyodideAddon": { + "install_on_import": true, + "pyodide_url": "https://github.com/pyodide/pyodide/releases/download/0.22.1/pyodide-core-0.22.1.tar.bz2" } } diff --git a/examples/pypi/repodata.json b/examples/pypi/repodata.json new file mode 100644 index 00000000..32209071 --- /dev/null +++ b/examples/pypi/repodata.json @@ -0,0 +1,17 @@ +{ + "packages": { + "sqlite3": { + "depends": [], + "file_name": "https://cdn.jsdelivr.net/pyodide/v0.22.1/full/sqlite3-1.0.0.zip", + "imports": [ + "sqlite3", + "_sqlite3" + ], + "install_dir": "stdlib", + "name": "sqlite3", + "sha256": "0d1b4bff8e89b57633dc63989225c88e54ed86e7c7476c7ab7f5cd4638019819", + "shared_library": true, + "version": "1.0.0" + } + } +} diff --git a/jupyterlite_pyodide_kernel/addons/_base.py b/jupyterlite_pyodide_kernel/addons/_base.py deleted file mode 100644 index cbf8e6d6..00000000 --- a/jupyterlite_pyodide_kernel/addons/_base.py +++ /dev/null @@ -1,94 +0,0 @@ -"""common addon features for ``jupyterlite-pyodide-kernel`` - -This should not be considered part of the public API, and much will disappear -when these features are added upstream: - - https://github.com/jupyterlite/jupyterlite/issues/996 -""" -import json -from pathlib import Path -from typing import Generator, Dict, Any -from jupyterlite_core.addons.base import BaseAddon -from jupyterlite_core.constants import ( - JUPYTERLITE_IPYNB, - JUPYTERLITE_JSON, - UTF8, - JUPYTERLITE_METADATA, - JUPYTER_CONFIG_DATA, - LITE_PLUGIN_SETTINGS, - JSON_FMT, -) - -from ..constants import PYODIDE_KERNEL_PLUGIN_ID - -__all__ = ["_BaseAddon"] - - -class _BaseAddon(BaseAddon): - def get_pyodide_settings(self, config_path: Path): - """Get the settings for the client-side Pyodide kernel.""" - return self.get_lite_plugin_settings(config_path, PYODIDE_KERNEL_PLUGIN_ID) - - def set_pyodide_settings(self, config_path: Path, settings: Dict[str, Any]) -> None: - """Update the settings for the client-side Pyodide kernel.""" - return self.set_lite_plugin_settings( - config_path, PYODIDE_KERNEL_PLUGIN_ID, settings - ) - - def get_output_config_paths(self) -> Generator[Path, None, None]: - """Yield an iterator of all config paths that _might_ exist in the - ``output_dir``. - - This will likely move upstream. - """ - for app in [None, *self.manager.apps]: - app_dir = self.manager.output_dir / app if app else self.manager.output_dir - for path_name in [JUPYTERLITE_JSON, JUPYTERLITE_IPYNB]: - config_path = app_dir / path_name - yield config_path - - def get_lite_plugin_settings( - self, config_path: Path, plugin_id: str - ) -> Dict[str, Any]: - """Get the plugin settings from a config path. - - The keys follow the JupyterLab settings naming convention, of module and - identifier e.g. - - @jupyterlite/contents:plugin - - This will likely move upstream. - """ - if not config_path.exists(): - return {} - - config = json.loads(config_path.read_text(**UTF8)) - - # if a notebook, look in the top-level metadata (which must exist) - if config_path.name == JUPYTERLITE_IPYNB: - config = config["metadata"].get(JUPYTERLITE_METADATA, {}) - - return ( - config.get(JUPYTER_CONFIG_DATA, {}) - .get(LITE_PLUGIN_SETTINGS, {}) - .get(plugin_id, {}) - ) - - def set_lite_plugin_settings( - self, config_path: Path, plugin_id: str, settings: Dict[str, Any] - ) -> None: - """Overwrite the plugin settings for a single plugin in a config path. - - This will likely move upstream. - """ - whole_file = config = json.loads(config_path.read_text(**UTF8)) - if config_path.name == JUPYTERLITE_IPYNB: - config = whole_file["metadata"][JUPYTERLITE_METADATA] - - config.setdefault(JUPYTER_CONFIG_DATA, {}).setdefault( - LITE_PLUGIN_SETTINGS, {} - ).update({plugin_id: settings}) - - config_path.write_text(json.dumps(whole_file, **JSON_FMT), **UTF8) - self.log.debug("%s wrote settings in %s: %s", plugin_id, config_path, settings) - self.maybe_timestamp(config_path) diff --git a/jupyterlite_pyodide_kernel/addons/piplite.py b/jupyterlite_pyodide_kernel/addons/piplite.py deleted file mode 100644 index acf3adb4..00000000 --- a/jupyterlite_pyodide_kernel/addons/piplite.py +++ /dev/null @@ -1,365 +0,0 @@ -"""a JupyterLite addon for supporting piplite wheels""" - -import datetime -import json -import re -import urllib.parse -from hashlib import md5, sha256 -from pathlib import Path -from typing import Tuple as _Tuple - -import doit.tools -from jupyterlite_core.constants import ( - ALL_JSON, - JSON_FMT, - JUPYTERLITE_JSON, - LAB_EXTENSIONS, - UTF8, -) -from jupyterlite_core.trait_types import TypedTuple -from traitlets import Unicode - -from ._base import _BaseAddon - -from ..constants import ( - ALL_WHL, - PIPLITE_INDEX_SCHEMA, - PIPLITE_URLS, - PKG_JSON_PIPLITE, - PKG_JSON_WHEELDIR, - PYODIDE_KERNEL_NPM_NAME, - PYPI_WHEELS, - KERNEL_SETTINGS_SCHEMA, -) - - -class PipliteAddon(_BaseAddon): - __all__ = ["post_init", "build", "post_build", "check"] - - # traits - piplite_urls: _Tuple[str] = TypedTuple( - Unicode(), - help="Local paths or URLs of piplite-compatible wheels to copy and index", - ).tag(config=True) - - # CLI - aliases = { - "piplite-wheels": "PipliteAddon.piplite_urls", - } - - @property - def output_wheels(self): - """where wheels will go in the output folder""" - return self.manager.output_dir / PYPI_WHEELS - - @property - def wheel_cache(self): - """where wheels will go in the cache folder""" - return self.manager.cache_dir / "wheels" - - @property - def output_extensions(self): - """where labextensions will go in the output folder""" - return self.manager.output_dir / LAB_EXTENSIONS - - @property - def output_kernel_extension(self): - """the location of the Pyodide kernel labextension static assets""" - return self.output_extensions / PYODIDE_KERNEL_NPM_NAME - - @property - def schemas(self): - """the path to the as-deployed schema in the labextension""" - return self.output_kernel_extension / "static/schema" - - @property - def piplite_schema(self): - """the schema for Warehouse-like API indexes""" - return self.schemas / PIPLITE_INDEX_SCHEMA - - @property - def settings_schema(self): - """the schema for the Pyodide kernel labextension""" - return self.schemas / KERNEL_SETTINGS_SCHEMA - - def post_init(self, manager): - """handle downloading of wheels""" - for path_or_url in self.piplite_urls: - yield from self.resolve_one_wheel(path_or_url) - - def build(self, manager): - """yield a doit task to copy each local wheel into the output_dir""" - for wheel in list_wheels(manager.lite_dir / PYPI_WHEELS): - yield from self.resolve_one_wheel(str(wheel.resolve())) - - def post_build(self, manager): - """update the root jupyter-lite.json with user-provided ``pipliteUrls``""" - jupyterlite_json = manager.output_dir / JUPYTERLITE_JSON - whl_metas = [] - - wheels = list_wheels(self.output_wheels) - pkg_jsons = sorted( - [ - *self.output_extensions.glob("*/package.json"), - *self.output_extensions.glob("@*/*/package.json"), - ] - ) - - for wheel in wheels: - whl_meta = self.wheel_cache / f"{wheel.name}.meta.json" - whl_metas += [whl_meta] - yield self.task( - name=f"meta:{whl_meta.name}", - doc=f"ensure {wheel} metadata", - file_dep=[wheel], - actions=[ - (doit.tools.create_folder, [whl_meta.parent]), - (self.index_wheel, [wheel, whl_meta]), - ], - targets=[whl_meta], - ) - - if whl_metas or pkg_jsons: - whl_index = self.manager.output_dir / PYPI_WHEELS / ALL_JSON - - yield self.task( - name="patch", - doc=f"ensure {JUPYTERLITE_JSON} includes any piplite wheels", - file_dep=[*whl_metas, jupyterlite_json], - actions=[ - ( - self.patch_jupyterlite_json, - [jupyterlite_json, whl_index, whl_metas, pkg_jsons], - ) - ], - targets=[whl_index], - ) - - def check(self, manager): - """verify that all JSON for settings and Warehouse API are valid""" - - for config_path in self.get_output_config_paths(): - yield from self.check_one_config_path(config_path) - - def check_one_config_path(self, config_path): - """verify the settings and Warehouse API for a single jupyter-lite config""" - if not config_path.exists(): - return - - rel_path = config_path.relative_to(self.manager.output_dir) - config = self.get_pyodide_settings(config_path) - - yield self.task( - name=f"validate:settings:{rel_path}", - doc=f"validate {config_path} with the pyodide kernel settings schema", - actions=[ - (self.validate_one_json_file, [self.settings_schema, None, config]), - ], - file_dep=[self.settings_schema, config_path], - ) - - urls = config.get(PIPLITE_URLS, []) - - for wheel_index_url in urls: - yield from self.check_one_wheel_index(wheel_index_url) - - def check_one_wheel_index(self, wheel_index_url): - """validate one wheel index against the Warehouse schema""" - if not wheel_index_url.startswith("./"): # pragma: no cover - return - - wheel_index_url = wheel_index_url.split("?")[0].split("#")[0] - - path = self.manager.output_dir / wheel_index_url - - if not path.exists(): # pragma: no cover - return - - yield self.task( - name=f"validate:wheels:{wheel_index_url}", - doc=f"validate {wheel_index_url} with the piplite API schema", - file_dep=[path], - actions=[(self.validate_one_json_file, [self.piplite_schema, path])], - ) - - def resolve_one_wheel(self, path_or_url): - """download a single wheel, and copy to the cache""" - local_path = None - will_fetch = False - - if re.findall(r"^https?://", path_or_url): - url = urllib.parse.urlparse(path_or_url) - name = url.path.split("/")[-1] - dest = self.wheel_cache / name - local_path = dest - if not dest.exists(): - yield self.task( - name=f"fetch:{name}", - doc=f"fetch the wheel {name}", - actions=[(self.fetch_one, [path_or_url, dest])], - targets=[dest], - ) - will_fetch = True - else: - local_path = (self.manager.lite_dir / path_or_url).resolve() - - if local_path.is_dir(): - for wheel in list_wheels(local_path): - yield from self.copy_wheel(wheel) - elif local_path.exists() or will_fetch: - suffix = local_path.suffix - - if suffix == ".whl": - yield from self.copy_wheel(local_path) - - else: # pragma: no cover - raise FileNotFoundError(path_or_url) - - def copy_wheel(self, wheel): - """copy one wheel to output""" - dest = self.output_wheels / wheel.name - if dest == wheel: # pragma: no cover - return - yield self.task( - name=f"copy:whl:{wheel.name}", - file_dep=[wheel], - targets=[dest], - actions=[(self.copy_one, [wheel, dest])], - ) - - def patch_jupyterlite_json(self, config_path, user_whl_index, whl_metas, pkg_jsons): - """add the piplite wheels to jupyter-lite.json""" - plugin_config = self.get_pyodide_settings(config_path) - old_urls = plugin_config.get(PIPLITE_URLS, []) - - new_urls = [] - - # first add user-specified wheels from piplite_urls - if whl_metas: - metadata = {} - for whl_meta in whl_metas: - meta = json.loads(whl_meta.read_text(**UTF8)) - whl = self.output_wheels / whl_meta.name.replace(".json", "") - metadata[whl] = meta["name"], meta["version"], meta["release"] - - write_wheel_index(self.output_wheels, metadata) - user_whl_index_url, user_whl_index_url_with_sha = self.get_index_urls( - user_whl_index - ) - - added_build = False - - for url in old_urls: - if url.split("#")[0].split("?")[0] == user_whl_index_url: - new_urls += [user_whl_index_url_with_sha] - added_build = True - else: - new_urls += [url] - - if not added_build: - new_urls = [user_whl_index_url_with_sha, *new_urls] - else: - new_urls = old_urls - - # ...then add wheels from federated extensions... - for pkg_json in pkg_jsons or []: - pkg_data = json.loads(pkg_json.read_text(**UTF8)) - wheel_dir = pkg_data.get(PKG_JSON_PIPLITE, {}).get(PKG_JSON_WHEELDIR) - if wheel_dir: - pkg_whl_index = pkg_json.parent / wheel_dir / ALL_JSON - if pkg_whl_index.exists(): - pkg_whl_index_url_with_sha = self.get_index_urls(pkg_whl_index)[1] - if pkg_whl_index_url_with_sha not in new_urls: - new_urls += [pkg_whl_index_url_with_sha] - - # ... and only update if actually changed - if new_urls: - plugin_config[PIPLITE_URLS] = new_urls - self.set_pyodide_settings(config_path, plugin_config) - - def get_index_urls(self, whl_index): - """get output dir relative URLs for all.json files""" - whl_index_sha256 = sha256(whl_index.read_bytes()).hexdigest() - whl_index_url = f"./{whl_index.relative_to(self.manager.output_dir).as_posix()}" - whl_index_url_with_sha = f"{whl_index_url}?sha256={whl_index_sha256}" - return whl_index_url, whl_index_url_with_sha - - def index_wheel(self, whl_path, whl_meta): - """Generate an intermediate file representation to merge with other releases""" - name, version, release = get_wheel_fileinfo(whl_path) - whl_meta.write_text( - json.dumps(dict(name=name, version=version, release=release), **JSON_FMT), - **UTF8, - ) - self.maybe_timestamp(whl_meta) - - -def list_wheels(wheel_dir): - """get all wheels we know how to handle in a directory""" - return sorted(sum([[*wheel_dir.glob(f"*{whl}")] for whl in ALL_WHL], [])) - - -def get_wheel_fileinfo(whl_path): - """Generate a minimal Warehouse-like JSON API entry from a wheel""" - import pkginfo - - metadata = pkginfo.get_metadata(str(whl_path)) - whl_stat = whl_path.stat() - whl_isodate = ( - datetime.datetime.fromtimestamp(whl_stat.st_mtime, tz=datetime.timezone.utc) - .isoformat() - .split("+")[0] - + "Z" - ) - whl_bytes = whl_path.read_bytes() - whl_sha256 = sha256(whl_bytes).hexdigest() - whl_md5 = md5(whl_bytes).hexdigest() - - release = { - "comment_text": "", - "digests": {"sha256": whl_sha256, "md5": whl_md5}, - "downloads": -1, - "filename": whl_path.name, - "has_sig": False, - "md5_digest": whl_md5, - "packagetype": "bdist_wheel", - "python_version": "py3", - "requires_python": metadata.requires_python, - "size": whl_stat.st_size, - "upload_time": whl_isodate, - "upload_time_iso_8601": whl_isodate, - "url": f"./{whl_path.name}", - "yanked": False, - "yanked_reason": None, - } - - return metadata.name, metadata.version, release - - -def get_wheel_index(wheels, metadata=None): - """Get the raw python object representing a wheel index for a bunch of wheels - - If given, metadata should be a dictionary of the form: - - {Path: (name, version, metadata)} - """ - metadata = metadata or {} - all_json = {} - - for whl_path in sorted(wheels): - name, version, release = metadata.get(whl_path, get_wheel_fileinfo(whl_path)) - # https://peps.python.org/pep-0503/#normalized-names - normalized_name = re.sub(r"[-_.]+", "-", name).lower() - if normalized_name not in all_json: - all_json[normalized_name] = {"releases": {}} - all_json[normalized_name]["releases"][version] = [release] - - return all_json - - -def write_wheel_index(whl_dir, metadata=None): - """Write out an all.json for a directory of wheels""" - wheel_index = Path(whl_dir) / ALL_JSON - index_data = get_wheel_index(list_wheels(whl_dir), metadata) - wheel_index.write_text(json.dumps(index_data, **JSON_FMT), **UTF8) - return wheel_index diff --git a/jupyterlite_pyodide_kernel/addons/pyodide.py b/jupyterlite_pyodide_kernel/addons/pyodide.py deleted file mode 100644 index a180390b..00000000 --- a/jupyterlite_pyodide_kernel/addons/pyodide.py +++ /dev/null @@ -1,222 +0,0 @@ -"""a JupyterLite addon for supporting the pyodide distribution""" - -import os -import re -import urllib.parse -from pathlib import Path - -import doit.tools -from jupyterlite_core.constants import ( - JUPYTERLITE_JSON, -) -from traitlets import Unicode, default - -from ._base import _BaseAddon -from ..constants import ( - PYODIDE, - PYODIDE_JS, - PYODIDE_REPODATA, - PYODIDE_URL, -) - - -class PyodideAddon(_BaseAddon): - __all__ = ["status", "post_init", "build", "post_build", "check"] - - # traits - pyodide_url: str = Unicode( - allow_none=True, help="Local path or URL of a pyodide distribution tarball" - ).tag(config=True) - - # CLI - aliases = { - "pyodide": "PyodideAddon.pyodide_url", - } - - @default("pyodide_url") - def _default_pyodide_url(self): - return os.environ.get("JUPYTERLITE_PYODIDE_URL") - - @property - def pyodide_cache(self): - """where pyodide stuff will go in the cache folder""" - return self.manager.cache_dir / PYODIDE - - @property - def output_pyodide(self): - """where labextensions will go in the output folder""" - return self.manager.output_dir / "static" / PYODIDE - - @property - def well_known_pyodide(self): - """a well-known path where pyodide might be stored""" - return self.manager.lite_dir / "static" / PYODIDE - - def status(self, manager): - """report on the status of pyodide""" - yield self.task( - name="pyodide", - actions=[ - lambda: print( - f" URL: {self.pyodide_url}", - ), - lambda: print(f" archive: {[*self.pyodide_cache.glob('*.bz2')]}"), - lambda: print( - f" cache: {len([*self.pyodide_cache.rglob('*')])} files", - ), - lambda: print( - f" local: {len([*self.well_known_pyodide.rglob('*')])} files" - ), - ], - ) - - def post_init(self, manager): - """handle downloading of pyodide""" - if self.pyodide_url is None: - return - - yield from self.cache_pyodide(self.pyodide_url) - - def build(self, manager): - """copy a local (cached or well-known) pyodide into the output_dir""" - cached_pyodide = self.pyodide_cache / PYODIDE / PYODIDE - - the_pyodide = None - - if self.well_known_pyodide.exists(): - the_pyodide = self.well_known_pyodide - elif self.pyodide_url is not None: - the_pyodide = cached_pyodide - - if not the_pyodide: - return - - file_dep = [ - p - for p in the_pyodide.rglob("*") - if not (p.is_dir() or self.is_ignored_sourcemap(p.name)) - ] - - yield self.task( - name="copy:pyodide", - file_dep=file_dep, - targets=[ - self.output_pyodide / p.relative_to(the_pyodide) for p in file_dep - ], - actions=[(self.copy_one, [the_pyodide, self.output_pyodide])], - ) - - def post_build(self, manager): - """configure jupyter-lite.json for pyodide""" - if not self.well_known_pyodide.exists() and self.pyodide_url is None: - return - - jupyterlite_json = manager.output_dir / JUPYTERLITE_JSON - - output_js = self.output_pyodide / PYODIDE_JS - - yield self.task( - name=f"patch:{JUPYTERLITE_JSON}", - doc=f"ensure {JUPYTERLITE_JSON} includes any piplite wheels", - file_dep=[output_js], - actions=[ - ( - self.patch_jupyterlite_json, - [jupyterlite_json, output_js], - ) - ], - ) - - def check(self, manager): - """ensure the pyodide configuration is sound""" - for app in [None, *manager.apps]: - app_dir = manager.output_dir / app if app else manager.output_dir - jupyterlite_json = app_dir / JUPYTERLITE_JSON - - yield self.task( - name=f"config:{jupyterlite_json.relative_to(manager.output_dir)}", - file_dep=[jupyterlite_json], - actions=[(self.check_config_paths, [jupyterlite_json])], - ) - - def check_config_paths(self, jupyterlite_json): - config = self.get_pyodide_settings(jupyterlite_json) - - pyodide_url = config.get(PYODIDE_URL) - - if not pyodide_url or not pyodide_url.startswith("./"): - return - - pyodide_path = Path(self.manager.output_dir / pyodide_url).parent - assert pyodide_path.exists(), f"{pyodide_path} not found" - pyodide_js = pyodide_path / PYODIDE_JS - assert pyodide_js.exists(), f"{pyodide_js} not found" - pyodide_repodata = pyodide_path / PYODIDE_REPODATA - assert pyodide_repodata.exists(), f"{pyodide_repodata} not found" - - def patch_jupyterlite_json(self, config_path, output_js): - """update jupyter-lite.json to use the local pyodide""" - settings = self.get_pyodide_settings(config_path) - - url = "./{}".format(output_js.relative_to(self.manager.output_dir).as_posix()) - if settings.get(PYODIDE_URL) != url: - settings[PYODIDE_URL] = url - self.set_pyodide_settings(config_path, settings) - - def cache_pyodide(self, path_or_url): - """copy pyodide to the cache""" - if re.findall(r"^https?://", path_or_url): - url = urllib.parse.urlparse(path_or_url) - name = url.path.split("/")[-1] - dest = self.pyodide_cache / name - local_path = dest - if not dest.exists(): - yield self.task( - name=f"fetch:{name}", - doc=f"fetch the pyodide distribution {name}", - actions=[(self.fetch_one, [path_or_url, dest])], - targets=[dest], - ) - will_fetch = True - else: - local_path = (self.manager.lite_dir / path_or_url).resolve() - dest = self.pyodide_cache / local_path.name - will_fetch = False - - if local_path.is_dir(): - all_paths = sorted([p for p in local_path.rglob("*") if not p.is_dir()]) - yield self.task( - name=f"copy:pyodide:{local_path.name}", - file_dep=[*all_paths], - targets=[dest / p.relative_to(local_path) for p in all_paths], - actions=[(self.copy_one, [local_path, dest])], - ) - - elif local_path.exists() or will_fetch: - suffix = local_path.suffix - extracted = self.pyodide_cache / PYODIDE - - if suffix == ".bz2": - yield from self.extract_pyodide(local_path, extracted) - - else: # pragma: no cover - raise FileNotFoundError(path_or_url) - - def extract_pyodide(self, local_path, dest): - """extract a local pyodide tarball to the cache""" - - yield self.task( - name="extract:pyodide", - file_dep=[local_path], - uptodate=[ - doit.tools.config_changed( - dict(no_sourcemaps=self.manager.no_sourcemaps) - ) - ], - targets=[ - # there are a lot of js/data files, but we actually talk about these... - dest / PYODIDE / PYODIDE_JS, - dest / PYODIDE / PYODIDE_REPODATA, - ], - actions=[(self.extract_one, [local_path, dest])], - ) diff --git a/nx.json b/nx.json index 869b8667..5570abc0 100644 --- a/nx.json +++ b/nx.json @@ -19,17 +19,15 @@ "build": { "dependsOn": ["build:py", "^build"], "outputs": [ - "{projectRoot}/lib", - "{projectRoot}/jupyterlite_pyodide_kernel/labextension/package.json", - "{projectRoot}/jupyterlite_pyodide_kernel/labextension/static/pypi/all.json" + "{projectRoot}/src/jupyterlite_pyodide_kernel/labextension/package.json", + "{projectRoot}/src/jupyterlite_pyodide_kernel/labextension/static/pypi/all.json" ] }, "build:prod": { "dependsOn": ["build:py", "^build:prod"], "outputs": [ - "{projectRoot}/lib", - "{projectRoot}/jupyterlite_pyodide_kernel/labextension/package.json", - "{projectRoot}/jupyterlite_pyodide_kernel/labextension/static/pypi/all.json" + "{projectRoot}/src/jupyterlite_pyodide_kernel/labextension/package.json", + "{projectRoot}/src/jupyterlite_pyodide_kernel/labextension/static/pypi/all.json" ] } } diff --git a/package.json b/package.json index 90fb563f..f3ad4d35 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,15 @@ "dist:npm": "lerna run --stream dist", "dist:pypi": "pyproject-build .", "dist": "jlpm dist:pypi && jlpm dist:npm", - "docs:lite": "cd examples && jupyter lite build", + "docs:lite": "jlpm docs:lite:build && jlpm docs:lite:archive && jlpm docs:lite:check", + "docs:lite:build": "cd examples && jupyter lite build", + "docs:lite:archive": "cd examples && jupyter lite archive", + "docs:lite:check": "cd examples && jupyter lite check", "docs:sphinx": "sphinx-build -W -b html docs build/docs", "docs": "jlpm docs:lite && jlpm docs:sphinx", "eslint:check": "eslint . --cache --ext .ts,.tsx", "eslint": "jlpm eslint:check --fix", + "dev": "lerna run dev", "lint": "jlpm lint:js && jlpm lint:py", "lint:check": "jlpm lint:js:check && jlpm lint:py:check", "lint:js": "jlpm prettier && jlpm eslint", @@ -34,16 +38,17 @@ "lint:py": "jlpm lint:py:black && jlpm lint:py:ruff --fix-only", "lint:py:pip": "python -m pip check", "lint:py:check": "jlpm lint:py:pip && jlpm lint:py:black --check && jlpm lint:py:ruff", - "lint:py:black": "black scripts jupyterlite_pyodide_kernel packages/pyodide-kernel/py", - "lint:py:ruff": "ruff scripts jupyterlite_pyodide_kernel packages/pyodide-kernel/py", + "lint:py:black": "black scripts src packages/pyodide-kernel/py", + "lint:py:ruff": "ruff scripts src packages/pyodide-kernel/py", "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md,.yml,.yaml}\"", "prettier:check": "jlpm prettier:base --check", "prettier": "jlpm prettier:base --write --list-different", - "quickstart": "npm run setup:py && jlpm && jlpm deduplicate && jlpm clean:all && jlpm bootstrap && jlpm lint && jlpm build:prod && jlpm dist && jlpm docs && jlpm test", + "quickstart": "npm run setup:py && jlpm && jlpm deduplicate && jlpm clean:all && jlpm bootstrap && jlpm lint && jlpm build:prod && jlpm dist && jlpm dev && jlpm docs && jlpm test", "serve": "cd build && python -m http.server -b 127.0.0.1", - "setup:py": "python -m pip install -e \".[dev,lint,test,docs]\"", + "setup:py": "python -m pip install -r requirements-dev.txt", "test:py": "pytest", - "test": "jlpm test:py" + "test": "jlpm test:py", + "watch": "lerna run watch" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.45.0", @@ -53,8 +58,9 @@ "eslint-plugin-prettier": "^4.2.1", "lerna": "^6.5.1", "prettier": "^2.8.0", - "rimraf": "^3.0.2", - "typescript": "~4.9.3", - "yarn-deduplicate": "^6.0.1" + "rimraf": "^4.4.0", + "typescript": "~4.9.4", + "yarn-deduplicate": "^6.0.1", + "npm-run-all": "^4.1.5" } } diff --git a/packages/pyodide-kernel-extension/package.json b/packages/pyodide-kernel-extension/package.json index 4ae8d19a..93f2cf66 100644 --- a/packages/pyodide-kernel-extension/package.json +++ b/packages/pyodide-kernel-extension/package.json @@ -35,15 +35,16 @@ "build:prod": "jlpm build:lib && jlpm build:labextension", "build:labextension": "jupyter labextension build .", "build:labextension:dev": "jupyter labextension build --development True .", - "build:lib": "tsc", + "build:lib": "tsc -b", + "dev": "cd ../../ && jupyter labextension develop --overwrite .", "dist": "cd ../../dist && npm pack ../packages/pyodide-kernel-extension", "clean": "jlpm clean:lib", "clean:lib": "rimraf lib tsconfig.tsbuildinfo", - "clean:labextension": "rimraf ../../jupyterlite_pyodide_kernel/labextension", + "clean:labextension": "rimraf ../../src/jupyterlite_pyodide_kernel/labextension", "clean:all": "jlpm clean:lib && jlpm clean:labextension", "docs": "typedoc src", "watch": "run-p watch:src watch:labextension", - "watch:src": "tsc -w", + "watch:src": "tsc -b -w --preserveWatchOutput", "watch:labextension": "jupyter labextension watch ." }, "dependencies": { @@ -55,7 +56,7 @@ }, "devDependencies": { "@jupyterlab/builder": "^3.5.0", - "rimraf": "~3.0.0", + "rimraf": "~4.4.0", "typescript": "~4.9.4" }, "publishConfig": { @@ -63,13 +64,17 @@ }, "jupyterlab": { "extension": true, - "outputDir": "../../jupyterlite_pyodide_kernel/labextension", + "outputDir": "../../src/jupyterlite_pyodide_kernel/labextension", "webpackConfig": "webpack.config.js", "sharedPackages": { "@jupyterlite/kernel": { "bundled": false, "singleton": true }, + "@jupyterlite/pyodide-kernel": { + "bundled": true, + "singleton": true + }, "@jupyterlite/server": { "bundled": false, "singleton": true @@ -83,7 +88,7 @@ "jupyterlite": { "liteExtension": true }, - "piplite": { + "pyodideKernel": { "wheelDir": "static/pypi" } } diff --git a/packages/pyodide-kernel-extension/schema/kernel.v0.schema.json b/packages/pyodide-kernel-extension/schema/kernel.v0.schema.json index 561d69c5..6edda6e0 100644 --- a/packages/pyodide-kernel-extension/schema/kernel.v0.schema.json +++ b/packages/pyodide-kernel-extension/schema/kernel.v0.schema.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema", "$id": "https://jupyterlite-pyodide-kernel.readthedocs.org/en/latest/reference/schema/settings-v0.html#", "title": "Pyodide Kernel Settings Schema v0", - "description": "Pyodide-specific configuration values. Will be defined in another location in the future.", + "description": "Pyodide Kernel extension-specific configuration values, as stored in `jupyter-lite.json`", "type": "object", "properties": { "pyodideUrl": { @@ -19,11 +19,22 @@ "pipliteUrls": { "description": "Paths to PyPI-compatible API endpoints for wheels. If ending in ``all.json``, assumed to be an aggregate, keyed by package name, with relative paths", "type": "array", + "uniqueItems": true, "items": { - "type": "string" + "type": "string", + "format": "uri" }, + "default": [] + }, + "repodataUrls": { + "description": "Paths to pyodide-compatible ``repodata.json`` files.", "default": [], - "format": "uri" + "type": "array", + "uniqueItems": true, + "items": { + "type": "string", + "format": "uri" + } } } } diff --git a/packages/pyodide-kernel-extension/src/index.ts b/packages/pyodide-kernel-extension/src/index.ts index 94e43b89..abd023e7 100644 --- a/packages/pyodide-kernel-extension/src/index.ts +++ b/packages/pyodide-kernel-extension/src/index.ts @@ -20,7 +20,7 @@ const KERNEL_ICON_URL = `data:image/svg+xml;base64,${btoa(KERNEL_ICON_SVG_STR)}` /** * The default CDN fallback for Pyodide */ -const PYODIDE_CDN_URL = 'https://cdn.jsdelivr.net/pyodide/v0.22.0/full/pyodide.js'; +const PYODIDE_CDN_URL = 'https://cdn.jsdelivr.net/pyodide/v0.22.1/full/pyodide.js'; /** * The id for the extension, and key in the litePlugins. @@ -46,7 +46,11 @@ const kernel: JupyterLiteServerPlugin = { const url = config.pyodideUrl || PYODIDE_CDN_URL; const pyodideUrl = URLExt.parse(url).href; const rawPipUrls = config.pipliteUrls || []; + const rawRepoUrls = config.repodataUrls || []; const pipliteUrls = rawPipUrls.map((pipUrl: string) => URLExt.parse(pipUrl).href); + const repodataUrls = rawRepoUrls.map( + (repoUrl: string) => URLExt.parse(repoUrl).href + ); const disablePyPIFallback = !!config.disablePyPIFallback; kernelspecs.register({ @@ -75,6 +79,7 @@ const kernel: JupyterLiteServerPlugin = { ...options, pyodideUrl, pipliteUrls, + repodataUrls, disablePyPIFallback, mountDrive, }); diff --git a/packages/pyodide-kernel/package.json b/packages/pyodide-kernel/package.json index 75cd3503..868786f9 100644 --- a/packages/pyodide-kernel/package.json +++ b/packages/pyodide-kernel/package.json @@ -48,8 +48,7 @@ "test": "jest", "test:cov": "jest --collect-coverage", "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand", - "test:debug:watch": "node --inspect-brk node_modules/.bin/jest --runInBand --watch", - "watch": "tsc -b --watch" + "test:debug:watch": "node --inspect-brk node_modules/.bin/jest --runInBand --watch" }, "dependencies": { "@jupyterlite/contents": "^0.1.0-beta.18", @@ -62,19 +61,23 @@ "@types/jest": "^26.0.10", "esbuild": "0.17.10", "jest": "^26.4.2", - "pyodide": "0.22.0", - "rimraf": "~3.0.0", + "pyodide": "0.22.1", + "rimraf": "^4.4.0", "ts-jest": "^26.3.0", "typescript": "~4.9.4" }, "publishConfig": { "access": "public" }, - "pyodide-kernel": { + "pyodideKernel": { "packages": { - "py/pyodide-kernel": "0.0.5", - "py/piplite": "0.0.5", "py/ipykernel": "6.9.2", + "py/jedi": "0.18.2", + "py/jupyterlab_widgets1/jupyterlab_widgets": "1.1.2", + "py/jupyterlab_widgets3/jupyterlab_widgets": "3.0.5", + "py/piplite": "0.0.5", + "py/prompt_toolkit": "3.0.36", + "py/pyodide-kernel": "0.0.5", "py/widgetsnbextension3/widgetsnbextension": "3.6.0", "py/widgetsnbextension4/widgetsnbextension": "4.0.2" } diff --git a/packages/pyodide-kernel/py/jedi/LICENSE b/packages/pyodide-kernel/py/jedi/LICENSE new file mode 100644 index 00000000..b7375aa7 --- /dev/null +++ b/packages/pyodide-kernel/py/jedi/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022, JupyterLite Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/pyodide-kernel/py/jedi/README.md b/packages/pyodide-kernel/py/jedi/README.md new file mode 100644 index 00000000..2d44036a --- /dev/null +++ b/packages/pyodide-kernel/py/jedi/README.md @@ -0,0 +1,3 @@ +# jedi mock + +This is a jedi mock to avoid its import expense at kernel startup time. diff --git a/packages/pyodide-kernel/py/jedi/jedi/__init__.py b/packages/pyodide-kernel/py/jedi/jedi/__init__.py new file mode 100644 index 00000000..a553ff79 --- /dev/null +++ b/packages/pyodide-kernel/py/jedi/jedi/__init__.py @@ -0,0 +1,7 @@ +"""A jedi mock""" + +__version__ = "0.18.2" + + +class settings: + case_insensitive_completion = None diff --git a/packages/pyodide-kernel/py/jedi/jedi/api/__init__.py b/packages/pyodide-kernel/py/jedi/jedi/api/__init__.py new file mode 100644 index 00000000..3b9d19a3 --- /dev/null +++ b/packages/pyodide-kernel/py/jedi/jedi/api/__init__.py @@ -0,0 +1,2 @@ +# this appeases a type hint +Completion = None diff --git a/packages/pyodide-kernel/py/jedi/jedi/api/classes.py b/packages/pyodide-kernel/py/jedi/jedi/api/classes.py new file mode 100644 index 00000000..983db2c2 --- /dev/null +++ b/packages/pyodide-kernel/py/jedi/jedi/api/classes.py @@ -0,0 +1 @@ +# an import required by ipython diff --git a/packages/pyodide-kernel/py/jedi/jedi/api/helpers.py b/packages/pyodide-kernel/py/jedi/jedi/api/helpers.py new file mode 100644 index 00000000..983db2c2 --- /dev/null +++ b/packages/pyodide-kernel/py/jedi/jedi/api/helpers.py @@ -0,0 +1 @@ +# an import required by ipython diff --git a/packages/pyodide-kernel/py/jedi/pyproject.toml b/packages/pyodide-kernel/py/jedi/pyproject.toml new file mode 100644 index 00000000..1295f05f --- /dev/null +++ b/packages/pyodide-kernel/py/jedi/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["hatchling>=1.11"] +build-backend = "hatchling.build" + +[tool.hatch.version] +path = "jedi/__init__.py" + +[project] +name = "jedi" +authors = [ + {name = "JupyterLite Contributors"}, +] +readme = "README.md" +requires-python = ">=3.10" +dynamic = ["version", "description"] +classifiers = [ + "License :: OSI Approved :: BSD License", +] + +[project.urls] +Source = "https://github.com/jupyterlite/pyodide-kernel" diff --git a/packages/pyodide-kernel/py/jupyterlab_widgets1/jupyterlab_widgets/LICENSE b/packages/pyodide-kernel/py/jupyterlab_widgets1/jupyterlab_widgets/LICENSE new file mode 100644 index 00000000..b7375aa7 --- /dev/null +++ b/packages/pyodide-kernel/py/jupyterlab_widgets1/jupyterlab_widgets/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022, JupyterLite Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/pyodide-kernel/py/jupyterlab_widgets1/jupyterlab_widgets/README.md b/packages/pyodide-kernel/py/jupyterlab_widgets1/jupyterlab_widgets/README.md new file mode 100644 index 00000000..dafe6af0 --- /dev/null +++ b/packages/pyodide-kernel/py/jupyterlab_widgets1/jupyterlab_widgets/README.md @@ -0,0 +1,4 @@ +# jupyterlab_widgets mock + +This is a `jupyterlab_widgets` mock that provides nothing. It's only here so that +`import ipywidgets` doesn't incur the download of the real `jupyterlab_widgets`. diff --git a/packages/pyodide-kernel/py/jupyterlab_widgets1/jupyterlab_widgets/jupyterlab_widgets/__init__.py b/packages/pyodide-kernel/py/jupyterlab_widgets1/jupyterlab_widgets/jupyterlab_widgets/__init__.py new file mode 100644 index 00000000..3d30fbe3 --- /dev/null +++ b/packages/pyodide-kernel/py/jupyterlab_widgets1/jupyterlab_widgets/jupyterlab_widgets/__init__.py @@ -0,0 +1,3 @@ +"""A jupyterlab_widgets mock""" + +__version__ = "1.1.2" diff --git a/packages/pyodide-kernel/py/jupyterlab_widgets1/jupyterlab_widgets/pyproject.toml b/packages/pyodide-kernel/py/jupyterlab_widgets1/jupyterlab_widgets/pyproject.toml new file mode 100644 index 00000000..26e78c1a --- /dev/null +++ b/packages/pyodide-kernel/py/jupyterlab_widgets1/jupyterlab_widgets/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["hatchling>=1.11"] +build-backend = "hatchling.build" + +[tool.hatch.version] +path = "jupyterlab_widgets/__init__.py" + +[project] +name = "jupyterlab_widgets" +authors = [ + {name = "JupyterLite Contributors"}, +] +readme = "README.md" +requires-python = ">=3.10,<3.11" +dynamic = ["version", "description"] +classifiers = [ + "License :: OSI Approved :: BSD License", +] + +[project.urls] +Source = "https://github.com/jupyterlite/pyodide-kernel" diff --git a/packages/pyodide-kernel/py/jupyterlab_widgets3/jupyterlab_widgets/LICENSE b/packages/pyodide-kernel/py/jupyterlab_widgets3/jupyterlab_widgets/LICENSE new file mode 100644 index 00000000..b7375aa7 --- /dev/null +++ b/packages/pyodide-kernel/py/jupyterlab_widgets3/jupyterlab_widgets/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022, JupyterLite Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/pyodide-kernel/py/jupyterlab_widgets3/jupyterlab_widgets/README.md b/packages/pyodide-kernel/py/jupyterlab_widgets3/jupyterlab_widgets/README.md new file mode 100644 index 00000000..337dafa9 --- /dev/null +++ b/packages/pyodide-kernel/py/jupyterlab_widgets3/jupyterlab_widgets/README.md @@ -0,0 +1,4 @@ +# jupyterlab_widgets mock + +This is an `jupyterlab_widgets` mock that provides nothing. It's only here so that +`import ipywidgets` doesn't incur the download of the real `jupyterlab_widgets`. diff --git a/packages/pyodide-kernel/py/jupyterlab_widgets3/jupyterlab_widgets/jupyterlab_widgets/__init__.py b/packages/pyodide-kernel/py/jupyterlab_widgets3/jupyterlab_widgets/jupyterlab_widgets/__init__.py new file mode 100644 index 00000000..7e7aa34e --- /dev/null +++ b/packages/pyodide-kernel/py/jupyterlab_widgets3/jupyterlab_widgets/jupyterlab_widgets/__init__.py @@ -0,0 +1,3 @@ +"""A jupyterlab_widgets mock""" + +__version__ = "3.0.5" diff --git a/packages/pyodide-kernel/py/jupyterlab_widgets3/jupyterlab_widgets/pyproject.toml b/packages/pyodide-kernel/py/jupyterlab_widgets3/jupyterlab_widgets/pyproject.toml new file mode 100644 index 00000000..26e78c1a --- /dev/null +++ b/packages/pyodide-kernel/py/jupyterlab_widgets3/jupyterlab_widgets/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["hatchling>=1.11"] +build-backend = "hatchling.build" + +[tool.hatch.version] +path = "jupyterlab_widgets/__init__.py" + +[project] +name = "jupyterlab_widgets" +authors = [ + {name = "JupyterLite Contributors"}, +] +readme = "README.md" +requires-python = ">=3.10,<3.11" +dynamic = ["version", "description"] +classifiers = [ + "License :: OSI Approved :: BSD License", +] + +[project.urls] +Source = "https://github.com/jupyterlite/pyodide-kernel" diff --git a/packages/pyodide-kernel/py/prompt_toolkit/LICENSE b/packages/pyodide-kernel/py/prompt_toolkit/LICENSE new file mode 100644 index 00000000..b7375aa7 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022, JupyterLite Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/pyodide-kernel/py/prompt_toolkit/README.md b/packages/pyodide-kernel/py/prompt_toolkit/README.md new file mode 100644 index 00000000..6820cc19 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/README.md @@ -0,0 +1,3 @@ +# prompt_toolkit mock + +This is a prompt_toolkit mock to avoid its import expense at kernel startup time. diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/__init__.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/__init__.py new file mode 100644 index 00000000..1a5afc0c --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/__init__.py @@ -0,0 +1,3 @@ +"""A prompt_toolkit mock""" + +__version__ = "3.0.36" diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/application/current.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/application/current.py new file mode 100644 index 00000000..89ec6093 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/application/current.py @@ -0,0 +1,2 @@ +def get_app(*args, **kwargs): + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/auto_suggest.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/auto_suggest.py new file mode 100644 index 00000000..dfb809c1 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/auto_suggest.py @@ -0,0 +1,5 @@ +class AutoSuggestFromHistory: + pass + + +Suggestion = AutoSuggestFromHistory diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/buffer.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/buffer.py new file mode 100644 index 00000000..b9ebc18f --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/buffer.py @@ -0,0 +1,2 @@ +class Buffer: + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/completion.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/completion.py new file mode 100644 index 00000000..652e0160 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/completion.py @@ -0,0 +1,6 @@ +class Completer: + pass + + +class Completion: + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/document.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/document.py new file mode 100644 index 00000000..7b831938 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/document.py @@ -0,0 +1,2 @@ +class Document: + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/enums.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/enums.py new file mode 100644 index 00000000..1d01ce38 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/enums.py @@ -0,0 +1,5 @@ +DEFAULT_BUFFER = SEARCH_BUFFER = EditingMode = None + + +class EditingMode: + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/filters.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/filters.py new file mode 100644 index 00000000..d2116c35 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/filters.py @@ -0,0 +1,31 @@ +class HasFocus: + pass + + +def Condition(*args, **kwargs): + return True + + +Always = IsDone = HasFocus + + +def has_focus(*args, **kwargs): + class Foo: + def __init__(self, *args, **kwargs): + def func(*args, **kwargs): + pass + + setattr(func, "__name__", "") + self.func = func + + def __or__(self, other): + return True + + __and__ = __or__ + + return Foo() + + +has_selection = ( + has_suggestion +) = vi_insert_mode = vi_mode = has_completions = emacs_insert_mode = True diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/formatted_text.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/formatted_text.py new file mode 100644 index 00000000..1c347434 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/formatted_text.py @@ -0,0 +1,6 @@ +class PygmentsTokens: + pass + + +def fragment_list_width(*args, **kwargs): + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/history.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/history.py new file mode 100644 index 00000000..07df9cc8 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/history.py @@ -0,0 +1,5 @@ +class History: + pass + + +InMemoryHistory = FileHistory = History diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/__init__.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/__init__.py new file mode 100644 index 00000000..7e0fd484 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/__init__.py @@ -0,0 +1,5 @@ +class KeyBindings: + pass + + +KeyPressEvent = KeyBindings diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/bindings/__init__.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/bindings/__init__.py new file mode 100644 index 00000000..c1425349 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/bindings/__init__.py @@ -0,0 +1,6 @@ +class __named_commands__: + def __getattr__(self, name): + return name + + +named_commands = __named_commands__() diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/bindings/completion.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/bindings/completion.py new file mode 100644 index 00000000..db2d5fb4 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/bindings/completion.py @@ -0,0 +1,2 @@ +def display_completions_like_readline(*args, **kwargs): + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/key_processor.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/key_processor.py new file mode 100644 index 00000000..13080c72 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/key_processor.py @@ -0,0 +1,2 @@ +class KeyPressEvent: + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/vi_state.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/vi_state.py new file mode 100644 index 00000000..18dc6791 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/key_binding/vi_state.py @@ -0,0 +1,5 @@ +class InputMode: + pass + + +ViState = InputMode diff --git a/jupyterlite_pyodide_kernel/addons/__init__.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/layout/__init__.py similarity index 100% rename from jupyterlite_pyodide_kernel/addons/__init__.py rename to packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/layout/__init__.py diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/layout/layout.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/layout/layout.py new file mode 100644 index 00000000..86e509ac --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/layout/layout.py @@ -0,0 +1,2 @@ +class FocusableElement: + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/layout/processors.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/layout/processors.py new file mode 100644 index 00000000..d7775532 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/layout/processors.py @@ -0,0 +1,7 @@ +class ConditionalProcessor: + pass + + +TransformationInput = ( + Transformation +) = Processor = HighlightMatchingBracketProcessor = ConditionalProcessor diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/lexers.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/lexers.py new file mode 100644 index 00000000..f70d65a4 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/lexers.py @@ -0,0 +1,6 @@ +class Lexer: + pass + + +class PygmentsLexer: + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/output.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/output.py new file mode 100644 index 00000000..682ca1ef --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/output.py @@ -0,0 +1,2 @@ +class ColorDepth: + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/patch_stdout.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/patch_stdout.py new file mode 100644 index 00000000..348ee43c --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/patch_stdout.py @@ -0,0 +1,2 @@ +def patch_stdout(*args, **kwargs): + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/shortcuts/__init__.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/shortcuts/__init__.py new file mode 100644 index 00000000..09c6da2f --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/shortcuts/__init__.py @@ -0,0 +1,9 @@ +class PromptSession: + pass + + +CompleteStyle = PromptSession + + +def print_formatted_text(*args, **kwargs): + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/shortcuts/prompt.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/shortcuts/prompt.py new file mode 100644 index 00000000..63b7808f --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/shortcuts/prompt.py @@ -0,0 +1,2 @@ +class PromptSession: + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/styles/__init__.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/styles/__init__.py new file mode 100644 index 00000000..6cfbf9f6 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/styles/__init__.py @@ -0,0 +1,6 @@ +class DynamicStyle: + pass + + +def merge_styles(*args, **kwargs): + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/styles/pygments.py b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/styles/pygments.py new file mode 100644 index 00000000..dfea1b63 --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/prompt_toolkit/styles/pygments.py @@ -0,0 +1,6 @@ +def style_from_pygments_cls(*args, **kwargs): + pass + + +def style_from_pygments_dict(*args, **kwargs): + pass diff --git a/packages/pyodide-kernel/py/prompt_toolkit/pyproject.toml b/packages/pyodide-kernel/py/prompt_toolkit/pyproject.toml new file mode 100644 index 00000000..508c59dc --- /dev/null +++ b/packages/pyodide-kernel/py/prompt_toolkit/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["hatchling>=1.11"] +build-backend = "hatchling.build" + +[tool.hatch.version] +path = "prompt_toolkit/__init__.py" + +[project] +name = "prompt_toolkit" +authors = [ + {name = "JupyterLite Contributors"}, +] +dependencies = [ + "wcwidth", +] +readme = "README.md" +requires-python = ">=3.10" +dynamic = ["version", "description"] +classifiers = [ + "License :: OSI Approved :: BSD License", +] + +[project.urls] +Source = "https://github.com/jupyterlite/pyodide-kernel" diff --git a/packages/pyodide-kernel/schema/package.v0.schema.json b/packages/pyodide-kernel/schema/package.v0.schema.json new file mode 100644 index 00000000..0e3349e3 --- /dev/null +++ b/packages/pyodide-kernel/schema/package.v0.schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://jupyterlite-pyodide-kernel.readthedocs.org/en/latest/reference/schema/package-v0.html#", + "title": "Pyodide Kernel JS Package properties", + "description": "Pyodide Kernel-specific ``package.json`` configuration.", + "type": "object", + "properties": { + "pyodideKernel": { + "description": "information about packages that contribute to the pyodide kernel", + "type": "object", + "properties": { + "packages": { + "description": "a map of paths to the versions of the wheel they would build", + "$ref": "#/definitions/package-versions" + }, + "wheelDir": { + "description": "a path to a directory containing pyodide-compatible wheels", + "type": "string" + } + } + } + }, + "definitions": { + "package-versions": { + "type": "object", + "patternProperties": { + ".*": { + "description": "a path to a package that can be built with `pyproject-build`", + "$ref": "#/definitions/pep-440-version" + } + } + }, + "pep-440-version": { + "type": "string", + "pattern": "^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\\.post(0|[1-9][0-9]*))?(\\.dev(0|[1-9][0-9]*))?$" + } + } +} diff --git a/packages/pyodide-kernel/schema/repodata.v0.schema.json b/packages/pyodide-kernel/schema/repodata.v0.schema.json new file mode 100644 index 00000000..c34582ba --- /dev/null +++ b/packages/pyodide-kernel/schema/repodata.v0.schema.json @@ -0,0 +1,112 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://jupyterlite-pyodide-kernel.readthedocs.org/en/latest/repodata-schema-v0.html#", + "title": "Pyodide Repodata Schema v0", + "description": "a schema for a partial pyodide repodata index", + "$ref": "#/definitions/top", + "definitions": { + "top": { + "type": "object", + "required": ["packages"], + "properties": { + "info": { + "description": "Pyodide version for which this repodata is valid. Presently not generated, and discarded at runtime.", + "$ref": "#/definitions/repodata-info" + }, + "packages": { + "type": "object", + "patternProperties": { + "[a-z][a-z\\-\\.]+": { + "$ref": "#/definitions/a-project-repodata" + } + } + } + } + }, + "repodata-info": { + "description": "The pyodide version runtime for which this repodata will work", + "properties": { + "arch": { + "type": "string", + "description": "The name of the architecture, e.g. ``wasm32``" + }, + "platform": { + "type": "string", + "description": "The name of the platform, e.g. ``emscripten_3_1_27``" + }, + "python": { + "type": "string", + "description": "The version of python, e.g. ``3.10.2``" + }, + "version": { + "type": "string", + "description": "The version of pyodide, e.g. ``0.22.1``" + } + } + }, + "a-project-repodata": { + "type": "object", + "required": [ + "depends", + "file_name", + "imports", + "install_dir", + "name", + "sha256", + "version" + ], + "properties": { + "depends": { + "type": "array", + "uniqueItems": true, + "description": "A list of PEP 503 normalized package names which this package depends on", + "items": { + "$ref": "#/definitions/a-pep-503-name" + } + }, + "file_name": { + "type": "string", + "format": "uri", + "description": "A relative path or URL that resolves to a ``.whl``, ``.zip`` or ``.tar`` file" + }, + "install_dir": { + "type": "string", + "description": "The destination for the package: currently only generates ``site``", + "enum": ["site", "dynlib", "stdlib"] + }, + "name": { + "description": "The PEP 503 name of the package.", + "$ref": "#/definitions/a-pep-503-name" + }, + "sha256": { + "description": "The observed sha256 hash digest of the package, which might be validated", + "$ref": "#/definitions/a-sha256-digest" + }, + "version": { + "description": "The version of the package", + "type": "string" + }, + "shared_library": { + "type": "boolean" + }, + "imports": { + "description": "The top-level import names which will trigger installation of this package", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + } + } + }, + "a-pep-503-name": { + "description": "A python distribution name that conforms to PEP 503", + "type": "string", + "pattern": "[a-z][a-z0-9\\-\\.]+" + }, + "a-sha256-digest": { + "type": "string", + "pattern": "[a-f0-9]{64}" + } + } +} diff --git a/packages/pyodide-kernel/scripts/generate-wheels-js.py b/packages/pyodide-kernel/scripts/generate-wheels-js.py index 2228a0df..0fcc7956 100644 --- a/packages/pyodide-kernel/scripts/generate-wheels-js.py +++ b/packages/pyodide-kernel/scripts/generate-wheels-js.py @@ -35,7 +35,7 @@ def which(cmd): ) PYODIDE_KERNEL_PACKAGES = { PYODIDE_KERNEL_PACKAGE / py_pkg: version - for py_pkg, version in PYODIDE_KERNEL_PACKAGE_JSON.get("pyodide-kernel", {}) + for py_pkg, version in PYODIDE_KERNEL_PACKAGE_JSON.get("pyodideKernel", {}) .get("packages", {}) .items() } @@ -78,6 +78,7 @@ def generate_pypi_ts(): lines = [ "// this file is autogenerated from the wheels described in ../package.json", "export * as allJSONUrl from '../pypi/all.json';", + "export * as repodataJSONUrl from '../pypi/repodata.json';", ] vars_made = {} diff --git a/packages/pyodide-kernel/src/_pypi.ts b/packages/pyodide-kernel/src/_pypi.ts index 90af0413..a6057f8a 100644 --- a/packages/pyodide-kernel/src/_pypi.ts +++ b/packages/pyodide-kernel/src/_pypi.ts @@ -1,7 +1,12 @@ // this file is autogenerated from the wheels described in ../package.json export * as allJSONUrl from '../pypi/all.json'; export * as ipykernelWheelUrl from '../pypi/ipykernel-6.9.2-py3-none-any.whl'; +export * as jediWheelUrl from '../pypi/jedi-0.18.2-py3-none-any.whl'; +export * as jupyterlab_widgetsWheelUrl from '../pypi/jupyterlab_widgets-1.1.2-py3-none-any.whl'; +export * as jupyterlab_widgetsWheelUrl1 from '../pypi/jupyterlab_widgets-3.0.5-py3-none-any.whl'; export * as pipliteWheelUrl from '../pypi/piplite-0.0.5-py3-none-any.whl'; +export * as prompt_toolkitWheelUrl from '../pypi/prompt_toolkit-3.0.36-py3-none-any.whl'; export * as pyodide_kernelWheelUrl from '../pypi/pyodide_kernel-0.0.5-py3-none-any.whl'; +export * as repodataJSONUrl from '../pypi/repodata.json'; export * as widgetsnbextensionWheelUrl from '../pypi/widgetsnbextension-3.6.0-py3-none-any.whl'; export * as widgetsnbextensionWheelUrl1 from '../pypi/widgetsnbextension-4.0.2-py3-none-any.whl'; diff --git a/packages/pyodide-kernel/src/declarations.d.ts b/packages/pyodide-kernel/src/declarations.d.ts index cb260473..b4054bb7 100644 --- a/packages/pyodide-kernel/src/declarations.d.ts +++ b/packages/pyodide-kernel/src/declarations.d.ts @@ -8,6 +8,11 @@ declare module '*all.json' { export default res; } +declare module '*repodata.json' { + const res: string; + export default res; +} + declare module '../schema/*.json' { const res: string; export default res; diff --git a/packages/pyodide-kernel/src/kernel.ts b/packages/pyodide-kernel/src/kernel.ts index 3ec5c7ab..7590b10a 100644 --- a/packages/pyodide-kernel/src/kernel.ts +++ b/packages/pyodide-kernel/src/kernel.ts @@ -9,7 +9,7 @@ import { wrap } from 'comlink'; import { IPyodideWorkerKernel, IRemotePyodideWorkerKernel } from './tokens'; -import { allJSONUrl, pipliteWheelUrl } from './_pypi'; +import { allJSONUrl, repodataJSONUrl, pipliteWheelUrl } from './_pypi'; /** * A kernel that executes Python code with Pyodide. @@ -54,7 +54,9 @@ export class PyodideKernel extends BaseKernel implements IKernel { const { pyodideUrl } = options; const indexUrl = pyodideUrl.slice(0, pyodideUrl.lastIndexOf('/') + 1); const baseUrl = PageConfig.getBaseUrl(); + const pipliteUrls = [...(options.pipliteUrls || []), allJSONUrl.default]; + const repodataUrls = [...(options.repodataUrls || []), repodataJSONUrl.default]; const disablePyPIFallback = !!options.disablePyPIFallback; @@ -64,6 +66,7 @@ export class PyodideKernel extends BaseKernel implements IKernel { indexUrl, pipliteWheelUrl: pipliteWheelUrl.default, pipliteUrls, + repodataUrls: repodataUrls, disablePyPIFallback, location: this.location, mountDrive: options.mountDrive, @@ -309,6 +312,11 @@ export namespace PyodideKernel { */ pipliteUrls: string[]; + /** + * The URLs with which to patch repodata.json + */ + repodataUrls: string[]; + /** * Do not try pypi.org if `piplite.install` fails against local URLs */ diff --git a/packages/pyodide-kernel/src/tokens.ts b/packages/pyodide-kernel/src/tokens.ts index 13ace73f..1e8c0da0 100644 --- a/packages/pyodide-kernel/src/tokens.ts +++ b/packages/pyodide-kernel/src/tokens.ts @@ -14,6 +14,16 @@ import { IWorkerKernel } from '@jupyterlite/kernel'; */ export * as PIPLITE_INDEX_SCHEMA from '../schema/piplite.v0.schema.json'; +/** + * The schema for a pyodide-compatible repodata. + */ +export * as REPODATA_INDEX_SCHEMA from '../schema/repodata.v0.schema.json'; + +/** + * The schema for a ``package.json` that might impact pyodide-kernel. + */ +export * as PKG_JSON_PYODIDE_KERNEL_SCHEMA from '../schema/package.v0.schema.json'; + /** * An interface for Pyodide workers. */ @@ -57,6 +67,11 @@ export namespace IPyodideWorkerKernel { */ pipliteUrls: string[]; + /** + * The URLs of additional pyodide repodata.json files + */ + repodataUrls: string[]; + /** * Whether `piplite` should fall back to the hard-coded `pypi.org` for resolving packages. */ @@ -72,4 +87,58 @@ export namespace IPyodideWorkerKernel { */ mountDrive: boolean; } + + /** + * Data about a single version of a package. + */ + export interface IRepoDataPackage { + /** + * A list of PEP 503 names for packages. + */ + depends: string[]; + /** + * The relative or full-qualified URL for the package to install. + */ + file_name: string; + /** + * Importable modules which should trigger installation of this package. + */ + imports: string[]; + /** + * The destination for this package: ``dynlib`` is not yet fully understood. + */ + install_dir: 'site' | 'dynlib'; + /** + * The PEP 503 name of the package. + */ + name: string; + /** + * A SHA256 digest for the ``file_name``. + */ + sha256: string; + /** + * The version of this package. + */ + version: string; + } + + export interface IRepoData { + /** + * Metadata about this repodata. + * + * This is not currently used. + */ + info?: { + arch: string; + platform: string; + python: string; + version: string; + }; + /** + * A dictionary of packages, keyed by PEP 503 name. + */ + packages: { + [key: string]: IRepoDataPackage; + }; + } } diff --git a/packages/pyodide-kernel/src/worker.ts b/packages/pyodide-kernel/src/worker.ts index ee7a54b4..4ce510ae 100644 --- a/packages/pyodide-kernel/src/worker.ts +++ b/packages/pyodide-kernel/src/worker.ts @@ -7,7 +7,7 @@ import type { DriveFS } from '@jupyterlite/contents'; import type { IPyodideWorkerKernel } from './tokens'; -export class PyodideRemoteKernel { +export class PyodideRemoteKernel implements IPyodideWorkerKernel { constructor() { this._initialized = new Promise((resolve, reject) => { this._initializer = { resolve, reject }; @@ -53,6 +53,21 @@ export class PyodideRemoteKernel { this._pyodide = await loadPyodide({ indexURL: indexUrl }); } + /** + * Fetch the repodata URL, and resolve all filename URLs + */ + async fetchRepodata(url: string): Promise { + const response = await fetch(url); + const repodata: IPyodideWorkerKernel.IRepoData = await response.json(); + for (const [packageName, packageData] of Object.entries(repodata.packages)) { + if (packageData) { + const file_name = new URL(packageData.file_name, url).toString(); + repodata.packages[packageName] = { ...packageData, file_name }; + } + } + return repodata; + } + protected async initPackageManager( options: IPyodideWorkerKernel.IOptions ): Promise { @@ -60,11 +75,39 @@ export class PyodideRemoteKernel { throw new Error('Uninitialized'); } - const { pipliteWheelUrl, disablePyPIFallback, pipliteUrls } = this._options; + const { pipliteWheelUrl, disablePyPIFallback, pipliteUrls, repodataUrls } = + this._options; + + if (repodataUrls.length) { + const API = (this._pyodide as any)._api; + const repodataPromises: Promise[] = []; + for (const url of repodataUrls) { + repodataPromises.push(this.fetchRepodata(url)); + } + + const repodataResults = await Promise.all(repodataPromises); + + for (const repo of repodataResults) { + API.repodata_packages = { + ...API.repodata_packages, + ...repo.packages, + }; + } + + for (const packageName of Object.keys(API.repodata_packages)) { + const packageData: IPyodideWorkerKernel.IRepoDataPackage = + API.repodata_packages[packageName]; + + for (const importName of packageData.imports) { + API._import_name_to_package_name.set(importName, packageName); + } + } + } + // this is the only use of `loadPackage`, allow `piplite` to handle the rest await this._pyodide.loadPackage(['micropip']); - // get piplite early enough to impact pyodide dependencies + // get piplite early enough to impact Pyodide dependencies await this._pyodide.runPythonAsync(` import micropip await micropip.install('${pipliteWheelUrl}', keep_going=True) @@ -77,12 +120,12 @@ export class PyodideRemoteKernel { protected async initKernel(options: IPyodideWorkerKernel.IOptions): Promise { // from this point forward, only use piplite (but not %pip) await this._pyodide.runPythonAsync(` - await piplite.install(['sqlite3'], keep_going=True); - await piplite.install(['ipykernel'], keep_going=True); + await piplite.install(['sqlite3', 'jedi', 'decorator', 'pygments', 'six', 'ipykernel'], keep_going=True); await piplite.install(['pyodide_kernel'], keep_going=True); await piplite.install(['ipython'], keep_going=True); import pyodide_kernel `); + // cd to the kernel location if (options.mountDrive && this._localPath) { await this._pyodide.runPythonAsync(` @@ -336,7 +379,7 @@ export class PyodideRemoteKernel { return { comms: results, - status: 'ok', + status: 'ok' as any, }; } diff --git a/pyproject.toml b/pyproject.toml index cb1958d7..8638ac18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,8 @@ classifiers = [ ] dependencies = [ "jupyterlite-core >=0.1.0b19,<0.2.0", - "pkginfo" + "packaging", + "pkginfo", ] [project.urls] @@ -65,9 +66,10 @@ lint = [ ] test = [ - "pytest-console-scripts", "pytest-cov", + "pytest-console-scripts", "pytest-html", + "pytest-xdist", "pytest", ] @@ -84,20 +86,17 @@ docs = [ "libarchive-c" ] -[tool.hatch.metadata] -allow-direct-references = true - [tool.hatch.version] -path = "jupyterlite_pyodide_kernel/_version.py" +path = "src/jupyterlite_pyodide_kernel/_version.py" [tool.hatch.build.targets.sdist] -artifacts = ["jupyterlite_pyodide_kernel/labextension"] +artifacts = ["src/jupyterlite_pyodide_kernel/labextension"] exclude = [ ".github", - "/jupyterlite_pyodide_kernel/tests/fixtures/.pyodide" + "/src/jupyterlite_pyodide_kernel/tests/fixtures/.pyodide" ] include = [ - "/jupyterlite_pyodide_kernel", + "/src/jupyterlite_pyodide_kernel", "/package.json", "/install.json", "/ts*.json", @@ -106,33 +105,63 @@ include = [ [tool.hatch.build.targets.wheel] include = [ - "/jupyterlite_pyodide_kernel", + "/src/jupyterlite_pyodide_kernel", ] +[tool.hatch.build.targets.wheel.sources] +"src" = "" + [tool.hatch.build.targets.wheel.shared-data] -"jupyterlite_pyodide_kernel/labextension" = "share/jupyter/labextensions/@jupyterlite/pyodide-kernel-extension" +"src/jupyterlite_pyodide_kernel/labextension" = "share/jupyter/labextensions/@jupyterlite/pyodide-kernel-extension" "install.json" = "share/jupyter/labextensions/@jupyterlite/pyodide-kernel-extension/install.json" [tool.pytest.ini_options] addopts = [ + "-vv", + "--ff", + # only test as-installed package "--pyargs", "jupyterlite_pyodide_kernel", - "-vv", # asyncio "--script-launch-mode=subprocess", - # cov + # parallel + "-n=auto", + # coverage scope "--cov=jupyterlite_pyodide_kernel", - "--cov-report=term-missing:skip-covered", - "--cov-report=html:build/reports/htmlcov", + # coverage options "--cov-branch", - "--cov-fail-under=97", - "--no-cov-on-fail", + "--cov-context=test", + # coverage reporting + "--cov-report=html:build/reports/htmlcov", + "--cov-report=term-missing:skip-covered", + # coverage threshold (local will be slightly be higher) + "--cov-fail-under=92", # html "--html=build/reports/pytest.html", "--self-contained-html", ] -testpaths = [ - "tests/", + +[tool.coverage.run] +data_file = "build/reports/.coverage" +omit = [ + "*/tests/*", +] + +[tool.coverage.html] +show_contexts = true + +[tool.coverage.paths] +jupyterlite_pyodide_kernel = [ + "src/jupyterlite_pyodide_kernel", + "*/src/jupyterlite_pyodide_kernel" +] +omit = [ + "*/tests/*", +] + +[tool.coverage.report] +omit = [ + "*/tests/*", ] [tool.jupyter-releaser.options] @@ -152,3 +181,6 @@ before-build-python = [ [tool.check-wheel-contents] ignore = ["W002"] + +[tool.ruff] +select = ["E", "F", "W", "I"] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..48d3af4e --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +-e ".[dev,lint,test,docs]" +# temporary requirements when more stable upstream are not available +# jupyterlite @ https://jupyterlite.rtfd.io/en/latest/_static/jupyterlite-0.1.0b19-py3-none-any.whl +# jupyterlite-core @ https://jupyterlite.rtfd.io/en/latest/_static/jupyterlite_core-0.1.0b19-py3-none-any.whl diff --git a/scripts/bump-version.py b/scripts/bump-version.py index 9a079ce9..29d00045 100644 --- a/scripts/bump-version.py +++ b/scripts/bump-version.py @@ -3,10 +3,10 @@ import argparse import json -from packaging.version import parse as parse_version from pathlib import Path from subprocess import run +from packaging.version import parse as parse_version ENC = dict(encoding="utf-8") HATCH_VERSION = "hatch version" @@ -44,8 +44,8 @@ def bump(): # bump the js version pyodide_kernel_json = json.loads(PYODIDE_PACKAGE_JSON.read_text(**ENC)) - pyodide_kernel_json["pyodide-kernel"]["packages"]["py/pyodide-kernel"] = py_version - pyodide_kernel_json["pyodide-kernel"]["packages"]["py/piplite"] = py_version + pyodide_kernel_json["pyodideKernel"]["packages"]["py/pyodide-kernel"] = py_version + pyodide_kernel_json["pyodideKernel"]["packages"]["py/piplite"] = py_version PYODIDE_PACKAGE_JSON.write_text(json.dumps(pyodide_kernel_json, indent=2), **ENC) # bump the JS version with lerna diff --git a/jupyterlite_pyodide_kernel/__init__.py b/src/jupyterlite_pyodide_kernel/__init__.py similarity index 100% rename from jupyterlite_pyodide_kernel/__init__.py rename to src/jupyterlite_pyodide_kernel/__init__.py diff --git a/jupyterlite_pyodide_kernel/_version.py b/src/jupyterlite_pyodide_kernel/_version.py similarity index 100% rename from jupyterlite_pyodide_kernel/_version.py rename to src/jupyterlite_pyodide_kernel/_version.py diff --git a/src/jupyterlite_pyodide_kernel/addons/__init__.py b/src/jupyterlite_pyodide_kernel/addons/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/jupyterlite_pyodide_kernel/addons/_base.py b/src/jupyterlite_pyodide_kernel/addons/_base.py new file mode 100644 index 00000000..e336cd8b --- /dev/null +++ b/src/jupyterlite_pyodide_kernel/addons/_base.py @@ -0,0 +1,118 @@ +"""common addon features for ``jupyterlite-pyodide-kernel`` + +This should not be considered part of the public API, and much will disappear +when these features are added upstream: + + https://github.com/jupyterlite/jupyterlite/issues/996 +""" +import json +from hashlib import sha256 +from pathlib import Path +from typing import Any, Dict, List, Optional + +from jupyterlite_core.addons.base import BaseAddon +from jupyterlite_core.constants import LAB_EXTENSIONS, UTF8 + +from ..constants import ( + PKG_JSON_PYODIDE_KERNEL, + PKG_JSON_WHEELDIR, + PYODIDE_KERNEL_NPM_NAME, + PYODIDE_KERNEL_PLUGIN_ID, + PYPI_WHEELS, +) + +__all__ = ["_BaseAddon"] + + +class _BaseAddon(BaseAddon): + @property + def output_extensions(self) -> Path: + """where labextensions will go in the output folder + + Candidate for hoisting to ``jupyterlite_core`` + """ + return self.manager.output_dir / LAB_EXTENSIONS + + def get_output_labextension_packages(self) -> List[Path]: + """All ``package.json`` files for labextensions in ``output_dir``. + + Candidate for hoisting to ``jupyterlite_core`` + """ + return sorted( + [ + *self.output_extensions.glob("*/package.json"), + *self.output_extensions.glob("@*/*/package.json"), + ] + ) + + def check_index_urls(self, raw_urls: List[str], schema: Path): + """Validate URLs against a schema.""" + for raw_url in raw_urls: + if not raw_url.startswith("./"): + continue + + index_url = raw_url.split("?")[0].split("#")[0] + + index_path = self.manager.output_dir / index_url + + if not index_path.exists(): + continue + + yield self.task( + name=f"validate:{index_url}", + doc=f"validate {index_url} against {schema}", + file_dep=[index_path], + actions=[(self.validate_one_json_file, [schema, index_path])], + ) + + @property + def output_kernel_extension(self) -> Path: + """the location of the Pyodide kernel labextension static assets""" + return self.output_extensions / PYODIDE_KERNEL_NPM_NAME + + @property + def schemas(self) -> Path: + """the path to the as-deployed schema in the labextension""" + return self.output_kernel_extension / "static/schema" + + @property + def well_known_wheels(self) -> Path: + return self.manager.lite_dir / PYPI_WHEELS + + @property + def output_wheels(self) -> Path: + """where wheels will go in the output folder""" + return self.manager.output_dir / PYPI_WHEELS + + @property + def wheel_cache(self) -> Path: + """where wheels will go in the cache folder""" + return self.manager.cache_dir / "wheels" + + def get_pyodide_settings(self, config_path: Path): + """Get the settings for the client-side Pyodide kernel.""" + return self.get_lite_plugin_settings(config_path, PYODIDE_KERNEL_PLUGIN_ID) + + def set_pyodide_settings(self, config_path: Path, settings: Dict[str, Any]) -> None: + """Update the settings for the client-side Pyodide kernel.""" + return self.set_lite_plugin_settings( + config_path, PYODIDE_KERNEL_PLUGIN_ID, settings + ) + + def get_index_urls(self, index_path: Path): + """Get output_dir relative URLs for an index file.""" + index_sha256 = sha256(index_path.read_bytes()).hexdigest() + index_url = f"./{index_path.relative_to(self.manager.output_dir).as_posix()}" + index_url_with_sha = f"{index_url}?sha256={index_sha256}" + return index_url, index_url_with_sha + + def get_package_wheel_index_url( + self, pkg_json: Path, index_name: str + ) -> Optional[Path]: + pkg_data = json.loads(pkg_json.read_text(**UTF8)) + wheel_dir = pkg_data.get(PKG_JSON_PYODIDE_KERNEL, {}).get(PKG_JSON_WHEELDIR) + if wheel_dir: + pkg_whl_index = pkg_json.parent / wheel_dir / index_name + if pkg_whl_index.exists(): + return self.get_index_urls(pkg_whl_index)[1] + return None diff --git a/src/jupyterlite_pyodide_kernel/addons/piplite.py b/src/jupyterlite_pyodide_kernel/addons/piplite.py new file mode 100644 index 00000000..527c404b --- /dev/null +++ b/src/jupyterlite_pyodide_kernel/addons/piplite.py @@ -0,0 +1,250 @@ +"""a JupyterLite addon for supporting piplite wheels""" + +import json +import re +import urllib.parse +from pathlib import Path +from typing import List as _List +from typing import Tuple as _Tuple + +import doit.tools +from jupyterlite_core.constants import ( + ALL_JSON, + JSON_FMT, + JUPYTERLITE_JSON, + UTF8, +) +from jupyterlite_core.manager import LiteManager +from jupyterlite_core.trait_types import TypedTuple +from traitlets import Unicode + +from ..constants import ( + KERNEL_SETTINGS_SCHEMA, + PIPLITE_INDEX_SCHEMA, + PIPLITE_URLS, + PYPI_WHEELS, +) +from ..wheel_utils import get_wheel_fileinfo, list_wheels, write_wheel_index +from ._base import _BaseAddon + + +class PipliteAddon(_BaseAddon): + __all__ = ["post_init", "build", "post_build", "check"] + + # CLI + aliases = { + "piplite-wheels": "PipliteAddon.piplite_urls", + } + + # traits + piplite_urls: _Tuple[str] = TypedTuple( + Unicode(), + help="Local paths or URLs of piplite-compatible wheels to copy and index", + ).tag(config=True) + + @property + def piplite_schema(self) -> Path: + """the schema for Warehouse-like API indexes""" + return self.schemas / PIPLITE_INDEX_SCHEMA + + @property + def settings_schema(self) -> Path: + """the schema for the Pyodide kernel labextension""" + return self.schemas / KERNEL_SETTINGS_SCHEMA + + def post_init(self, manager: LiteManager): + """handle downloading of wheels""" + for path_or_url in self.piplite_urls: + yield from self.resolve_one_wheel(path_or_url) + + def build(self, manager: LiteManager): + """yield a doit task to copy each local wheel into the output_dir""" + for wheel in list_wheels(manager.lite_dir / PYPI_WHEELS): + yield from self.resolve_one_wheel(str(wheel.resolve())) + + def post_build(self, manager: LiteManager): + """update the root jupyter-lite.json with user-provided ``pipliteUrls``""" + jupyterlite_json = manager.output_dir / JUPYTERLITE_JSON + whl_metas = [] + + wheels = list_wheels(self.output_wheels) + pkg_jsons = self.get_output_labextension_packages() + + for wheel in wheels: + whl_meta = self.wheel_cache / f"{wheel.name}.meta.json" + whl_metas += [whl_meta] + yield self.task( + name=f"meta:{whl_meta.name}", + doc=f"ensure {wheel} metadata", + file_dep=[wheel], + actions=[ + (doit.tools.create_folder, [whl_meta.parent]), + (self.index_wheel, [wheel, whl_meta]), + ], + targets=[whl_meta], + ) + + if whl_metas or pkg_jsons: + whl_index = self.manager.output_dir / PYPI_WHEELS / ALL_JSON + + yield self.task( + name="patch", + doc=f"ensure {JUPYTERLITE_JSON} includes any piplite wheels", + file_dep=[*whl_metas, jupyterlite_json], + actions=[ + ( + self.patch_jupyterlite_json, + [ + jupyterlite_json, + whl_index, + whl_metas, + pkg_jsons, + ], + ) + ], + targets=[whl_index], + ) + + def check(self, manager: LiteManager): + """verify that all JSON for settings and Warehouse API are valid""" + + for config_path in self.get_output_config_paths(): + yield from self.check_one_config_path(config_path) + + def check_one_config_path(self, config_path: Path): + """verify the settings and Warehouse API for a single jupyter-lite config""" + if not config_path.exists(): + return + + rel_path = config_path.relative_to(self.manager.output_dir) + plugin_config = self.get_pyodide_settings(config_path) + + yield from self.check_index_urls( + plugin_config.get(PIPLITE_URLS, []), self.piplite_schema + ) + + yield self.task( + name=f"validate:settings:{rel_path}", + doc=f"validate {config_path} with the pyodide kernel settings schema", + actions=[ + ( + self.validate_one_json_file, + [self.settings_schema, None, plugin_config], + ), + ], + file_dep=[self.settings_schema, config_path], + ) + + def resolve_one_wheel(self, path_or_url): + """download a single wheel, and copy to the cache""" + local_path = None + will_fetch = False + + if re.findall(r"^https?://", path_or_url): + url = urllib.parse.urlparse(path_or_url) + name = url.path.split("/")[-1] + dest = self.wheel_cache / name + local_path = dest + if not dest.exists(): + yield self.task( + name=f"fetch:{name}", + doc=f"fetch the wheel {name}", + actions=[(self.fetch_one, [path_or_url, dest])], + targets=[dest], + ) + will_fetch = True + else: + local_path = (self.manager.lite_dir / path_or_url).resolve() + + if local_path.is_dir(): + for wheel in list_wheels(local_path): + yield from self.copy_wheel(wheel) + elif local_path.exists() or will_fetch: + suffix = local_path.suffix + + if suffix == ".whl": + yield from self.copy_wheel(local_path) + + else: # pragma: no cover + raise FileNotFoundError(path_or_url) + + def copy_wheel(self, wheel: Path): + """copy one wheel to output""" + dest = self.output_wheels / wheel.name + if dest == wheel: # pragma: no cover + return + yield self.task( + name=f"copy:whl:{wheel.name}", + file_dep=[wheel], + targets=[dest], + actions=[(self.copy_one, [wheel, dest])], + ) + + def patch_jupyterlite_json( + self, + config_path: Path, + whl_index: Path, + whl_metas: _List[Path], + pkg_jsons: _List[Path], + ): + """add the piplite wheels to jupyter-lite.json""" + plugin_config = self.get_pyodide_settings(config_path) + # first add user-specified wheels to warehouse + warehouse_urls = self.update_warehouse_index( + plugin_config, whl_index, whl_metas + ) + needs_save = False + + # ...then add wheels from federated extensions... + for pkg_json in pkg_jsons: + pkg_warehouse_url = self.get_package_wheel_index_url(pkg_json, ALL_JSON) + if pkg_warehouse_url and pkg_warehouse_url not in warehouse_urls: + warehouse_urls += [pkg_warehouse_url] + + # ... and only update if actually changed + if warehouse_urls and plugin_config.get(PIPLITE_URLS) != warehouse_urls: + plugin_config[PIPLITE_URLS] = warehouse_urls + needs_save = True + + if needs_save: + self.set_pyodide_settings(config_path, plugin_config) + + def update_warehouse_index(self, plugin_config, whl_index: Path, whl_metas): + """Ensure the warehouse index is up-to-date, reporting new URLs.""" + old_warehouse_urls = plugin_config.get(PIPLITE_URLS, []) + + if not whl_metas: + return old_warehouse_urls + + new_urls = [] + metadata = {} + for whl_meta in whl_metas: + meta = json.loads(whl_meta.read_text(**UTF8)) + whl = self.output_wheels / whl_meta.name.replace(".json", "") + metadata[whl] = meta["name"], meta["version"], meta["release"] + + write_wheel_index(self.output_wheels, metadata) + whl_index_url, whl_index_url_with_sha = self.get_index_urls(whl_index) + + added_build = False + + for url in old_warehouse_urls: + if url.split("#")[0].split("?")[0] == whl_index_url: + new_urls += [whl_index_url_with_sha] + added_build = True + else: + new_urls += [url] + + if not added_build: + new_urls = [whl_index_url_with_sha, *new_urls] + + return new_urls + + def index_wheel(self, whl_path: Path, whl_meta: Path): + """Generate an intermediate file representation to merge with other releases""" + name, version, release = get_wheel_fileinfo(whl_path) + whl_meta.write_text( + json.dumps(dict(name=name, version=version, release=release), **JSON_FMT), + **UTF8, + ) + self.maybe_timestamp(whl_meta) diff --git a/src/jupyterlite_pyodide_kernel/addons/pyodide.py b/src/jupyterlite_pyodide_kernel/addons/pyodide.py new file mode 100644 index 00000000..6f841179 --- /dev/null +++ b/src/jupyterlite_pyodide_kernel/addons/pyodide.py @@ -0,0 +1,554 @@ +"""a JupyterLite addon for supporting the pyodide distribution""" + +import json +import os +import re +import urllib.parse +from copy import deepcopy +from pathlib import Path +from typing import Any, Dict, List, Optional + +import doit.tools +from jupyterlite_core.constants import ( + JSON_FMT, + JUPYTERLITE_JSON, + UTF8, +) +from jupyterlite_core.manager import LiteManager +from traitlets import Bool, Unicode, default + +from ..constants import ( + ALL_WHEELISH, + PKG_JSON_SCHEMA, + PYODIDE, + PYODIDE_JS, + PYODIDE_REPODATA, + PYODIDE_URL, + PYPI_WHEELS, + REPODATA_JSON, + REPODATA_SCHEMA, + REPODATA_URLS, +) +from ..wheel_utils import get_wheel_repodata, list_wheels, write_repo_index +from ._base import _BaseAddon + + +class PyodideAddon(_BaseAddon): + __all__ = ["pre_status", "status", "post_init", "build", "post_build", "check"] + + # CLI + aliases = { + "pyodide": "PyodideAddon.pyodide_url", + } + + flags = { + "pyodide-install-on-import": ( + {"PyodideAddon": {"install_on_import": True}}, + "Index wheels by import names to install when imported", + ) + } + + # traits + pyodide_url: str = Unicode( + allow_none=True, help="Local path or URL of a pyodide distribution tarball" + ).tag(config=True) + + install_on_import: bool = Bool( + False, help="Index wheels by import names to install when imported" + ).tag(config=True) + + @default("pyodide_url") + def _default_pyodide_url(self): + return os.environ.get("JUPYTERLITE_PYODIDE_URL") + + @property + def pyodide_cache(self): + """where pyodide stuff will go in the cache folder""" + return self.manager.cache_dir / PYODIDE + + @property + def output_pyodide(self): + """where labextensions will go in the output folder""" + return self.manager.output_dir / "static" / PYODIDE + + @property + def well_known_pyodide(self): + """a well-known path where pyodide might be stored""" + return self.manager.lite_dir / "static" / PYODIDE + + @property + def repodata_schema(self) -> Path: + """the schema for pyodide repodata""" + return self.schemas / REPODATA_SCHEMA + + @property + def package_json_schema(self) -> Path: + """the schema for pyodide repodata""" + return self.schemas / PKG_JSON_SCHEMA + + @property + def well_known_repodata(self) -> Path: + return self.well_known_wheels / REPODATA_JSON + + @property + def well_known_repo_packages(self) -> Dict[str, Dict[str, Any]]: + if not self.well_known_repodata.exists(): + return {} + return json.loads(self.well_known_repodata.read_text(**UTF8))["packages"] + + def pre_status(self, manager: LiteManager): + if self.well_known_repodata.exists(): + re_repodata = REPODATA_JSON.replace(".", r"\.") + + self.manager.ignore_contents = [*self.manager.ignore_contents, re_repodata] + yield dict( + name="ignore:contents:repodata", + actions=[lambda: self.log.info("ignoring %s", re_repodata)], + ) + + def status(self, manager: LiteManager): + """report on the status of pyodide""" + yield self.task( + name="pyodide", + actions=[ + lambda: print( + f" URL: {self.pyodide_url}", + ), + lambda: print(f" archive: {[*self.pyodide_cache.glob('*.bz2')]}"), + lambda: print( + f" cache: {len([*self.pyodide_cache.rglob('*')])} files", + ), + lambda: print( + f" local: {len([*self.well_known_pyodide.rglob('*')])} files" + ), + ], + ) + + def post_init(self, manager: LiteManager): + """handle downloading of pyodide""" + if self.pyodide_url is None: + return + + yield from self.cache_pyodide(self.pyodide_url) + + def build(self, manager: LiteManager): + """copy a local (cached or well-known) pyodide into the output_dir""" + cached_pyodide = self.pyodide_cache / PYODIDE / PYODIDE + + the_pyodide = None + + if self.well_known_pyodide.exists(): + the_pyodide = self.well_known_pyodide + elif self.pyodide_url is not None: + the_pyodide = cached_pyodide + + if the_pyodide: + file_dep = [ + p + for p in the_pyodide.rglob("*") + if not (p.is_dir() or self.is_ignored_sourcemap(p.name)) + ] + + yield self.task( + name="copy:pyodide", + file_dep=file_dep, + targets=[ + self.output_pyodide / p.relative_to(the_pyodide) for p in file_dep + ], + actions=[(self.copy_one, [the_pyodide, self.output_pyodide])], + ) + + for package_name, package_info in self.well_known_repo_packages.items(): + yield from self.cache_one_repo_package(package_name, package_info) + + def post_build(self, manager): + """configure jupyter-lite.json for pyodide""" + repo_index: Optional[Path] = None + jupyterlite_json = manager.output_dir / JUPYTERLITE_JSON + file_dep: List[Path] = [] + whl_repos: List[Path] = [] + targets: list[Path] = [] + + if self.install_on_import: + wheels = list_wheels(self.output_wheels) + + # find normal wheels + for wheel in wheels: + whl_repo = self.wheel_cache / f"{wheel.name}.repodata.json" + whl_repos += [whl_repo] + yield from self.build_one_wheel(wheel, whl_repo) + + # find stdlib/dynlib + for package_info in self.well_known_repo_packages.values(): + file_name = str(package_info["file_name"]) + url_info = urllib.parse.urlparse(file_name) + non_wheel = self.output_wheels / Path(url_info.path).name + non_whl_repo = self.wheel_cache / f"{non_wheel.name}.repodata.json" + whl_repos += [non_whl_repo] + yield from self.build_one_non_wheel( + package_info, non_wheel, non_whl_repo + ) + + if whl_repos: + repo_index = self.manager.output_dir / PYPI_WHEELS / REPODATA_JSON + targets += [repo_index] + + file_dep += whl_repos + + output_js = None + + if self.well_known_pyodide.exists() or self.pyodide_url: + output_js = self.output_pyodide / PYODIDE_JS + file_dep += [output_js] + + if whl_repos or output_js: + yield self.task( + name=f"patch:{JUPYTERLITE_JSON}", + doc=( + f"ensure {JUPYTERLITE_JSON} includes pyodide.js URL " + "and maybe pyodide repodata" + ), + file_dep=file_dep, + actions=[ + ( + self.patch_jupyterlite_json, + [jupyterlite_json, output_js, whl_repos, repo_index], + ) + ], + targets=targets, + ) + + def build_one_wheel(self, wheel: Path, whl_repo: Path): + yield self.task( + name=f"meta:wheel:{whl_repo.name}", + doc=f"ensure {wheel} repodata", + file_dep=[wheel], + actions=[ + (doit.tools.create_folder, [whl_repo.parent]), + (self.repodata_wheel, [wheel, whl_repo]), + ], + targets=[whl_repo], + ) + + def build_one_non_wheel( + self, package_info: Dict[str, Any], non_wheel: Path, non_whl_repo: Path + ): + package_info = deepcopy(package_info) + package_info["file_name"] = non_wheel.name + + yield self.task( + name=f"meta:nonwheel:{non_whl_repo.name}", + doc=f"ensure {non_wheel} repodata", + file_dep=[self.well_known_repodata, non_wheel], + actions=[ + (doit.tools.create_folder, [non_whl_repo.parent]), + lambda: json.dump(package_info, non_whl_repo.open("w"), **JSON_FMT), + ], + targets=[non_whl_repo], + ) + + def patch_jupyterlite_json( + self, + config_path: Path, + output_js: Optional[Path], + whl_repos: List[Path], + repo_index: Optional[Path], + ): + """update ``jupyter-lite.json`` to use the local pyodide and wheels""" + plugin_config = self.get_pyodide_settings(config_path) + repodata_urls = [] + needs_save = False + + # ...then maybe add repodata + if self.install_on_import: + repodata_urls = self.update_repo_index(plugin_config, repo_index, whl_repos) + + # ...then add wheels from federated extensions... + for pkg_json in self.get_output_labextension_packages(): + pkg_repodata_url = self.get_package_wheel_index_url( + pkg_json, REPODATA_JSON + ) + if pkg_repodata_url and pkg_repodata_url not in repodata_urls: + repodata_urls += [pkg_repodata_url] + + if output_js: + url = "./{}".format( + output_js.relative_to(self.manager.output_dir).as_posix() + ) + if plugin_config.get(PYODIDE_URL) != url: + needs_save = True + plugin_config[PYODIDE_URL] = url + elif plugin_config.get(PYODIDE_URL): + plugin_config.pop(PYODIDE_URL) + needs_save = True + + if self.install_on_import: + if repodata_urls and plugin_config.get(REPODATA_URLS) != repodata_urls: + plugin_config[REPODATA_URLS] = repodata_urls + needs_save = True + + if needs_save: + self.set_pyodide_settings(config_path, plugin_config) + + def check(self, manager: LiteManager): + """ensure the pyodide configuration is sound""" + for config_path in self.get_output_config_paths(): + yield from self.check_one_config_path(config_path) + + for pkg_json in self.get_output_labextension_packages(): + yield from self.check_one_package_json(pkg_json) + + if self.install_on_import and self.pyodide_url: + yield from self.check_repodata_totality() + + def check_one_config_path(self, config_path: Path): + """verify the JS and repodata for a single jupyter-lite config""" + if not config_path.exists(): + return + + rel_path = config_path.relative_to(self.manager.output_dir) + plugin_config = self.get_pyodide_settings(config_path) + + yield self.task( + name=f"js:{rel_path}", + file_dep=[config_path], + actions=[(self.check_pyodide_js_files, [config_path])], + ) + + if self.install_on_import: + repo_urls = plugin_config.get(REPODATA_URLS, []) + if repo_urls: + yield from self.check_index_urls(repo_urls, self.repodata_schema) + + def check_pyodide_js_files(self, config_path: Path): + """Ensure any local pyodide JS assets exist""" + config = self.get_pyodide_settings(config_path) + + pyodide_url = config.get(PYODIDE_URL) + + if not pyodide_url or not pyodide_url.startswith("./"): + return + + pyodide_path = Path(self.manager.output_dir / pyodide_url).parent + assert pyodide_path.exists(), f"{pyodide_path} not found" + pyodide_js = pyodide_path / PYODIDE_JS + assert pyodide_js.exists(), f"{pyodide_js} not found" + pyodide_repodata = pyodide_path / PYODIDE_REPODATA + assert pyodide_repodata.exists(), f"{pyodide_repodata} not found" + + def check_one_package_json(self, pkg_json: Path): + """validate ``pyodideKernel`` settings in a labextension's ``package.json``""" + if not pkg_json.exists(): # pragma: no cover + return + + rel_path = pkg_json.parent.relative_to(self.manager.output_dir) + + yield self.task( + name=f"validate:package:{rel_path}", + doc=f"validate pyodideKernel data in {rel_path}", + actions=[ + ( + self.validate_one_json_file, + [self.package_json_schema, pkg_json], + ), + ], + file_dep=[self.package_json_schema, pkg_json], + ) + + def check_repodata_totality(self): + """check whether the union of all repodata provides all dependencies.""" + config_paths = sorted(self.get_output_config_paths()) + yield dict( + name="repo:totality", + doc="check if the union of all repodata is complete", + file_dep=config_paths, + actions=[(self.check_totality, [config_paths])], + ) + + def check_totality(self, config_paths: List[Path]): + local_repo_packages: Dict[str, Any] = {} + for config_path in config_paths: + plugin_config = self.get_pyodide_settings(config_path) + for repo_url in plugin_config.get(REPODATA_URLS, []): + url = urllib.parse.urlparse(str(repo_url)) + if url.scheme: + self.log.warning( + "non-local repodata %s in %s will not be checked", + url, + config_path.relative_to(self.manager.output_dir), + ) + continue + repo_path = (config_path.parent / url.path).resolve() + repo_packages = json.loads(repo_path.read_text(**UTF8))["packages"] + local_repo_packages[repo_path.parent] = repo_packages + + resolved = {} + for repo, packages in local_repo_packages.items(): + for package_name, package_info in packages.items(): + file_name = str(package_info["file_name"]) + file_url = urllib.parse.urlparse(file_name) + if file_url.scheme: + resolved[package_name] = "remote" + else: + if (repo / file_url.path).exists(): + resolved[package_name] = "local" + + missing_deps = {} + for repo, packages in local_repo_packages.items(): + for package_name, package_info in packages.items(): + for dep in package_info.get("depends", []): + if dep not in resolved: + missing_deps.setdefault(package_name, []).append(dep) + + if missing_deps: + print(json.dumps(missing_deps, **JSON_FMT), flush=True) + message = ( + "Repodata is not self-contained:" + f"""Dependencies missing for: {sorted(missing_deps)}""" + ) + raise ValueError(message) + + def cache_pyodide(self, path_or_url): + """copy pyodide to the cache""" + if re.findall(r"^https?://", path_or_url): + url = urllib.parse.urlparse(path_or_url) + name = url.path.split("/")[-1] + dest = self.pyodide_cache / name + local_path = dest + if not dest.exists(): + yield self.task( + name=f"fetch:{name}", + doc=f"fetch the pyodide distribution {name}", + actions=[(self.fetch_one, [path_or_url, dest])], + targets=[dest], + ) + will_fetch = True + else: + local_path = (self.manager.lite_dir / path_or_url).resolve() + dest = self.pyodide_cache / local_path.name + will_fetch = False + + if local_path.is_dir(): + all_paths = sorted([p for p in local_path.rglob("*") if not p.is_dir()]) + yield self.task( + name=f"copy:pyodide:{local_path.name}", + file_dep=[*all_paths], + targets=[dest / p.relative_to(local_path) for p in all_paths], + actions=[(self.copy_one, [local_path, dest])], + ) + + elif local_path.exists() or will_fetch: + suffix = local_path.suffix + extracted = self.pyodide_cache / PYODIDE + + if suffix == ".bz2": + yield from self.extract_pyodide(local_path, extracted) + + else: # pragma: no cover + raise FileNotFoundError(path_or_url) + + def extract_pyodide(self, local_path, dest): + """extract a local pyodide tarball to the cache""" + + yield self.task( + name="extract:pyodide", + file_dep=[local_path], + uptodate=[ + doit.tools.config_changed( + dict(no_sourcemaps=self.manager.no_sourcemaps) + ) + ], + targets=[ + # there are a lot of js/data files, but we actually talk about these... + dest / PYODIDE / PYODIDE_JS, + dest / PYODIDE / PYODIDE_REPODATA, + ], + actions=[(self.extract_one, [local_path, dest])], + ) + + def update_repo_index(self, plugin_config, repo_index: Path, whl_repos: List[Path]): + """Ensure the repodata index is up-to-date, reporting new URLs.""" + old_urls = plugin_config.get(REPODATA_URLS, []) + + if not whl_repos: + return old_urls + + new_urls = [] + metadata = {} + for whl_repo in whl_repos: + meta = json.loads(whl_repo.read_text(**UTF8)) + whl = self.output_wheels / meta["file_name"] + metadata[whl] = meta["name"], meta["version"], meta + + extensions = None + if self.install_on_import: + extensions = ALL_WHEELISH + write_repo_index(self.output_wheels, metadata, extensions) + repo_index_url, repo_index_url_with_sha = self.get_index_urls(repo_index) + + added_build = False + + for url in old_urls: + if url.split("#")[0].split("?")[0] == repo_index_url: + new_urls += [repo_index_url_with_sha] + added_build = True + else: + new_urls += [url] + + if not added_build: + new_urls = [repo_index_url_with_sha, *new_urls] + + return new_urls + + def repodata_wheel(self, whl_path: Path, whl_repo: Path) -> None: + """Write out the repodata for a wheel.""" + pkg_entry = get_wheel_repodata(whl_path)[2] + whl_repo.write_text( + json.dumps(pkg_entry, **JSON_FMT), + **UTF8, + ) + self.maybe_timestamp(whl_repo) + + def cache_one_repo_package(self, package_name, package_info): + """ensure a local cache of a repodata package""" + file_name = str(package_info["file_name"]) + url_info = urllib.parse.urlparse(file_name) + scheme = url_info.scheme + cache_path: Optional[Path] = None + + if not scheme: + on_disk = (self.well_known_wheels / file_name).resolve() + if on_disk.exists(): + name = on_disk.name + cache_path = self.wheel_cache / name + yield self.task( + name=f"repo:fetch:{package_name}:{name}", + actions=[(self.copy_one, [on_disk, cache_path])], + file_dep=[on_disk], + targets=[cache_path], + ) + + elif scheme in ["http", "https", "file"]: + url = file_name + name = Path(url_info.path).name + cache_path = self.wheel_cache / name + yield self.task( + name=f"repo:fetch:{package_name}:{name}", + actions=[ + (self.fetch_one, [url, cache_path]), + ], + targets=[cache_path], + ) + + if cache_path is None: + raise ValueError( + f"Don't know what to do with {package_name}: {package_info}" + ) + + dest = self.output_wheels / cache_path.name + yield self.task( + name=f"repo:copy:{dest.name}", + actions=[(self.copy_one, [cache_path, dest])], + file_dep=[cache_path], + targets=[dest], + ) diff --git a/jupyterlite_pyodide_kernel/app.py b/src/jupyterlite_pyodide_kernel/app.py similarity index 91% rename from jupyterlite_pyodide_kernel/app.py rename to src/jupyterlite_pyodide_kernel/app.py index 618e4bfe..cb999fff 100644 --- a/jupyterlite_pyodide_kernel/app.py +++ b/src/jupyterlite_pyodide_kernel/app.py @@ -6,7 +6,7 @@ from jupyterlite_core.trait_types import CPath from ._version import __version__ -from .addons.piplite import list_wheels +from .wheel_utils import list_wheels, write_repo_index, write_wheel_index class PipliteIndex(DescribedMixin, JupyterApp): @@ -22,7 +22,7 @@ class PipliteIndex(DescribedMixin, JupyterApp): "jupyterlab": { "extension": true }, - "piplite": { + "jupyterlite-pyodide-kernel": { "wheelDir": "./pypi" } } @@ -43,9 +43,9 @@ def start(self): raise ValueError(f"{self.wheel_dir} does not exist") if not list_wheels(self.wheel_dir): raise ValueError(f"no supported wheels found in {self.wheel_dir}") - from .addons.piplite import write_wheel_index write_wheel_index(self.wheel_dir) + write_repo_index(self.wheel_dir) class PipliteApp(DescribedMixin, JupyterApp): diff --git a/jupyterlite_pyodide_kernel/constants.py b/src/jupyterlite_pyodide_kernel/constants.py similarity index 51% rename from jupyterlite_pyodide_kernel/constants.py rename to src/jupyterlite_pyodide_kernel/constants.py index 2e3b8109..08ce3cad 100644 --- a/jupyterlite_pyodide_kernel/constants.py +++ b/src/jupyterlite_pyodide_kernel/constants.py @@ -1,4 +1,5 @@ """Well-known (and otherwise) constants used by ``jupyterlite-pyodide-kernel``""" +from typing import Dict, List ### pyodide-specific values #: the key for PyPI-compatible API responses pointing to wheels @@ -7,6 +8,8 @@ #: the schema for piplite-compatible wheel index PIPLITE_INDEX_SCHEMA = "piplite.v0.schema.json" #: the schema for piplite-compatible wheel index +REPODATA_SCHEMA = "repodata.v0.schema.json" +#: the schema for piplite-compatible wheel index KERNEL_SETTINGS_SCHEMA = "kernel.v0.schema.json" #: where we put wheels, for now PYPI_WHEELS = "pypi" @@ -14,14 +17,49 @@ PYODIDE_KERNEL_PLUGIN_ID = "@jupyterlite/pyodide-kernel-extension:kernel" #: the npm name of the pyodide kernel PYODIDE_KERNEL_NPM_NAME = PYODIDE_KERNEL_PLUGIN_ID.split(":")[0] -#: the package.json key for piplite -PKG_JSON_PIPLITE = "piplite" +#: the package.json key for pyodide-kernel metadata +PKG_JSON_PYODIDE_KERNEL = "pyodideKernel" #: the package.json/piplite key for wheels PKG_JSON_WHEELDIR = "wheelDir" +#: the schema for a pyodide-kernel-compatible ``package.json`` +PKG_JSON_SCHEMA = "package.v0.schema.json" #: where we put wheels, for now PYODIDE_URL = "pyodideUrl" +#: the key for pyodide-compatible repodata.json +REPODATA_URLS = "repodataUrls" + +#: where setuptools wheels store their exported modules +TOP_LEVEL_TXT = "top_level.txt" + +#: where all wheels store a list of all exported files +WHL_RECORD = "RECORD" + +#: the pyodide index of wheels +REPODATA_JSON = "repodata.json" + +#: extra known dependencies +REPODATA_EXTRA_DEPENDS = {"ipython": ["sqlite3"]} + +#: the observed default environment of pyodide +PYODIDE_MARKER_ENV = { + "implementation_name": "cpython", + "implementation_version": "3.10.2", + "os_name": "posix", + "platform_machine": "wasm32", + "platform_release": "3.1.27", + "platform_system": "Emscripten", + "platform_version": "#1", + "python_full_version": "3.10.2", + "platform_python_implementation": "CPython", + "python_version": "3.10", + "sys_platform": "emscripten", +} + +TDistPackages = Dict[str, List[str]] + + #: where we put pyodide, for now PYODIDE = "pyodide" PYODIDE_JS = "pyodide.js" @@ -38,3 +76,5 @@ WASM_WHL = "emscripten_*_wasm32.whl" ALL_WHL = [NOARCH_WHL, WASM_WHL] + +ALL_WHEELISH = [*ALL_WHL, "zip", "tar"] diff --git a/jupyterlite_pyodide_kernel/tests/__init__.py b/src/jupyterlite_pyodide_kernel/tests/__init__.py similarity index 100% rename from jupyterlite_pyodide_kernel/tests/__init__.py rename to src/jupyterlite_pyodide_kernel/tests/__init__.py diff --git a/jupyterlite_pyodide_kernel/tests/conftest.py b/src/jupyterlite_pyodide_kernel/tests/conftest.py similarity index 99% rename from jupyterlite_pyodide_kernel/tests/conftest.py rename to src/jupyterlite_pyodide_kernel/tests/conftest.py index e30ac958..caebe5e8 100644 --- a/jupyterlite_pyodide_kernel/tests/conftest.py +++ b/src/jupyterlite_pyodide_kernel/tests/conftest.py @@ -1,6 +1,7 @@ """test configuration for jupyterlite-pyodide-kernel""" -from pathlib import Path import sys +from pathlib import Path + import pytest from jupyterlite_core.tests.conftest import ( a_fixture_server, @@ -9,8 +10,8 @@ ) from jupyterlite_pyodide_kernel.constants import ( - PYODIDE_VERSION, PYODIDE_KERNEL_NPM_NAME, + PYODIDE_VERSION, ) __all__ = [ diff --git a/jupyterlite_pyodide_kernel/tests/fixtures/the_smallest_extension-0.1.0-py3-none-any.whl b/src/jupyterlite_pyodide_kernel/tests/fixtures/the_smallest_extension-0.1.0-py3-none-any.whl similarity index 100% rename from jupyterlite_pyodide_kernel/tests/fixtures/the_smallest_extension-0.1.0-py3-none-any.whl rename to src/jupyterlite_pyodide_kernel/tests/fixtures/the_smallest_extension-0.1.0-py3-none-any.whl diff --git a/src/jupyterlite_pyodide_kernel/tests/test_examples.py b/src/jupyterlite_pyodide_kernel/tests/test_examples.py new file mode 100644 index 00000000..5c29d2c5 --- /dev/null +++ b/src/jupyterlite_pyodide_kernel/tests/test_examples.py @@ -0,0 +1,63 @@ +"""Use the demo site for a more extensive test.""" +import json +import os +import shutil +from pathlib import Path + +import pytest +from jupyterlite_core.constants import ALL_JSON, UTF8 + +from jupyterlite_pyodide_kernel.constants import PYPI_WHEELS + +from .conftest import HERE + +IN_TREE_EXAMPLES = HERE / "../../../examples" +EXAMPLES = Path(os.environ.get("LITE_PYODIDE_KERNEL_DEMO", IN_TREE_EXAMPLES)) + +if not EXAMPLES.exists(): # pragma: no cover + pytest.skip( + "not in a source checkout, skipping example test", allow_module_level=True + ) + + +@pytest.fixture +def an_example_with_tarball(tmp_path, a_pyodide_tarball): + examples = tmp_path / EXAMPLES.name + shutil.copytree(EXAMPLES, examples) + config_path = examples / "jupyter_lite_config.json" + config = json.loads(config_path.read_text(**UTF8)) + config["PyodideAddon"]["pyodide_url"] = str(a_pyodide_tarball) + config_path.write_text(json.dumps(config)) + return examples + + +def test_examples_good(script_runner, an_example_with_tarball): + """verify the demo site builds (if it available)""" + opts = dict(cwd=str(an_example_with_tarball)) + + build = script_runner.run("jupyter", "lite", "build", **opts) + assert build.success + + output = an_example_with_tarball.parent / "build/docs-app" + assert (output / f"api/contents/{ALL_JSON}").exists() + assert (output / "files/intro.ipynb").exists() + assert not (output / f"files/{PYPI_WHEELS}").exists() + + archive = script_runner.run("jupyter", "lite", "archive", **opts) + assert archive.success + + check = script_runner.run("jupyter", "lite", "check", **opts) + assert check.success + + +def test_examples_bad_missing(script_runner, an_example_with_tarball): + """verify the demo site check fails for missing deps""" + opts = dict(cwd=str(an_example_with_tarball)) + + shutil.rmtree(an_example_with_tarball / PYPI_WHEELS) + + check = script_runner.run("jupyter", "lite", "check", **opts) + assert not check.success + all_out = f"{check.stderr}{check.stdout}" + assert "ipython" in all_out, "didn't find the missing dependent" + assert "sqlite3" in all_out, "didn't find the missing dependency" diff --git a/jupyterlite_pyodide_kernel/tests/test_metadata.py b/src/jupyterlite_pyodide_kernel/tests/test_metadata.py similarity index 76% rename from jupyterlite_pyodide_kernel/tests/test_metadata.py rename to src/jupyterlite_pyodide_kernel/tests/test_metadata.py index 1cf912c3..cd9e2bca 100644 --- a/jupyterlite_pyodide_kernel/tests/test_metadata.py +++ b/src/jupyterlite_pyodide_kernel/tests/test_metadata.py @@ -1,3 +1,4 @@ +"""verify the labextension metadata is correct""" import jupyterlite_pyodide_kernel diff --git a/jupyterlite_pyodide_kernel/tests/test_piplite.py b/src/jupyterlite_pyodide_kernel/tests/test_piplite.py similarity index 79% rename from jupyterlite_pyodide_kernel/tests/test_piplite.py rename to src/jupyterlite_pyodide_kernel/tests/test_piplite.py index ec9918f9..85605edc 100644 --- a/jupyterlite_pyodide_kernel/tests/test_piplite.py +++ b/src/jupyterlite_pyodide_kernel/tests/test_piplite.py @@ -3,28 +3,33 @@ import shutil import pytest -from pytest import mark - from jupyterlite_core.constants import ( + ALL_JSON, + JSON_FMT, + JUPYTER_CONFIG_DATA, JUPYTERLITE_IPYNB, JUPYTERLITE_JSON, - UTF8, JUPYTERLITE_METADATA, LITE_PLUGIN_SETTINGS, - JSON_FMT, - JUPYTER_CONFIG_DATA, + UTF8, ) +from pytest import mark from jupyterlite_pyodide_kernel.constants import ( - PYODIDE_KERNEL_PLUGIN_ID, DISABLE_PYPI_FALLBACK, + PIPLITE_URLS, + PYODIDE_KERNEL_PLUGIN_ID, + PYPI_WHEELS, + REPODATA_JSON, ) -from .conftest import WHEELS, PYODIDE_KERNEL_EXTENSION +from .conftest import PYODIDE_KERNEL_EXTENSION, WHEELS -def has_wheel_after_build(an_empty_lite_dir, script_runner): - """run a build, expecting the fixture wheel to be there""" +def has_wheel_after_build(an_empty_lite_dir, script_runner, install_on_import=False): + """run a build, expecting the fixture wheel, ``all.json`` (and maybe + ``repodata.json``) to be there + """ build = script_runner.run("jupyter", "lite", "build", cwd=str(an_empty_lite_dir)) assert build.success @@ -33,25 +38,37 @@ def has_wheel_after_build(an_empty_lite_dir, script_runner): output = an_empty_lite_dir / "_output" - lite_json = output / "jupyter-lite.json" + lite_json = output / JUPYTERLITE_JSON lite_data = json.loads(lite_json.read_text(encoding="utf-8")) - assert lite_data["jupyter-config-data"]["litePluginSettings"][ + assert lite_data[JUPYTER_CONFIG_DATA][LITE_PLUGIN_SETTINGS][ PYODIDE_KERNEL_PLUGIN_ID - ]["pipliteUrls"], "bad wheel urls" + ][PIPLITE_URLS], "bad wheel urls" - wheel_out = output / "pypi" + wheel_out = output / PYPI_WHEELS assert (wheel_out / WHEELS[0].name).exists() - wheel_index = output / "pypi/all.json" + wheel_index = output / PYPI_WHEELS / ALL_JSON wheel_index_text = wheel_index.read_text(encoding="utf-8") assert WHEELS[0].name in wheel_index_text, wheel_index_text + repodata = output / PYPI_WHEELS / REPODATA_JSON + if install_on_import: + assert repodata.exists() + else: + assert not repodata.exists() + @mark.parametrize( "remote,folder", [[True, False], [False, False], [False, True]], ) +@mark.parametrize("install_on_import", [True, False]) def test_piplite_urls( - an_empty_lite_dir, script_runner, remote, folder, a_fixture_server + an_empty_lite_dir, + script_runner, + remote, + folder, + a_fixture_server, + install_on_import, ): """can we include a single wheel?""" ext = WHEELS[0] @@ -78,15 +95,18 @@ def test_piplite_urls( "PipliteAddon": { "piplite_urls": piplite_urls, }, + "PyodideAddon": { + "install_on_import": install_on_import, + }, } (an_empty_lite_dir / "jupyter_lite_config.json").write_text(json.dumps(config)) - has_wheel_after_build(an_empty_lite_dir, script_runner) + has_wheel_after_build(an_empty_lite_dir, script_runner, install_on_import) def test_lite_dir_wheel(an_empty_lite_dir, script_runner): - wheel_dir = an_empty_lite_dir / "pypi" + wheel_dir = an_empty_lite_dir / PYPI_WHEELS wheel_dir.mkdir() shutil.copy2(WHEELS[0], wheel_dir / WHEELS[0].name) @@ -115,7 +135,7 @@ def test_piplite_cli_win(script_runner, tmp_path, index_cmd, in_cwd): pargs = [] if in_cwd else [str(path)] build = script_runner.run(*index_cmd, *pargs, **kwargs) assert build.success - assert json.loads((path / "all.json").read_text(encoding="utf-8")) + assert json.loads((path / ALL_JSON).read_text(encoding="utf-8")) @pytest.fixture(params=[JUPYTERLITE_IPYNB, JUPYTERLITE_JSON]) diff --git a/jupyterlite_pyodide_kernel/tests/test_pyodide.py b/src/jupyterlite_pyodide_kernel/tests/test_pyodide.py similarity index 100% rename from jupyterlite_pyodide_kernel/tests/test_pyodide.py rename to src/jupyterlite_pyodide_kernel/tests/test_pyodide.py diff --git a/src/jupyterlite_pyodide_kernel/tests/test_repo.py b/src/jupyterlite_pyodide_kernel/tests/test_repo.py new file mode 100644 index 00000000..d7d7d26e --- /dev/null +++ b/src/jupyterlite_pyodide_kernel/tests/test_repo.py @@ -0,0 +1,42 @@ +"""Validate the integrity of the repo (or source checkout)""" +import json +from pathlib import Path + +import pytest +from jupyterlite_core.constants import UTF8 + +from jupyterlite_pyodide_kernel.constants import PYODIDE_VERSION + +from .conftest import HERE + +PACKAGES = HERE / "../../../packages" +KERNEL_PKG = PACKAGES / "pyodide-kernel" +KERNEL_PKG_JSON = KERNEL_PKG / "package.json" + +if not KERNEL_PKG_JSON.exists(): # pragma: no cover + pytest.skip( + "not in a source checkout, skipping repo tests", allow_module_level=True + ) + + +def test_pyodide_version(): + kernel_pkg_data = json.loads(KERNEL_PKG_JSON.read_text(**UTF8)) + assert ( + kernel_pkg_data["devDependencies"]["pyodide"] == PYODIDE_VERSION + ), f"{kernel_pkg_data} pyodide devDependency is not {PYODIDE_VERSION}" + + +@pytest.fixture +def the_default_pyodide_url(): + return f"https://cdn.jsdelivr.net/pyodide/v{PYODIDE_VERSION}/full/pyodide.js" + + +@pytest.mark.parametrize( + "pkg_path", + [ + "pyodide-kernel-extension/schema/kernel.v0.schema.json", + "pyodide-kernel-extension/src/index.ts", + ], +) +def test_pyodide_url(pkg_path: Path, the_default_pyodide_url: str): + assert the_default_pyodide_url in (PACKAGES / pkg_path).read_text(**UTF8) diff --git a/src/jupyterlite_pyodide_kernel/wheel_utils.py b/src/jupyterlite_pyodide_kernel/wheel_utils.py new file mode 100644 index 00000000..6dd1b183 --- /dev/null +++ b/src/jupyterlite_pyodide_kernel/wheel_utils.py @@ -0,0 +1,237 @@ +"""Utilties for working with wheels and package metadata.""" +import datetime +import functools +import json +import re +import warnings +import zipfile +from hashlib import md5, sha256 +from pathlib import Path +from typing import List, Optional + +from jupyterlite_core.constants import ( + ALL_JSON, + JSON_FMT, + UTF8, +) + +from .constants import ( + ALL_WHL, + NOARCH_WHL, + PYODIDE_MARKER_ENV, + REPODATA_EXTRA_DEPENDS, + REPODATA_JSON, + TOP_LEVEL_TXT, + WHL_RECORD, +) + + +def list_wheels(wheel_dir: Path, extensions: Optional[List[str]] = None) -> List[Path]: + """Get all files we know how to handle in a directory""" + extensions = extensions or ALL_WHL + wheelish = sum([[*wheel_dir.glob(f"*{ext}")] for ext in extensions], []) + return sorted(wheelish) + + +def get_wheel_fileinfo(whl_path: Path): + """Generate a minimal Warehouse-like JSON API entry from a wheel""" + metadata = get_wheel_pkginfo(whl_path) + whl_stat = whl_path.stat() + whl_isodate = ( + datetime.datetime.fromtimestamp(whl_stat.st_mtime, tz=datetime.timezone.utc) + .isoformat() + .split("+")[0] + + "Z" + ) + whl_bytes = whl_path.read_bytes() + whl_sha256 = sha256(whl_bytes).hexdigest() + whl_md5 = md5(whl_bytes).hexdigest() + + release = { + "comment_text": "", + "digests": {"sha256": whl_sha256, "md5": whl_md5}, + "downloads": -1, + "filename": whl_path.name, + "has_sig": False, + "md5_digest": whl_md5, + "packagetype": "bdist_wheel", + "python_version": "py3", + "requires_python": metadata.requires_python, + "size": whl_stat.st_size, + "upload_time": whl_isodate, + "upload_time_iso_8601": whl_isodate, + "url": f"./{whl_path.name}", + "yanked": False, + "yanked_reason": None, + } + + return metadata.name, metadata.version, release + + +def get_wheel_repodata(whl_path: Path): + """Get pyodide-compatible `repodata.json` fragment for a wheel. + + This only knows how to handle "simple" noarch wheels, without extra binary + depnendencies. + """ + name, version, release = get_wheel_fileinfo(whl_path) + normalized_name = get_normalized_name(name) + depends = get_wheel_depends(whl_path) + REPODATA_EXTRA_DEPENDS.get( + normalized_name, [] + ) + modules = get_wheel_modules(whl_path) + pkg_entry = { + "name": normalized_name, + "version": version, + "file_name": whl_path.name, + "install_dir": "site" if whl_path.name.endswith(NOARCH_WHL) else "dynlib", + "sha256": release["digests"]["sha256"], + "imports": modules, + "depends": depends, + } + return normalized_name, version, pkg_entry + + +@functools.lru_cache(1000) +def get_wheel_pkginfo(whl_path: Path): + """Return the as-parsed distribution information from ``pkginfo``.""" + import pkginfo + + return pkginfo.get_metadata(str(whl_path)) + + +def get_wheel_modules(whl_path: Path) -> List[str]: + """Get the exported top-level modules from a wheel.""" + top_levels = {} + records = {} + with zipfile.ZipFile(whl_path) as zf: + for zipinfo in zf.infolist(): + if zipinfo.filename.endswith(TOP_LEVEL_TXT): + top_levels[zipinfo.filename] = ( + zf.read(zipinfo).decode("utf-8").strip().splitlines() + ) + if zipinfo.filename.endswith(WHL_RECORD): + records[zipinfo.filename] = ( + zf.read(zipinfo).decode("utf-8").strip().splitlines() + ) + + if len(top_levels): + sorted_top_levels = sorted(top_levels.items(), key=lambda x: len(x[0])) + return sorted_top_levels[0][1] + + if len(records): + sorted_records = sorted(records.items(), key=lambda x: len(x[0])) + # discard hash, length, etc. + record_bits = sorted( + [line.split(",")[0].split("/") for line in sorted_records[0][1]], + key=lambda x: len(x), + ) + + imports = set() + inits = [] + for bits in record_bits: + if bits[0].endswith(".data") or bits[0].endswith(".dist-info"): + continue + elif bits[0].endswith(".py"): + # this is a single-file module that gets dropped in site-packages + imports.add(bits[0].replace(".py", "")) + elif bits[-1].endswith("__init__.py"): + # this might be a namespace package + inits += [bits] + + if not imports and inits: + for init_bits in inits: + dotted = ".".join(init_bits[:-1]) + if any(f"{imp}." in dotted for imp in imports): + continue + imports.add(dotted) + + if imports: + return sorted(imports) + + # this should probably never happen + raise ValueError(f"{whl_path} contains neither {TOP_LEVEL_TXT} nor {WHL_RECORD}") + + +def get_wheel_depends(whl_path: Path): + """Get the normalize runtime distribution dependencies from a wheel.""" + from packaging.requirements import Requirement + + metadata = get_wheel_pkginfo(str(whl_path)) + + depends: List[str] = [] + + for dep_str in metadata.requires_dist: + if dep_str.endswith(";"): + dep_str = dep_str[:-1] + req = Requirement(dep_str) + if req.marker is None or req.marker.evaluate(PYODIDE_MARKER_ENV): + depends += [get_normalized_name(req.name)] + + return sorted(set(depends)) + + +def get_normalized_name(raw_name: str) -> str: + """Get a PEP 503 normalized name for a python package. + + https://peps.python.org/pep-0503/#normalized-names + """ + return re.sub(r"[-_.]+", "-", raw_name).lower() + + +def get_wheel_index(wheels: List[Path], metadata=None): + """Get the raw python object representing a wheel index for a bunch of wheels + + If given, metadata should be a dictionary of the form: + + {Path: (name, version, metadata)} + """ + metadata = metadata or {} + all_json = {} + + for whl_path in sorted(wheels): + name, version, release = metadata.get(whl_path) or get_wheel_fileinfo(whl_path) + normalized_name = get_normalized_name(name) + if normalized_name not in all_json: + all_json[normalized_name] = {"releases": {}} + all_json[normalized_name]["releases"][version] = [release] + + return all_json + + +def get_repo_index(wheelish: List[Path], metadata=None): + """Get the data for a ``repodata.json``.""" + metadata = metadata or {} + repodata_json = {"packages": {}} + + for whl_path in sorted(wheelish): + name, version, pkg_entry = metadata.get(whl_path) or get_wheel_repodata( + whl_path + ) + normalized_name = get_normalized_name(name) + if normalized_name in repodata_json["packages"]: + old_version = repodata_json["packages"][normalized_name]["version"] + warnings.warn( + f"{normalized_name} {old_version} will be clobbered by {version}" + ) + repodata_json["packages"][normalized_name] = pkg_entry + + return repodata_json + + +def write_wheel_index(whl_dir: Path, metadata=None) -> Path: + """Write out an ``all.json`` for a directory of wheels.""" + wheel_index = Path(whl_dir) / ALL_JSON + index_data = get_wheel_index(list_wheels(whl_dir), metadata) + wheel_index.write_text(json.dumps(index_data, **JSON_FMT), **UTF8) + return wheel_index + + +def write_repo_index(whl_dir: Path, metadata=None, extensions=None) -> Path: + """Write out a ``repodata.json`` for a directory of wheels.""" + repo_index = Path(whl_dir) / REPODATA_JSON + wheelish = list_wheels(whl_dir, extensions) + + index_data = get_repo_index(wheelish, metadata) + repo_index.write_text(json.dumps(index_data, **JSON_FMT), **UTF8) + return repo_index diff --git a/yarn.lock b/yarn.lock index 41d8ad92..3660d087 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2696,6 +2696,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + axios@^1.0.0: version "1.3.4" resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.4.tgz#f5760cefd9cfb51fd2481acf88c05f67c4523024" @@ -2985,7 +2990,7 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" -call-bind@^1.0.2: +call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== @@ -3479,7 +3484,7 @@ cosmiconfig@7.0.0: path-type "^4.0.0" yaml "^1.10.0" -cross-spawn@^6.0.0: +cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -3650,7 +3655,7 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -define-properties@^1.1.3: +define-properties@^1.1.3, define-properties@^1.1.4: version "1.2.0" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== @@ -3920,11 +3925,68 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +es-abstract@^1.19.0, es-abstract@^1.20.4: + version "1.21.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.1.tgz#e6105a099967c08377830a0c9cb589d570dd86c6" + integrity sha512-QudMsPOz86xYz/1dG1OuGBKOELjCh99IIWHLzy5znUB6j8xG2yMA7bfTV86VSqKF+Y/H08vQPR+9jyXpuC6hfg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.3" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.4" + is-array-buffer "^3.0.1" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.10" + is-weakref "^1.0.2" + object-inspect "^1.12.2" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.9" + es-module-lexer@^0.9.0: version "0.9.3" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-set-tostringtag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" + integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + has-tostringtag "^1.0.0" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + es6-templates@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/es6-templates/-/es6-templates-0.2.3.tgz#5cb9ac9fb1ded6eb1239342b81d792bbb4078ee4" @@ -4430,6 +4492,13 @@ follow-redirects@^1.15.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -4511,6 +4580,16 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + functions-have-names@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" @@ -4540,7 +4619,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== @@ -4588,6 +4667,14 @@ get-stream@^5.0.0: dependencies: pump "^3.0.0" +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -4696,6 +4783,16 @@ glob@^8.0.1: minimatch "^5.0.1" once "^1.3.0" +glob@^9.2.0: + version "9.2.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.2.1.tgz#f47e34e1119e7d4f93a546e75851ba1f1e68de50" + integrity sha512-Pxxgq3W0HyA3XUvSXcFhRSs+43Jsx0ddxcFrbjxNGkL2Ak5BAUBxLqI5G6ADDeCHLfzzXFhe0b1yYcctGmytMA== + dependencies: + fs.realpath "^1.0.0" + minimatch "^7.4.1" + minipass "^4.2.4" + path-scurry "^1.6.1" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -4708,6 +4805,13 @@ globals@^13.19.0: dependencies: type-fest "^0.20.2" +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + globby@11.1.0, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" @@ -4720,6 +4824,13 @@ globby@11.1.0, globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + graceful-fs@4.2.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" @@ -4762,6 +4873,11 @@ harmony-reflect@^1.4.6: resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.2.tgz#31ecbd32e648a34d030d86adb67d4d47547fe710" integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g== +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -4779,6 +4895,11 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbols@^1.0.2, has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" @@ -5095,6 +5216,15 @@ inquirer@^8.2.4: through "^2.3.6" wrap-ansi "^7.0.0" +internal-slot@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" + integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== + dependencies: + get-intrinsic "^1.2.0" + has "^1.0.3" + side-channel "^1.0.4" + interpret@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" @@ -5127,16 +5257,45 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-array-buffer@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" + integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + is-typed-array "^1.1.10" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + is-ci@2.0.0, is-ci@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" @@ -5239,6 +5398,18 @@ is-lambda@^1.0.1: resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -5283,7 +5454,7 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-regex@^1.0.4: +is-regex@^1.0.4, is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== @@ -5291,6 +5462,13 @@ is-regex@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + is-ssh@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.4.0.tgz#4f8220601d2839d8fa624b3106f8e8884f01b8b2" @@ -5308,6 +5486,20 @@ is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + is-text-path@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" @@ -5315,6 +5507,17 @@ is-text-path@^1.0.1: dependencies: text-extensions "^1.0.0" +is-typed-array@^1.1.10, is-typed-array@^1.1.9: + version "1.1.10" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" + integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -5325,6 +5528,13 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -6395,10 +6605,10 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: - version "7.18.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.1.tgz#4716408dec51d5d0104732647f584d1f6738b109" - integrity sha512-8/HcIENyQnfUTCDizRu9rrDyG6XG/21M4X7/YEGZeD76ZJilFPAUVb/2zysFf7VVO1LEjCDFyHp8pMMvozIrvg== +lru-cache@^7.14.1, lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== ltgt@^2.1.2: version "2.2.1" @@ -6502,6 +6712,11 @@ marked@^4.0.17: resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.12.tgz#d69a64e21d71b06250da995dcd065c11083bebb5" integrity sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw== +memorystream@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" + integrity sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw== + meow@^8.0.0: version "8.1.2" resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" @@ -6613,6 +6828,13 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" +minimatch@^7.4.1: + version "7.4.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.2.tgz#157e847d79ca671054253b840656720cb733f10f" + integrity sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA== + dependencies: + brace-expansion "^2.0.1" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -6681,6 +6903,11 @@ minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6: dependencies: yallist "^4.0.0" +minipass@^4.0.2, minipass@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.4.tgz#7d0d97434b6a19f59c5c3221698b48bbf3b2cd06" + integrity sha512-lwycX3cBMTvcejsHITUgYj6Gy6A7Nh4Q6h9NP4sTHY1ccJlC7yKzDmiShEHsJ16Jf1nKGDEaiHxiltsJEvk0nQ== + minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -7008,6 +7235,21 @@ npm-registry-fetch@13.3.0, npm-registry-fetch@^13.0.0, npm-registry-fetch@^13.0. npm-package-arg "^9.0.1" proc-log "^2.0.0" +npm-run-all@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/npm-run-all/-/npm-run-all-4.1.5.tgz#04476202a15ee0e2e214080861bff12a51d98fba" + integrity sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ== + dependencies: + ansi-styles "^3.2.1" + chalk "^2.4.1" + cross-spawn "^6.0.5" + memorystream "^0.3.1" + minimatch "^3.0.4" + pidtree "^0.3.0" + read-pkg "^3.0.0" + shell-quote "^1.6.1" + string.prototype.padend "^3.0.0" + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -7102,6 +7344,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-inspect@^1.12.2, object-inspect@^1.9.0: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== + object-is@^1.0.1: version "1.1.5" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" @@ -7122,6 +7369,16 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" +object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + object.pick@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" @@ -7434,6 +7691,14 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.6.1.tgz#dab45f7bb1d3f45a0e271ab258999f4ab7e23132" + integrity sha512-OW+5s+7cw6253Q4E+8qQ/u1fVvcJQCJo/VFD8pje+dbJCF1n5ZRMV2AEHbGp+5Q7jxQIYJxkHopnj6nzdGeZLA== + dependencies: + lru-cache "^7.14.1" + minipass "^4.0.2" + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -7456,6 +7721,11 @@ picomatch@^2.0.4, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pidtree@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.1.tgz#ef09ac2cc0533df1f3250ccf2c4d366b0d12114a" + integrity sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA== + pify@5.0.0, pify@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" @@ -7690,10 +7960,10 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== -pyodide@0.22.0: - version "0.22.0" - resolved "https://registry.yarnpkg.com/pyodide/-/pyodide-0.22.0.tgz#44c264e26d5c054e676b051cc5a15a1d14e259c6" - integrity sha512-H5BV2m3dDXisZSxKZpPXDudsoAcRRo7Zq9ca4nlj5WjzSC2SY0+Q3fiZDMosgiz7W1BQz5FJUv3EDTmmscJRvA== +pyodide@0.22.1: + version "0.22.1" + resolved "https://registry.yarnpkg.com/pyodide/-/pyodide-0.22.1.tgz#45bfab36ef332ec7fed6fb9736ab13ebd54f05d1" + integrity sha512-6+PkFLTC+kcBKtFQxYBxR44J5IBxLm8UGkobLgZv1SxzV9qOU2rb0YYf0qDtlnfDiN/IQd2uckf+D8Zwe88Mqg== dependencies: base-64 "^1.0.0" node-fetch "^2.6.1" @@ -7930,7 +8200,7 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp.prototype.flags@^1.2.0: +regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== @@ -8038,13 +8308,20 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^3.0.0, rimraf@^3.0.2, rimraf@~3.0.0: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" +rimraf@^4.4.0, rimraf@~4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.4.0.tgz#c7a9f45bb2ec058d2e60ef9aca5167974313d605" + integrity sha512-X36S+qpCUR0HjXlkDe4NAOhS//aHH0Z+h8Ckf2auGJk3PTnx5rLmrHkwNdbVQuCSUhOyFrlRvFEllZOYE+yZGQ== + dependencies: + glob "^9.2.0" + rsvp@^4.8.4: version "4.8.5" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" @@ -8074,6 +8351,15 @@ safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -8230,11 +8516,25 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shell-quote@^1.6.1: + version "1.8.0" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba" + integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ== + shellwords@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + signal-exit@3.0.7, signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -8474,6 +8774,33 @@ string-length@^4.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string.prototype.padend@^3.0.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.4.tgz#2c43bb3a89eb54b6750de5942c123d6c98dd65b6" + integrity sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string.prototype.trimend@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" + integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string.prototype.trimstart@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" + integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + string_decoder@^1.1.1, string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -8903,6 +9230,15 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + typed-styles@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9" @@ -8920,7 +9256,7 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -"typescript@^3 || ^4", typescript@~4.9.3, typescript@~4.9.4: +"typescript@^3 || ^4", typescript@~4.9.4: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== @@ -8941,6 +9277,16 @@ uglify-js@3.4.x, uglify-js@^3.1.4: commander "~2.19.0" source-map "~0.6.1" +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -9304,11 +9650,34 @@ whatwg-url@^8.0.0, whatwg-url@^8.5.0: tr46 "^2.1.0" webidl-conversions "^6.1.0" +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== +which-typed-array@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" + integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.10" + which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"