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

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
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
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@v4
with:
fetch-depth: 0

- uses: actions/setup-python@v5
name: Install Python
with:
python-version: '3.12'

- name: Build wheels for wasm
uses: pypa/cibuildwheel@7940a4c0e76eb2030e473a5f864f291f63ee879b # v2.21.3
env:
CIBW_PLATFORM: "pyodide"

- uses: actions/upload-artifact@v4
with:
name: cibw-wheels-${{ runner.os }}-${{ matrix.cibw_archs }}
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 @@ -360,6 +360,8 @@ def bin_path(cls):
@classmethod
def isAvailable(cls):
"""Return whether a MovieWriter subclass is actually available."""
if sys.platform == 'emscripten':
return False
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 @@ -1070,7 +1070,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 @@ -1090,7 +1090,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 @@ -1092,9 +1095,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 1103 in lib/matplotlib/font_manager.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/font_manager.py#L1102-L1103

Added lines #L1102 - L1103 were not covered by tests
try:
for fontext in ["afm", "ttf"]:
for path in [*findSystemFonts(paths, fontext=fontext),
Expand All @@ -1107,7 +1113,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
39 changes: 35 additions & 4 deletions lib/matplotlib/mlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@

import functools
from numbers import Number
import sys

import numpy as np

Expand Down Expand Up @@ -210,6 +211,30 @@
return y - (b*x + a)


def _stride_windows(x, n, noverlap=0):
if noverlap >= n:
raise ValueError('noverlap must be less than n')

Check warning on line 216 in lib/matplotlib/mlab.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/mlab.py#L216

Added line #L216 was not covered by tests
if n < 1:
raise ValueError('n cannot be less than 1')

Check warning on line 218 in lib/matplotlib/mlab.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/mlab.py#L218

Added line #L218 was not covered by tests

x = np.asarray(x)

Check warning on line 220 in lib/matplotlib/mlab.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/mlab.py#L220

Added line #L220 was not covered by tests

if n == 1 and noverlap == 0:
return x[np.newaxis]

Check warning on line 223 in lib/matplotlib/mlab.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/mlab.py#L223

Added line #L223 was not covered by tests
if n > x.size:
raise ValueError('n cannot be greater than the length of x')

Check warning on line 225 in lib/matplotlib/mlab.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/mlab.py#L225

Added line #L225 was not covered by tests

# np.lib.stride_tricks.as_strided easily leads to memory corruption for
# non integer shape and strides, i.e. noverlap or n. See #3845.
noverlap = int(noverlap)
n = int(n)

Check warning on line 230 in lib/matplotlib/mlab.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/mlab.py#L229-L230

Added lines #L229 - L230 were not covered by tests

step = n - noverlap
shape = (n, (x.shape[-1]-noverlap)//step)
strides = (x.strides[0], step*x.strides[0])
return np.lib.stride_tricks.as_strided(x, shape=shape, strides=strides)

Check warning on line 235 in lib/matplotlib/mlab.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/mlab.py#L232-L235

Added lines #L232 - L235 were not covered by tests


def _spectral_helper(x, y=None, NFFT=None, Fs=None, detrend_func=None,
window=None, noverlap=None, pad_to=None,
sides=None, scale_by_freq=None, mode=None):
Expand Down Expand Up @@ -304,17 +329,23 @@
raise ValueError(
"The window length must match the data's first dimension")

result = np.lib.stride_tricks.sliding_window_view(
x, NFFT, axis=0)[::NFFT - noverlap].T
if sys.maxsize > 2**32: # NumPy version on 32-bit OOMs.
result = np.lib.stride_tricks.sliding_window_view(
x, NFFT, axis=0)[::NFFT - noverlap].T
else:
result = _stride_windows(x, NFFT, noverlap=noverlap)

Check warning on line 336 in lib/matplotlib/mlab.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/mlab.py#L336

Added line #L336 was not covered by tests
result = detrend(result, detrend_func, axis=0)
result = result * window.reshape((-1, 1))
result = np.fft.fft(result, n=pad_to, axis=0)[:numFreqs, :]
freqs = np.fft.fftfreq(pad_to, 1/Fs)[:numFreqs]

if not same_data:
# if same_data is False, mode must be 'psd'
resultY = np.lib.stride_tricks.sliding_window_view(
y, NFFT, axis=0)[::NFFT - noverlap].T
if sys.maxsize > 2**32: # NumPy version on 32-bit OOMs.
resultY = np.lib.stride_tricks.sliding_window_view(
y, NFFT, axis=0)[::NFFT - noverlap].T
else:
resultY = _stride_windows(y, NFFT, noverlap=noverlap)

Check warning on line 348 in lib/matplotlib/mlab.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/mlab.py#L348

Added line #L348 was not covered by tests
resultY = detrend(resultY, detrend_func, axis=0)
resultY = resultY * window.reshape((-1, 1))
resultY = np.fft.fft(resultY, n=pad_to, axis=0)[:numFreqs, :]
Expand Down
5 changes: 4 additions & 1 deletion lib/matplotlib/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@
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 93 in lib/matplotlib/testing/__init__.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/testing/__init__.py#L92-L93

Added lines #L92 - L93 were not covered by tests
if capture_output:
stdout = stderr = subprocess.PIPE
try:
Expand Down Expand Up @@ -175,7 +178,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
2 changes: 2 additions & 0 deletions lib/matplotlib/tests/test_agg.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import io
import sys

import numpy as np
from numpy.testing import assert_array_almost_equal
Expand Down Expand Up @@ -279,6 +280,7 @@ def test_draw_path_collection_error_handling():
fig.canvas.draw()


@pytest.mark.skipif(sys.platform == 'emscripten', reason='Too large for emscripten VM')
def test_chunksize_fails():
# NOTE: This test covers multiple independent test scenarios in a single
# function, because each scenario uses ~2GB of memory and we don't
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 @@ -278,6 +278,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 @@ -546,6 +548,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 552 in lib/matplotlib/tests/test_animation.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/tests/test_animation.py#L552

Added line #L552 was not covered by tests
else:
match_str = r"\[Errno 2] .*'/foo"
with pytest.raises(FileNotFoundError, match=match_str):
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 @@ -307,8 +306,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
5 changes: 3 additions & 2 deletions lib/matplotlib/tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,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 Expand Up @@ -1507,6 +1507,7 @@ def test_rc_interpolation_stage():
mpl.rcParams["image.interpolation_stage"] = val


@pytest.mark.skipif(sys.platform == 'emscripten', reason='Figure too large for WASM')
# We check for the warning with a draw() in the test, but we also need to
# filter the warning as it is emitted by the figure test decorator
@pytest.mark.filterwarnings(r'ignore:Data with more than .* '
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/tests/test_matplotlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,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",
@pytest.mark.skipif(sys.platform not in ["linux", "darwin"],
reason="chmod() doesn't work as is on Windows")
@pytest.mark.skipif(sys.platform != "win32" and os.geteuid() == 0,
@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
6 changes: 6 additions & 0 deletions lib/matplotlib/tests/test_mlab.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import sys

from numpy.testing import (assert_allclose, assert_almost_equal,
assert_array_equal, assert_array_almost_equal_nulp)
import numpy as np
Expand Down Expand Up @@ -873,6 +875,8 @@ def test_single_dataset_element(self):
with pytest.raises(ValueError):
mlab.GaussianKDE([42])

@pytest.mark.skipif(sys.platform == 'emscripten',
reason="WASM doesn't support floating-point exceptions")
def test_silverman_multidim_dataset(self):
"""Test silverman's for a multi-dimensional array."""
x1 = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
Expand All @@ -886,6 +890,8 @@ def test_silverman_singledim_dataset(self):
y_expected = 0.76770389927475502
assert_almost_equal(mygauss.covariance_factor(), y_expected, 7)

@pytest.mark.skipif(sys.platform == 'emscripten',
reason="WASM doesn't support floating-point exceptions")
def test_scott_multidim_dataset(self):
"""Test scott's output for a multi-dimensional array."""
x1 = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
Expand Down
Loading
Loading