From a866298c33e6b264e2408c018e2eb7e3b3069ee2 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 8 Jul 2025 00:30:51 -0400 Subject: [PATCH 01/11] Fix mlab fallback for 32-bit systems Unfortunately, I applied the change from https://github.com/matplotlib/matplotlib/pull/29115#discussion_r2189746839 directly without noticing the typo, or running full tests. So fix the swapped condition, and add a test (for `csd` only, which should be enough since everything goes though `_spectral_helper`.) --- lib/matplotlib/mlab.py | 4 ++-- lib/matplotlib/tests/test_mlab.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/mlab.py b/lib/matplotlib/mlab.py index b4b4c3f96828..4a4480c485fc 100644 --- a/lib/matplotlib/mlab.py +++ b/lib/matplotlib/mlab.py @@ -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] diff --git a/lib/matplotlib/tests/test_mlab.py b/lib/matplotlib/tests/test_mlab.py index 3b0d2529b5f1..109a6d542450 100644 --- a/lib/matplotlib/tests/test_mlab.py +++ b/lib/matplotlib/tests/test_mlab.py @@ -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 @@ -429,7 +431,16 @@ def test_spectral_helper_psd(self, mode, case): assert spec.shape[0] == freqs.shape[0] assert spec.shape[1] == getattr(self, f"t_{case}").shape[0] - def test_csd(self): + @pytest.mark.parametrize('bitsize', [ + pytest.param(None, id='default'), + pytest.param(32, + marks=pytest.mark.skipif(sys.maxsize <= 2**32, + reason='System is already 32-bit'), + id='32-bit') + ]) + def test_csd(self, bitsize, monkeypatch): + if bitsize is not None: + monkeypatch.setattr(sys, 'maxsize', 2**bitsize) freqs = self.freqs_density spec, fsp = mlab.csd(x=self.y, y=self.y+1, NFFT=self.NFFT_density, From 8eceb82c94423addd6af231753fac81b57dfcabe Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 4 Jun 2024 16:45:34 -0700 Subject: [PATCH 02/11] TST: Fix condition for test_tmpconfigdir_warning This checks `os.geteuid`, but this is only available on Unix, not Emscripten or WASM. --- lib/matplotlib/tests/test_matplotlib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_matplotlib.py b/lib/matplotlib/tests/test_matplotlib.py index d0a3f8c617e1..312fa4154b66 100644 --- a/lib/matplotlib/tests/test_matplotlib.py +++ b/lib/matplotlib/tests/test_matplotlib.py @@ -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.""" From 5095a3310c666c940ed27f43d1ce7e9e4fc294f9 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 4 Jun 2024 17:15:43 -0700 Subject: [PATCH 03/11] Ignore threading errors when preparing font cache In Enscripten/WASM, this module can be imported, but doesn't work, so we can't fall back to `dummy_threading` at import-time as we used to do. --- lib/matplotlib/font_manager.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 2db98b75ab2e..60d2ca0c2a1d 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -1086,9 +1086,12 @@ def __init__(self, size=None, weight='normal'): 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 try: for fontext in ["afm", "ttf"]: for path in [*findSystemFonts(paths, fontext=fontext), @@ -1101,7 +1104,8 @@ def __init__(self, size=None, weight='normal'): _log.info("Failed to extract font properties from %s: " "%s", path, exc) finally: - timer.cancel() + if timer: + timer.cancel() def addfont(self, path): """ From 09527b09edebeaf07a8fdf48637521dc67ae66cc Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 4 Jun 2024 17:28:28 -0700 Subject: [PATCH 04/11] Don't attempt to load system fonts on Emscripten The file system is either sandboxed or there are simply no other fonts, so limit ourselves to our pre-shipped fonts. --- lib/matplotlib/font_manager.py | 3 +++ lib/matplotlib/tests/test_font_manager.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 60d2ca0c2a1d..014c6b713fe2 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -287,6 +287,9 @@ def findSystemFonts(fontpaths=None, fontext='ttf'): if sys.platform == 'win32': installed_fonts = _get_win32_installed_fonts() fontpaths = [] + elif sys.platform == 'emscripten': + installed_fonts = [] + fontpaths = [] else: installed_fonts = _get_fontconfig_fonts() if sys.platform == 'darwin': diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 97ee8672b1d4..2253596ac3e2 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -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(): From ed9d5ecd677071efa6788f8eb4741fb6cbece37e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 26 Sep 2024 06:14:03 -0400 Subject: [PATCH 05/11] agg: Explicitly cast dimensions when creating Python buffer On WASM, which is wholly 32-bit, casting unsigned int to signed long is a narrowing conversion, which it seems to treat as an error. The Agg buffer cannot be over `(1<<23)` in width or height, so this cast is safe even with the smaller sizes. --- src/_backend_agg_wrapper.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 3dd50b31f64a..11d45773d186 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -254,12 +254,12 @@ PYBIND11_MODULE(_backend_agg, m, py::mod_gil_not_used()) .def_buffer([](RendererAgg *renderer) -> py::buffer_info { std::vector shape { - renderer->get_height(), - renderer->get_width(), + static_cast(renderer->get_height()), + static_cast(renderer->get_width()), 4 }; std::vector strides { - renderer->get_width() * 4, + static_cast(renderer->get_width() * 4), 4, 1 }; From 616c0bfc50097fa07630b937aa268c034d0356c6 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 5 Nov 2024 04:33:59 -0500 Subject: [PATCH 06/11] wasm: Skip more subprocess & thread tests --- lib/matplotlib/animation.py | 2 ++ lib/matplotlib/dviread.py | 4 ++-- lib/matplotlib/testing/__init__.py | 7 ++++++- lib/matplotlib/testing/decorators.py | 2 ++ lib/matplotlib/tests/test_animation.py | 4 ++++ lib/matplotlib/tests/test_dviread.py | 3 ++- lib/matplotlib/tests/test_figure.py | 5 +++++ lib/matplotlib/tests/test_font_manager.py | 2 ++ lib/matplotlib/texmanager.py | 8 ++++---- 9 files changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 8756cb0c1439..7267644b8b19 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -369,6 +369,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 diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 9e8b6a5facf5..20c19e8a5b19 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -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: @@ -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: diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index d6affb1b039f..c7319c0aba56 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -87,9 +87,14 @@ def subprocess_run_for_testing(command, env=None, timeout=60, stdout=None, 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') if capture_output: stdout = stderr = subprocess.PIPE try: @@ -177,7 +182,7 @@ def _has_tex_package(package): try: mpl.dviread.find_tex_file(f"{package}.sty") return True - except FileNotFoundError: + except (FileNotFoundError, OSError): return False diff --git a/lib/matplotlib/testing/decorators.py b/lib/matplotlib/testing/decorators.py index 17509449e768..f99d8a4866cd 100644 --- a/lib/matplotlib/testing/decorators.py +++ b/lib/matplotlib/testing/decorators.py @@ -138,6 +138,8 @@ def copy_baseline(self, baseline, extension): try: if 'microsoft' in uname().release.lower(): raise OSError # On WSL, symlink breaks silently + if sys.platform == 'emscripten': + raise OSError os.symlink(orig_expected_path, expected_fname) except OSError: # On Windows, symlink *may* be unavailable. shutil.copyfile(orig_expected_path, expected_fname) diff --git a/lib/matplotlib/tests/test_animation.py b/lib/matplotlib/tests/test_animation.py index 114e38996a10..b34dc01e41cb 100644 --- a/lib/matplotlib/tests/test_animation.py +++ b/lib/matplotlib/tests/test_animation.py @@ -271,6 +271,8 @@ def test_no_length_frames(anim): 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" @@ -522,6 +524,8 @@ def test_disable_cache_warning(anim): 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" else: match_str = r"\[Errno 2] .*'/foo" with pytest.raises(FileNotFoundError, match=match_str): diff --git a/lib/matplotlib/tests/test_dviread.py b/lib/matplotlib/tests/test_dviread.py index 7b7ff151be18..2aa3abe12e4b 100644 --- a/lib/matplotlib/tests/test_dviread.py +++ b/lib/matplotlib/tests/test_dviread.py @@ -1,6 +1,7 @@ import json from pathlib import Path import shutil +import sys import matplotlib.dviread as dr import pytest @@ -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' diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index c5890a2963b3..702b51ca590a 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -3,6 +3,7 @@ import io import pickle import platform +import sys from threading import Timer from types import SimpleNamespace import warnings @@ -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() @@ -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() diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 2253596ac3e2..8a1537350ca7 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -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(): diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index 020a26e31cbe..993f9b0aeb6e 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -255,10 +255,6 @@ def _run_checked_subprocess(cls, command, tex, *, cwd=None): report = subprocess.check_output( command, cwd=cwd if cwd is not None else cls._texcache, stderr=subprocess.STDOUT) - except FileNotFoundError as exc: - raise RuntimeError( - f'Failed to process string with tex because {command[0]} ' - 'could not be found') from exc except subprocess.CalledProcessError as exc: raise RuntimeError( '{prog} was not able to process the following string:\n' @@ -271,6 +267,10 @@ def _run_checked_subprocess(cls, command, tex, *, cwd=None): tex=tex.encode('unicode_escape'), exc=exc.output.decode('utf-8', 'backslashreplace')) ) from None + except (FileNotFoundError, OSError) as exc: + raise RuntimeError( + f'Failed to process string with tex because {command[0]} ' + 'could not be found') from exc _log.debug(report) return report From bdbeb32122f13e8ef93cfc7f3b0e1c6c74431e3a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 6 Nov 2024 05:20:05 -0500 Subject: [PATCH 07/11] Skip tests that expect LinAlgError on WASM --- lib/matplotlib/tests/test_mlab.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/matplotlib/tests/test_mlab.py b/lib/matplotlib/tests/test_mlab.py index 109a6d542450..82f877b4cc01 100644 --- a/lib/matplotlib/tests/test_mlab.py +++ b/lib/matplotlib/tests/test_mlab.py @@ -884,6 +884,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]]) @@ -897,6 +899,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]]) From 3a1a61e3b374339d3039c4193ff06b208a3faf14 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 6 Nov 2024 00:25:44 -0500 Subject: [PATCH 08/11] ci: Add a build for wasm This adds a `pyproject.toml` config for it, so you can replicate locally with cibuildwheel. --- .github/labeler.yml | 4 ++- .github/workflows/nightlies.yml | 40 +++++++++++------------ .github/workflows/wasm.yml | 58 +++++++++++++++++++++++++++++++++ pyproject.toml | 20 ++++++++++++ 4 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/wasm.yml diff --git a/.github/labeler.yml b/.github/labeler.yml index 75adfed57f43..77b79146b47f 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -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'] diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 393ce2e73472..b534aa3ef5b0 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -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/ diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml new file mode 100644 index 000000000000..38872d6439d0 --- /dev/null +++ b/.github/workflows/wasm.yml @@ -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 diff --git a/pyproject.toml b/pyproject.toml index b580feff930e..64393eb51358 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -319,3 +319,23 @@ testpaths = ["lib"] addopts = [ "--import-mode=importlib", ] + +[tool.cibuildwheel.pyodide] +test-requires = "pytest" +test-command = [ + # Wheels are built without test images, so copy them into the testing directory. + "basedir=$(python -c 'import pathlib, matplotlib; print(pathlib.Path(matplotlib.__file__).parent.parent)')", + "cp -a {package}/lib/matplotlib/tests/data $basedir/matplotlib/tests/", + """ + for subdir in matplotlib mpl_toolkits/axes_grid1 mpl_toolkits/axisartist mpl_toolkits/mplot3d; do + cp -a {package}/lib/${subdir}/tests/baseline_images $basedir/${subdir}/tests/ + done""", + # Test installed, not repository, copy as we aren't using an editable install. + "pytest -p no:cacheprovider --pyargs matplotlib mpl_toolkits.axes_grid1 mpl_toolkits.axisartist mpl_toolkits.mplot3d", +] +[tool.cibuildwheel.pyodide.environment] +# Exceptions are needed for pybind11: +# https://github.com/pybind/pybind11/pull/5298 +CFLAGS = "-fexceptions" +CXXFLAGS = "-fexceptions" +LDFLAGS = "-fexceptions" From 7b48eef9f6c987c21c57c7dd1bd91637f80cc2d3 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 6 Nov 2024 01:20:52 -0500 Subject: [PATCH 09/11] Open symbol visibility for FreeType on wasm --- subprojects/packagefiles/freetype-2.6.1-meson/meson.build | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/subprojects/packagefiles/freetype-2.6.1-meson/meson.build b/subprojects/packagefiles/freetype-2.6.1-meson/meson.build index 9a5180ef7586..1fd4bc44e7b5 100644 --- a/subprojects/packagefiles/freetype-2.6.1-meson/meson.build +++ b/subprojects/packagefiles/freetype-2.6.1-meson/meson.build @@ -179,11 +179,17 @@ ft_config_headers += [configure_file(input: 'include/freetype/config/ftoption.h. output: 'ftoption.h', configuration: conf)] +if cc.get_id() == 'emscripten' + kwargs = {} +else + kwargs = {'gnu_symbol_visibility': 'inlineshidden'} +endif + libfreetype = static_library('freetype', base_sources, include_directories: incbase, dependencies: deps, c_args: c_args, - gnu_symbol_visibility: 'inlineshidden', + kwargs: kwargs ) freetype_dep = declare_dependency( From e35fc24185a27fa90fb6af7b3c0579443daf8c48 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 7 Nov 2024 18:53:55 -0500 Subject: [PATCH 10/11] TST: Avoid using os.devnull for path tests On wasm, this file doesn't support seeking, which is sometimes necessary depending on file type. --- lib/matplotlib/tests/test_backend_pdf.py | 5 ++--- lib/matplotlib/tests/test_image.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index f126fb543e78..a3e9895d8e4d 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -1,7 +1,6 @@ import datetime import decimal import io -import os from pathlib import Path import numpy as np @@ -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()) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 00c223c59362..e3d425ba3460 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -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(): From 5eb8a4249c8cfc821dd65430e1f35606df086cc1 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 16 May 2025 19:30:26 -0400 Subject: [PATCH 11/11] Increase some tolerances for wasm --- lib/matplotlib/tests/test_axes.py | 4 ++-- lib/matplotlib/tests/test_legend.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index c96173e340f7..495f773afa69 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -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], @@ -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 diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 9b100037cc41..c18b52563042 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -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'],