From 0fc4f6b91ec86107b6baceb106953d8e6f291293 Mon Sep 17 00:00:00 2001 From: Eric Ma Date: Sun, 8 Mar 2026 18:56:17 -0400 Subject: [PATCH 1/5] Add Pyodide (WASM) wheel build and release asset - Replace old 'Build universal wheel for Pyodide' job with proper pyodide-build + Emscripten job: Python 3.12, pyodide-build, setup-emsdk, pip install -e .[pyodide], pyodide build + Upload artifact pyodide_wheel (dist/*wasm32.whl) - Add job to upload Pyodide wheel to GitHub Release on publish (PyPI does not accept wasm32 yet) - Add optional dependency pyodide = [pyodide-build>=0.29.2] and document manual build in dev guide - Remove Pixi references: .pixi from pre-commit sphinx-lint args, conda/mamba/pixi -> conda/mamba in cmodule and troubleshooting - upload_pypi no longer downloads universal_wheel; PyPI gets only sdist + cibuildwheel wheels - Numba remains optional on wasm via existing env markers (micropip install works without numba) Made-with: Cursor --- .github/workflows/pypi.yml | 57 +++++++++++++++++++++++++++----------- .pre-commit-config.yaml | 2 +- doc/dev_start_guide.rst | 48 ++++++++++++++++++++++++++++++++ doc/troubleshooting.rst | 2 +- pyproject.toml | 1 + pytensor/link/c/cmodule.py | 2 +- 6 files changed, 93 insertions(+), 19 deletions(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 9503eba1aa..fcee969f3a 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -129,10 +129,9 @@ jobs: name: wheels-${{ matrix.platform }} path: ./wheelhouse/*.whl - build_universal_wheel: - name: Build universal wheel for Pyodide + build_pyodide_wheel: + name: Build Pyodide (WASM) wheel needs: check_changes - # Run if it's a release or if relevant files changed on main if: | needs.check_changes.outputs.should_run == 'true' runs-on: ubuntu-latest @@ -150,24 +149,34 @@ jobs: with: python-version: '3.12' - - name: Install dependencies - run: pip install --upgrade setuptools numpy versioneer wheel - - - name: Build universal wheel + - name: Get Emscripten version for pyodide-build + id: emscripten run: | - PYODIDE=1 python setup.py bdist_wheel --universal + pip install pyodide-build>=0.29.2 + echo "version=$(pyodide config get emscripten_version)" >> $GITHUB_OUTPUT + + - name: Set up Emscripten + uses: mymindstorm/setup-emsdk@v14 + with: + version: ${{ steps.emscripten.outputs.version }} + + - name: Install project with pyodide extra + run: pip install -e ".[pyodide]" + + - name: Build Pyodide wheel + run: pyodide build - name: Attest GitHub build provenance uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 # Don't attest from forks if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository with: - subject-path: dist/*.whl + subject-path: dist/*wasm32.whl - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: universal_wheel - path: dist/*.whl + name: pyodide_wheel + path: dist/*wasm32.whl check_dist: name: Check dist @@ -203,6 +212,27 @@ jobs: - run: pipx run twine check --strict dist/* + upload_pyodide_to_release: + name: Upload Pyodide wheel to GitHub Release + needs: [build_pyodide_wheel] + runs-on: ubuntu-latest + if: github.repository_owner == 'pymc-devs' && github.event_name == 'release' && github.event.action == 'published' + permissions: + contents: write + steps: + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: pyodide_wheel + path: dist + + - name: Upload Pyodide wheel to Release + uses: softprops/action-gh-release@c20b0eb2e0d72b0e686e246117c0d71a611d7843f # v2.0.0 + with: + tag_name: ${{ github.event.release.tag_name }} + files: dist/*.whl + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + upload_pypi: name: Upload to PyPI on release # Use the `release` GitHub environment to protect the Trusted Publishing (OIDC) @@ -226,10 +256,5 @@ jobs: path: dist merge-multiple: true - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: universal_wheel - path: dist - - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 # Implicitly attests that the packages were uploaded in the context of this workflow. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8f4f36e6cb..0b35a8fec4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: rev: v1.0.2 hooks: - id: sphinx-lint - args: ["-i", ".pixi", "."] + args: ["."] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.16 hooks: diff --git a/doc/dev_start_guide.rst b/doc/dev_start_guide.rst index 010d0ffb75..bb1bb99d63 100644 --- a/doc/dev_start_guide.rst +++ b/doc/dev_start_guide.rst @@ -222,6 +222,54 @@ Afterward, you can go to `html/index.html` and navigate the changes in a browser **Do not commit the `html` directory. The documentation is built automatically.** For more documentation customizations such as different formats e.g., PDF, refer to the `Sphinx documentation `_. +Building a WebAssembly (Pyodide) wheel +-------------------------------------- + +To build a wheel targeting WebAssembly for use with `Pyodide `_ (e.g. for the browser or JupyterLite), use the Pyodide build tooling. This produces a wheel in ``dist/`` with a name like ``*-cpXXX-cpXXX-pyodide_*_wasm32.whl``. + +On Pyodide/wasm32, the **numba** dependency is omitted (via environment markers in ``pyproject.toml``), so the package can be installed with ``micropip.install(url)`` without pulling in numba. + +**Full workflow (copy-paste)** + +Use Python 3.11, 3.12, or 3.13 (Pyodide does not yet support 3.14). From the project root: + +**1. One-time: install Emscripten** + +.. code-block:: bash + + # Clone and install the Emscripten SDK + git clone https://github.com/emscripten-core/emsdk.git /path/to/emsdk + cd /path/to/emsdk + # Use the version required by pyodide-build (check with: pyodide config get emscripten_version) + ./emsdk install 3.1.45 + ./emsdk activate 3.1.45 + source emsdk_env.sh + +In every new shell where you will run ``pyodide build``, run ``source /path/to/emsdk/emsdk_env.sh`` first (or add it to your profile). + +**2. One-time per machine: create a venv and install the Pyodide build extra** + +.. code-block:: bash + + python3.12 -m venv .venv-pyodide + source .venv-pyodide/bin/activate # On Windows: .venv-pyodide\Scripts\activate + pip install -e ".[pyodide]" + +This installs the project in editable form and the ``pyodide-build`` tool (declared in the ``pyodide`` optional dependency). + +**3. Each time you want to build the wasm wheel** + +.. code-block:: bash + + source /path/to/emsdk/emsdk_env.sh + source .venv-pyodide/bin/activate + cd /path/to/pytensor + pyodide build + +The wheel will appear in ``dist/``. PyPI does not yet accept emscripten/wasm32 wheels; host the file elsewhere (e.g. GitHub Releases) and install in Pyodide with ``micropip.install(url)``. + +To see which Emscripten version your installed ``pyodide-build`` expects, run ``pyodide config get emscripten_version`` and use that in step 1 if it differs from ``3.1.45``. For more detail, see `Pyodide: building packages `_. + Other tools that might help =========================== diff --git a/doc/troubleshooting.rst b/doc/troubleshooting.rst index 6c7ffd3451..a9d5f43166 100644 --- a/doc/troubleshooting.rst +++ b/doc/troubleshooting.rst @@ -196,7 +196,7 @@ or running :func:`pytensor.link.c.cmodule.default_blas_ldflags`. Here are some different way to configure BLAS: 0) Do nothing and use the default config. -This will usually work great for installation via conda/mamba/pixi (conda-forge channel). +This will usually work great for installation via conda/mamba (conda-forge channel). It will usually fail to link altogether for installation via pip. 1) Disable the usage of BLAS and fall back on NumPy for dot products. To do diff --git a/pyproject.toml b/pyproject.toml index d18a1ae0bb..0e73f76660 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,7 @@ kanren = [ "miniKanren", "cons", ] +pyodide = ["pyodide-build>=0.29.2"] [tool.setuptools.packages.find] include = ["pytensor*"] diff --git a/pytensor/link/c/cmodule.py b/pytensor/link/c/cmodule.py index bb21559fae..0f1e20fc09 100644 --- a/pytensor/link/c/cmodule.py +++ b/pytensor/link/c/cmodule.py @@ -2938,7 +2938,7 @@ def default_blas_ldflags() -> str: _logger.debug("Failed to identify blas ldflags. Will leave them empty.") warnings.warn( "PyTensor could not link to a BLAS installation. Operations that might benefit from BLAS will be severely degraded.\n" - "This usually happens when PyTensor is installed via pip. We recommend it be installed via conda/mamba/pixi instead.\n" + "This usually happens when PyTensor is installed via pip. We recommend it be installed via conda/mamba instead.\n" "Alternatively, you can use an experimental backend such as Numba or JAX that perform their own BLAS optimizations, " "by setting `pytensor.config.mode == 'NUMBA'` or passing `mode='NUMBA'` when compiling a PyTensor function.\n" "For more options and details see https://pytensor.readthedocs.io/en/latest/troubleshooting.html#how-do-i-configure-test-my-blas-library", From d8f10d06b49bb572f52175c22c0bd63243a0171c Mon Sep 17 00:00:00 2001 From: Eric Ma Date: Sun, 8 Mar 2026 18:59:06 -0400 Subject: [PATCH 2/5] ci: pin setup-emsdk to full SHA; use gh release upload instead of softprops action Made-with: Cursor --- .github/workflows/pypi.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index fcee969f3a..1357611c0f 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -156,7 +156,7 @@ jobs: echo "version=$(pyodide config get emscripten_version)" >> $GITHUB_OUTPUT - name: Set up Emscripten - uses: mymindstorm/setup-emsdk@v14 + uses: mymindstorm/setup-emsdk@6ab9eb1bda2574c4ddb79809fc9247783eaf9021 # v14 with: version: ${{ steps.emscripten.outputs.version }} @@ -226,12 +226,9 @@ jobs: path: dist - name: Upload Pyodide wheel to Release - uses: softprops/action-gh-release@c20b0eb2e0d72b0e686e246117c0d71a611d7843f # v2.0.0 - with: - tag_name: ${{ github.event.release.tag_name }} - files: dist/*.whl + run: gh release upload "${{ github.event.release.tag_name }}" dist/*.whl env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} upload_pypi: name: Upload to PyPI on release From 2329e7ae283c5322347adfd3f9078b657004b846 Mon Sep 17 00:00:00 2001 From: Eric Ma Date: Sun, 8 Mar 2026 19:53:28 -0400 Subject: [PATCH 3/5] ci: pass release tag via env var to fix code injection scan Made-with: Cursor --- .github/workflows/pypi.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 1357611c0f..6e991d9ede 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -226,9 +226,10 @@ jobs: path: dist - name: Upload Pyodide wheel to Release - run: gh release upload "${{ github.event.release.tag_name }}" dist/*.whl + run: gh release upload "$RELEASE_TAG" dist/*.whl env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ github.event.release.tag_name }} upload_pypi: name: Upload to PyPI on release From 1a5f2bae08912ed371114e2297ace192fd7d6ce1 Mon Sep 17 00:00:00 2001 From: Eric Ma Date: Fri, 13 Mar 2026 20:06:39 -0400 Subject: [PATCH 4/5] Address PR review feedback - Restore pixi references in docs and warnings (previously removed without explanation) - Restore helpful workflow comments in pypi.yml - Move Pyodide build docs from dev_start_guide to install.rst (more appropriate location) - Add user-facing WASM/Pyodide installation instructions in install.rst --- .github/workflows/pypi.yml | 1 + .pre-commit-config.yaml | 2 +- doc/dev_start_guide.rst | 48 -------------------------------------- doc/install.rst | 19 +++++++++++++++ doc/troubleshooting.rst | 2 +- pytensor/link/c/cmodule.py | 2 +- 6 files changed, 23 insertions(+), 51 deletions(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 6e991d9ede..1b8a7ca7de 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -132,6 +132,7 @@ jobs: build_pyodide_wheel: name: Build Pyodide (WASM) wheel needs: check_changes + # Run if it's a release or if relevant files changed on main if: | needs.check_changes.outputs.should_run == 'true' runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0b35a8fec4..8f4f36e6cb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: rev: v1.0.2 hooks: - id: sphinx-lint - args: ["."] + args: ["-i", ".pixi", "."] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.16 hooks: diff --git a/doc/dev_start_guide.rst b/doc/dev_start_guide.rst index bb1bb99d63..010d0ffb75 100644 --- a/doc/dev_start_guide.rst +++ b/doc/dev_start_guide.rst @@ -222,54 +222,6 @@ Afterward, you can go to `html/index.html` and navigate the changes in a browser **Do not commit the `html` directory. The documentation is built automatically.** For more documentation customizations such as different formats e.g., PDF, refer to the `Sphinx documentation `_. -Building a WebAssembly (Pyodide) wheel --------------------------------------- - -To build a wheel targeting WebAssembly for use with `Pyodide `_ (e.g. for the browser or JupyterLite), use the Pyodide build tooling. This produces a wheel in ``dist/`` with a name like ``*-cpXXX-cpXXX-pyodide_*_wasm32.whl``. - -On Pyodide/wasm32, the **numba** dependency is omitted (via environment markers in ``pyproject.toml``), so the package can be installed with ``micropip.install(url)`` without pulling in numba. - -**Full workflow (copy-paste)** - -Use Python 3.11, 3.12, or 3.13 (Pyodide does not yet support 3.14). From the project root: - -**1. One-time: install Emscripten** - -.. code-block:: bash - - # Clone and install the Emscripten SDK - git clone https://github.com/emscripten-core/emsdk.git /path/to/emsdk - cd /path/to/emsdk - # Use the version required by pyodide-build (check with: pyodide config get emscripten_version) - ./emsdk install 3.1.45 - ./emsdk activate 3.1.45 - source emsdk_env.sh - -In every new shell where you will run ``pyodide build``, run ``source /path/to/emsdk/emsdk_env.sh`` first (or add it to your profile). - -**2. One-time per machine: create a venv and install the Pyodide build extra** - -.. code-block:: bash - - python3.12 -m venv .venv-pyodide - source .venv-pyodide/bin/activate # On Windows: .venv-pyodide\Scripts\activate - pip install -e ".[pyodide]" - -This installs the project in editable form and the ``pyodide-build`` tool (declared in the ``pyodide`` optional dependency). - -**3. Each time you want to build the wasm wheel** - -.. code-block:: bash - - source /path/to/emsdk/emsdk_env.sh - source .venv-pyodide/bin/activate - cd /path/to/pytensor - pyodide build - -The wheel will appear in ``dist/``. PyPI does not yet accept emscripten/wasm32 wheels; host the file elsewhere (e.g. GitHub Releases) and install in Pyodide with ``micropip.install(url)``. - -To see which Emscripten version your installed ``pyodide-build`` expects, run ``pyodide config get emscripten_version`` and use that in step 1 if it differs from ``3.1.45``. For more detail, see `Pyodide: building packages `_. - Other tools that might help =========================== diff --git a/doc/install.rst b/doc/install.rst index 71ff66a53e..c25472b62e 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -26,3 +26,22 @@ The current development branch of PyTensor can be installed from GitHub using `p To use the Numba and JAX backend you will need to install these libraries in addition to PyTensor. Please refer to `Numba's installation instructions `__ and `JAX's installation instructions `__ respectively. + + +Installing on Pyodide (WebAssembly) +----------------------------------- + +PyTensor can be used in browser-based Python environments via `Pyodide `__, such as `JupyterLite `__. + +PyPI does not yet accept WebAssembly wheels, so you'll need to install from a GitHub Release: + +.. code-block:: python + + import micropip + micropip.install("https://github.com/pymc-devs/pytensor/releases/download/vVERSION/pytensor-VERSION-py312-py312-pyodide_2024_0_wasm32.whl") + +Replace ``VERSION`` with the desired release version (e.g., ``2.26.4``). + +.. note:: + + The Numba backend is not available on Pyodide/wasm32. PyTensor will use other backends automatically. diff --git a/doc/troubleshooting.rst b/doc/troubleshooting.rst index a9d5f43166..6c7ffd3451 100644 --- a/doc/troubleshooting.rst +++ b/doc/troubleshooting.rst @@ -196,7 +196,7 @@ or running :func:`pytensor.link.c.cmodule.default_blas_ldflags`. Here are some different way to configure BLAS: 0) Do nothing and use the default config. -This will usually work great for installation via conda/mamba (conda-forge channel). +This will usually work great for installation via conda/mamba/pixi (conda-forge channel). It will usually fail to link altogether for installation via pip. 1) Disable the usage of BLAS and fall back on NumPy for dot products. To do diff --git a/pytensor/link/c/cmodule.py b/pytensor/link/c/cmodule.py index 0f1e20fc09..bb21559fae 100644 --- a/pytensor/link/c/cmodule.py +++ b/pytensor/link/c/cmodule.py @@ -2938,7 +2938,7 @@ def default_blas_ldflags() -> str: _logger.debug("Failed to identify blas ldflags. Will leave them empty.") warnings.warn( "PyTensor could not link to a BLAS installation. Operations that might benefit from BLAS will be severely degraded.\n" - "This usually happens when PyTensor is installed via pip. We recommend it be installed via conda/mamba instead.\n" + "This usually happens when PyTensor is installed via pip. We recommend it be installed via conda/mamba/pixi instead.\n" "Alternatively, you can use an experimental backend such as Numba or JAX that perform their own BLAS optimizations, " "by setting `pytensor.config.mode == 'NUMBA'` or passing `mode='NUMBA'` when compiling a PyTensor function.\n" "For more options and details see https://pytensor.readthedocs.io/en/latest/troubleshooting.html#how-do-i-configure-test-my-blas-library", From 3596bde39955fcce995ae3e4c78031daae320ae0 Mon Sep 17 00:00:00 2001 From: Eric Ma Date: Sat, 14 Mar 2026 13:48:08 -0400 Subject: [PATCH 5/5] Add Pyodide/WASM installation links to README and docs landing page Addresses lucianopaz's feedback: make WASM installation instructions discoverable from the README and docs index, not just install.rst. --- README.rst | 3 +++ doc/index.rst | 2 ++ doc/install.rst | 2 ++ 3 files changed, 7 insertions(+) diff --git a/README.rst b/README.rst index 76299252db..fd502aa98b 100644 --- a/README.rst +++ b/README.rst @@ -115,6 +115,9 @@ The current development branch of |Project Name| can be installed from GitHub, a pip install git+https://github.com/pymc-devs/pytensor +For browser-based Python environments (Pyodide/JupyterLite), see the `Pyodide installation instructions `__. + + Background ========== diff --git a/doc/index.rst b/doc/index.rst index a70a28df82..886a5bfdb2 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -41,6 +41,8 @@ directory, so that when you pull updates via Git, they will be automatically reflected the "installed" version. For more information about installation and configuration, see :ref:`installing PyTensor `. +For browser-based Python environments (Pyodide/JupyterLite), see :ref:`the Pyodide installation section `. + .. _available on PyPI: http://pypi.python.org/pypi/pytensor .. _Related Projects: https://github.com/pymc-devs/pytensor/wiki/Related-projects diff --git a/doc/install.rst b/doc/install.rst index c25472b62e..cf9661a43b 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -31,6 +31,8 @@ To use the Numba and JAX backend you will need to install these libraries in add Installing on Pyodide (WebAssembly) ----------------------------------- +.. _install-pyodide: + PyTensor can be used in browser-based Python environments via `Pyodide `__, such as `JupyterLite `__. PyPI does not yet accept WebAssembly wheels, so you'll need to install from a GitHub Release: