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/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/font_manager.py b/lib/matplotlib/font_manager.py index ab6b495631de..81f41a0b8f54 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -291,6 +291,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': @@ -1090,9 +1093,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), @@ -1105,7 +1111,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): """ diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index 904ee5d73db4..b2dbabdf997f 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: @@ -187,7 +192,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_axes.py b/lib/matplotlib/tests/test_axes.py index f03183b20323..8969daede433 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -8289,7 +8289,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], @@ -8307,7 +8307,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_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_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 24421b8e30b3..ef8499c7b26f 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -21,7 +21,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(): @@ -229,6 +229,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/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(): 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'], 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.""" 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]]) 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 diff --git a/pyproject.toml b/pyproject.toml index cf8503a0f3fe..2ac70de5e580 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -329,3 +329,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" 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 }; 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( pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy