Skip to content

Add wasm CI #29093

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
---
"CI: Run cibuildwheel":
- changed-files:
- any-glob-to-any-file: ['.github/workflows/cibuildwheel.yml']
- any-glob-to-any-file:
- '.github/workflows/cibuildwheel.yml'
- '.github/workflows/wasm.yml'
"CI: Run cygwin":
- changed-files:
- any-glob-to-any-file: ['.github/workflows/cygwin.yml']
Expand Down
40 changes: 20 additions & 20 deletions .github/workflows/nightlies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,28 @@ jobs:
run: |
PROJECT_REPO="matplotlib/matplotlib"
BRANCH="main"
WORKFLOW_NAME="cibuildwheel.yml"
ARTIFACT_PATTERN="cibw-wheels-*"

gh run --repo "${PROJECT_REPO}" \
list --branch "${BRANCH}" \
--workflow "${WORKFLOW_NAME}" \
--json event,status,conclusion,databaseId > runs.json
RUN_ID=$(
jq --compact-output \
'[
.[] |
# Filter on "push" events to main (merged PRs) ...
select(.event == "push") |
# that have completed successfully ...
select(.status == "completed" and .conclusion == "success")
] |
# and get ID of latest build of wheels.
sort_by(.databaseId) | reverse | .[0].databaseId' runs.json
)
gh run --repo "${PROJECT_REPO}" view "${RUN_ID}"
gh run --repo "${PROJECT_REPO}" \
download "${RUN_ID}" --pattern "${ARTIFACT_PATTERN}"
for WORKFLOW_NAME in cibuildwheel.yml wasm.yml; do
gh run --repo "${PROJECT_REPO}" \
list --branch "${BRANCH}" \
--workflow "${WORKFLOW_NAME}" \
--json event,status,conclusion,databaseId > runs.json
RUN_ID=$(
jq --compact-output \
'[
.[] |
# Filter on "push" events to main (merged PRs) ...
select(.event == "push") |
# that have completed successfully ...
select(.status == "completed" and .conclusion == "success")
] |
# and get ID of latest build of wheels.
sort_by(.databaseId) | reverse | .[0].databaseId' runs.json
)
gh run --repo "${PROJECT_REPO}" view "${RUN_ID}"
gh run --repo "${PROJECT_REPO}" download "${RUN_ID}" --pattern "${ARTIFACT_PATTERN}"
done

mkdir dist
mv ${ARTIFACT_PATTERN}/*.whl dist/
Expand Down
58 changes: 58 additions & 0 deletions .github/workflows/wasm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
name: Build wasm wheels

on:
# Save CI by only running this on release branches or tags.
push:
branches:
- main
- v[0-9]+.[0-9]+.x
tags:
- v*
# Also allow running this action on PRs if requested by applying the
# "Run cibuildwheel" label.
pull_request:
types:
- opened
- synchronize
- reopened
- labeled

permissions:
contents: read

jobs:
build_wasm:
if: >-
github.event_name == 'push' ||
github.event_name == 'pull_request' && (
(
github.event.action == 'labeled' &&
github.event.label.name == 'CI: Run cibuildwheel'
) ||
contains(github.event.pull_request.labels.*.name,
'CI: Run cibuildwheel')
)
name: Build wasm
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0

- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
name: Install Python
with:
python-version: '3.13'

- name: Build wheels for wasm
uses: pypa/cibuildwheel@faf86a6ed7efa889faf6996aa23820831055001a # v2.23.3
env:
CIBW_PLATFORM: "pyodide"

- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: cibw-wheels-wasm
path: ./wheelhouse/*.whl
if-no-files-found: error
2 changes: 2 additions & 0 deletions lib/matplotlib/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@
@classmethod
def isAvailable(cls):
"""Return whether a MovieWriter subclass is actually available."""
if sys.platform == 'emscripten':
return False

Check warning on line 373 in lib/matplotlib/animation.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/animation.py#L373

Added line #L373 was not covered by tests
return shutil.which(cls.bin_path()) is not None


Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/dviread.py
Original file line number Diff line number Diff line change
Expand Up @@ -1135,7 +1135,7 @@ def find_tex_file(filename):

try:
lk = _LuatexKpsewhich()
except FileNotFoundError:
except (FileNotFoundError, OSError):
lk = None # Fallback to directly calling kpsewhich, as below.

if lk:
Expand All @@ -1155,7 +1155,7 @@ def find_tex_file(filename):
path = (cbook._check_and_log_subprocess(['kpsewhich', filename],
_log, **kwargs)
.rstrip('\n'))
except (FileNotFoundError, RuntimeError):
except (FileNotFoundError, OSError, RuntimeError):
path = None

if path:
Expand Down
15 changes: 11 additions & 4 deletions lib/matplotlib/font_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,9 @@
if sys.platform == 'win32':
installed_fonts = _get_win32_installed_fonts()
fontpaths = []
elif sys.platform == 'emscripten':
installed_fonts = []
fontpaths = []

Check warning on line 292 in lib/matplotlib/font_manager.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/font_manager.py#L291-L292

Added lines #L291 - L292 were not covered by tests
else:
installed_fonts = _get_fontconfig_fonts()
if sys.platform == 'darwin':
Expand Down Expand Up @@ -1086,9 +1089,12 @@
self.ttflist = []

# Delay the warning by 5s.
timer = threading.Timer(5, lambda: _log.warning(
'Matplotlib is building the font cache; this may take a moment.'))
timer.start()
try:
timer = threading.Timer(5, lambda: _log.warning(
'Matplotlib is building the font cache; this may take a moment.'))
timer.start()
except RuntimeError:
timer = None

Check warning on line 1097 in lib/matplotlib/font_manager.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/font_manager.py#L1096-L1097

Added lines #L1096 - L1097 were not covered by tests
try:
for fontext in ["afm", "ttf"]:
for path in [*findSystemFonts(paths, fontext=fontext),
Expand All @@ -1101,7 +1107,8 @@
_log.info("Failed to extract font properties from %s: "
"%s", path, exc)
finally:
timer.cancel()
if timer:
timer.cancel()

def addfont(self, path):
"""
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/mlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,9 @@ def _stride_windows(x, n, noverlap=0):
x = np.asarray(x)

_api.check_isinstance(Integral, n=n, noverlap=noverlap)
if not (1 <= n <= x.size and n < noverlap):
if not (1 <= n <= x.size and noverlap < n):
raise ValueError(f'n ({n}) and noverlap ({noverlap}) must be positive integers '
f'with n < noverlap and n <= x.size ({x.size})')
f'with noverlap < n and n <= x.size ({x.size})')

if n == 1 and noverlap == 0:
return x[np.newaxis]
Expand Down
7 changes: 6 additions & 1 deletion lib/matplotlib/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,14 @@

Raises
------
pytest.skip
If running on emscripten, which does not support subprocesses.
pytest.xfail
If platform is Cygwin and subprocess reports a fork() failure.
"""
if sys.platform == 'emscripten':
import pytest
pytest.skip('emscripten does not support subprocesses')

Check warning on line 97 in lib/matplotlib/testing/__init__.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/testing/__init__.py#L96-L97

Added lines #L96 - L97 were not covered by tests
if capture_output:
stdout = stderr = subprocess.PIPE
try:
Expand Down Expand Up @@ -177,7 +182,7 @@
try:
mpl.dviread.find_tex_file(f"{package}.sty")
return True
except FileNotFoundError:
except (FileNotFoundError, OSError):
return False


Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/testing/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@
try:
if 'microsoft' in uname().release.lower():
raise OSError # On WSL, symlink breaks silently
if sys.platform == 'emscripten':
raise OSError

Check warning on line 142 in lib/matplotlib/testing/decorators.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/testing/decorators.py#L142

Added line #L142 was not covered by tests
os.symlink(orig_expected_path, expected_fname)
except OSError: # On Windows, symlink *may* be unavailable.
shutil.copyfile(orig_expected_path, expected_fname)
Expand Down
4 changes: 4 additions & 0 deletions lib/matplotlib/tests/test_animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@
anim.save('unused.null', writer=NullMovieWriter())


@pytest.mark.skipif(sys.platform == 'emscripten',
reason='emscripten does not support subprocesses')
def test_movie_writer_registry():
assert len(animation.writers._registered) > 0
mpl.rcParams['animation.ffmpeg_path'] = "not_available_ever_xxxx"
Expand Down Expand Up @@ -522,6 +524,8 @@
def test_movie_writer_invalid_path(anim):
if sys.platform == "win32":
match_str = r"\[WinError 3] .*\\\\foo\\\\bar\\\\aardvark'"
elif sys.platform == "emscripten":
match_str = r"\[Errno 44] .*'/foo"

Check warning on line 528 in lib/matplotlib/tests/test_animation.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/tests/test_animation.py#L528

Added line #L528 was not covered by tests
else:
match_str = r"\[Errno 2] .*'/foo"
with pytest.raises(FileNotFoundError, match=match_str):
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -8277,7 +8277,7 @@ def test_normal_axes():
]
for nn, b in enumerate(bbaxis):
targetbb = mtransforms.Bbox.from_bounds(*target[nn])
assert_array_almost_equal(b.bounds, targetbb.bounds, decimal=2)
assert_array_almost_equal(b.bounds, targetbb.bounds, decimal=1)

target = [
[150.0, 119.999, 930.0, 11.111],
Expand All @@ -8295,7 +8295,7 @@ def test_normal_axes():

target = [85.5138, 75.88888, 1021.11, 1017.11]
targetbb = mtransforms.Bbox.from_bounds(*target)
assert_array_almost_equal(bbtb.bounds, targetbb.bounds, decimal=2)
assert_array_almost_equal(bbtb.bounds, targetbb.bounds, decimal=1)

# test that get_position roundtrips to get_window_extent
axbb = ax.get_position().transformed(fig.transFigure).bounds
Expand Down
5 changes: 2 additions & 3 deletions lib/matplotlib/tests/test_backend_pdf.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import datetime
import decimal
import io
import os
from pathlib import Path

import numpy as np
Expand Down Expand Up @@ -291,8 +290,8 @@ def test_text_urls_tex():
assert annot.Rect[1] == decimal.Decimal('0.7') * 72


def test_pdfpages_fspath():
with PdfPages(Path(os.devnull)) as pdf:
def test_pdfpages_fspath(tmp_path):
with PdfPages(tmp_path / 'unused.pdf') as pdf:
pdf.savefig(plt.figure())


Expand Down
3 changes: 2 additions & 1 deletion lib/matplotlib/tests/test_dviread.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
from pathlib import Path
import shutil
import sys

import matplotlib.dviread as dr
import pytest
Expand Down Expand Up @@ -60,7 +61,7 @@ def test_PsfontsMap(monkeypatch):
fontmap[b'%']


@pytest.mark.skipif(shutil.which("kpsewhich") is None,
@pytest.mark.skipif(sys.platform == "emscripten" or shutil.which("kpsewhich") is None,
reason="kpsewhich is not available")
def test_dviread():
dirpath = Path(__file__).parent / 'baseline_images/dviread'
Expand Down
5 changes: 5 additions & 0 deletions lib/matplotlib/tests/test_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import io
import pickle
import platform
import sys
from threading import Timer
from types import SimpleNamespace
import warnings
Expand Down Expand Up @@ -1605,6 +1606,8 @@ def test_add_axes_kwargs():
plt.close()


@pytest.mark.skipif(sys.platform == 'emscripten',
reason='emscripten does not support threads')
def test_ginput(recwarn): # recwarn undoes warn filters at exit.
warnings.filterwarnings("ignore", "cannot show the figure")
fig, ax = plt.subplots()
Expand All @@ -1627,6 +1630,8 @@ def multi_presses():
np.testing.assert_allclose(fig.ginput(3), [(.3, .4), (.5, .6)])


@pytest.mark.skipif(sys.platform == 'emscripten',
reason='emscripten does not support threads')
def test_waitforbuttonpress(recwarn): # recwarn undoes warn filters at exit.
warnings.filterwarnings("ignore", "cannot show the figure")
fig = plt.figure()
Expand Down
4 changes: 3 additions & 1 deletion lib/matplotlib/tests/test_font_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from matplotlib.testing import subprocess_run_helper, subprocess_run_for_testing


has_fclist = shutil.which('fc-list') is not None
has_fclist = sys.platform != 'emscripten' and shutil.which('fc-list') is not None


def test_font_priority():
Expand Down Expand Up @@ -228,6 +228,8 @@ def _model_handler(_):
plt.close()


@pytest.mark.skipif(sys.platform == 'emscripten',
reason='emscripten does not support subprocesses')
@pytest.mark.skipif(not hasattr(os, "register_at_fork"),
reason="Cannot register at_fork handlers")
def test_fork():
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ def test_imsave_rgba_origin(origin):


@pytest.mark.parametrize("fmt", ["png", "pdf", "ps", "eps", "svg"])
def test_imsave_fspath(fmt):
plt.imsave(Path(os.devnull), np.array([[0, 1]]), format=fmt)
def test_imsave_fspath(fmt, tmp_path):
plt.imsave(tmp_path / f'unused.{fmt}', np.array([[0, 1]]), format=fmt)


def test_imsave_color_alpha():
Expand Down
8 changes: 4 additions & 4 deletions lib/matplotlib/tests/test_legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,10 +513,10 @@ def test_figure_legend_outside():
leg = fig.legend(loc='outside ' + todo)
fig.draw_without_rendering()

assert_allclose(axs.get_window_extent().extents,
axbb[nn])
assert_allclose(leg.get_window_extent().extents,
legbb[nn])
assert_allclose(axs.get_window_extent().extents, axbb[nn],
rtol=1e-4)
assert_allclose(leg.get_window_extent().extents, legbb[nn],
rtol=1e-4)


@image_comparison(['legend_stackplot.png'],
Expand Down
6 changes: 3 additions & 3 deletions lib/matplotlib/tests/test_matplotlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ def test_parse_to_version_info(version_str, version_tuple):
assert matplotlib._parse_to_version_info(version_str) == version_tuple


@pytest.mark.skipif(sys.platform == "win32",
reason="chmod() doesn't work as is on Windows")
@pytest.mark.skipif(sys.platform != "win32" and os.geteuid() == 0,
@pytest.mark.skipif(sys.platform not in ["linux", "darwin"],
reason="chmod() doesn't work on this platform")
@pytest.mark.skipif(sys.platform in ["linux", "darwin"] and os.geteuid() == 0,
reason="chmod() doesn't work as root")
def test_tmpconfigdir_warning(tmp_path):
"""Test that a warning is emitted if a temporary configdir must be used."""
Expand Down
Loading
Loading