From 3fd67530516dcaa8ead0dc7bb2e29307b7a4fad5 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Wed, 4 Feb 2026 17:29:30 +0000 Subject: [PATCH 01/15] CI: make tests verbose [skip doc] [skip appveyor] --- .github/workflows/tests.yml | 30 +----------------------------- azure-pipelines.yml | 2 +- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6f79beae1bd5..e22bc4a86009 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -47,34 +47,6 @@ jobs: fail-fast: false matrix: include: - - name-suffix: "(Minimum Versions)" - os: ubuntu-22.04 - python-version: '3.11' - extra-requirements: '-c requirements/testing/minver.txt' - delete-font-cache: true - # https://github.com/matplotlib/matplotlib/issues/29844 - pygobject-ver: '<3.52.0' - - os: ubuntu-22.04 - python-version: '3.11' - CFLAGS: "-fno-lto" # Ensure that disabling LTO works. - extra-requirements: '-r requirements/testing/extra.txt' - # https://github.com/matplotlib/matplotlib/issues/29844 - pygobject-ver: '<3.52.0' - - name-suffix: "(Extra TeX packages)" - os: ubuntu-22.04 - python-version: '3.13' - extra-packages: 'texlive-fonts-extra texlive-lang-cyrillic' - # https://github.com/matplotlib/matplotlib/issues/29844 - pygobject-ver: '<3.52.0' - - name-suffix: "Free-threaded" - os: ubuntu-22.04 - python-version: '3.13t' - # https://github.com/matplotlib/matplotlib/issues/29844 - pygobject-ver: '<3.52.0' - - os: ubuntu-24.04 - python-version: '3.12' - - os: ubuntu-24.04 - python-version: '3.14' - os: ubuntu-24.04-arm python-version: '3.12' - os: macos-14 # This runner is on M1 (arm64) chips. @@ -336,7 +308,7 @@ jobs: if [[ "${{ matrix.python-version }}" == '3.13t' ]]; then export PYTHON_GIL=0 fi - pytest -rfEsXR -n auto \ + pytest -vrfEsXR -n auto \ --maxfail=50 --timeout=300 --durations=25 \ --cov-report=xml --cov=lib --log-level=DEBUG --color=yes diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d68a9d36f0d3..5eb69258b671 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -120,7 +120,7 @@ stages: echo "##vso[task.setvariable variable=VS_COVERAGE_TOOL]$TOOL" - PYTHONFAULTHANDLER=1 pytest -rfEsXR -n 2 \ + PYTHONFAULTHANDLER=1 pytest -vrfEsXR -n 2 \ --maxfail=50 --timeout=300 --durations=25 \ --junitxml=junit/test-results.xml --cov-report=xml --cov=lib From 775c698ed2f9347089a2289fa613570c3836bca0 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:13:28 +0000 Subject: [PATCH 02/15] skip the streamplot test [skip doc] [skip appveyor] --- lib/matplotlib/tests/test_streamplot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/tests/test_streamplot.py b/lib/matplotlib/tests/test_streamplot.py index 697ee527f253..8ea9ba9ea785 100644 --- a/lib/matplotlib/tests/test_streamplot.py +++ b/lib/matplotlib/tests/test_streamplot.py @@ -100,6 +100,7 @@ def test_direction(): linewidth=2, density=2) +@pytest.mark.skip(reason='Eliminating test from enquiries') @image_comparison(['streamplot_integration.png'], style='mpl20', tol=0.05) def test_integration_options(): # Linear potential flow over a lifting cylinder From efc6e4969a8ec633d3d19dcded4ddcb36deb0af2 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:44:05 +0000 Subject: [PATCH 03/15] run with single worker [skip doc] [skip appveyor] --- .github/workflows/tests.yml | 2 +- azure-pipelines.yml | 2 +- lib/matplotlib/tests/test_streamplot.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e22bc4a86009..c97be0202510 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -308,7 +308,7 @@ jobs: if [[ "${{ matrix.python-version }}" == '3.13t' ]]; then export PYTHON_GIL=0 fi - pytest -vrfEsXR -n auto \ + pytest -vrfEsXR \ --maxfail=50 --timeout=300 --durations=25 \ --cov-report=xml --cov=lib --log-level=DEBUG --color=yes diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5eb69258b671..1e39c0f12156 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -120,7 +120,7 @@ stages: echo "##vso[task.setvariable variable=VS_COVERAGE_TOOL]$TOOL" - PYTHONFAULTHANDLER=1 pytest -vrfEsXR -n 2 \ + PYTHONFAULTHANDLER=1 pytest -vrfEsXR \ --maxfail=50 --timeout=300 --durations=25 \ --junitxml=junit/test-results.xml --cov-report=xml --cov=lib diff --git a/lib/matplotlib/tests/test_streamplot.py b/lib/matplotlib/tests/test_streamplot.py index 8ea9ba9ea785..ca7843f72b58 100644 --- a/lib/matplotlib/tests/test_streamplot.py +++ b/lib/matplotlib/tests/test_streamplot.py @@ -100,7 +100,7 @@ def test_direction(): linewidth=2, density=2) -@pytest.mark.skip(reason='Eliminating test from enquiries') +# @pytest.mark.skip(reason='Eliminating test from enquiries') @image_comparison(['streamplot_integration.png'], style='mpl20', tol=0.05) def test_integration_options(): # Linear potential flow over a lifting cylinder From 23b376ca513ff0c18e9175f54803400baa30e1b8 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:19:06 +0000 Subject: [PATCH 04/15] try GHA with two workers [skip doc] [skip appveyor] --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c97be0202510..7e95dc3229dd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -308,7 +308,7 @@ jobs: if [[ "${{ matrix.python-version }}" == '3.13t' ]]; then export PYTHON_GIL=0 fi - pytest -vrfEsXR \ + pytest -vrfEsXR -n 2 \ --maxfail=50 --timeout=300 --durations=25 \ --cov-report=xml --cov=lib --log-level=DEBUG --color=yes From 824f6649dd20dab9c0eb08f529c667242fa68a0d Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Fri, 6 Feb 2026 22:35:23 +0000 Subject: [PATCH 05/15] back to two workers on Azure [skip doc] [skip appveyor] --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1e39c0f12156..5eb69258b671 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -120,7 +120,7 @@ stages: echo "##vso[task.setvariable variable=VS_COVERAGE_TOOL]$TOOL" - PYTHONFAULTHANDLER=1 pytest -vrfEsXR \ + PYTHONFAULTHANDLER=1 pytest -vrfEsXR -n 2 \ --maxfail=50 --timeout=300 --durations=25 \ --junitxml=junit/test-results.xml --cov-report=xml --cov=lib From 2bbbc02ef4a24545bd314bffc7499b1324c7bb88 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 7 Feb 2026 12:53:02 +0000 Subject: [PATCH 06/15] Try delaying re-run [skip doc] [skip appveyor] --- lib/matplotlib/tests/test_backend_ps.py | 2 +- lib/matplotlib/tests/test_backend_tk.py | 4 ++-- lib/matplotlib/tests/test_backends_interactive.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index 9859a286e5fd..9d23550fe25e 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -19,7 +19,7 @@ # This tests tends to hit a TeX cache lock on AppVeyor. -@pytest.mark.flaky(reruns=3) +@pytest.mark.flaky(reruns=3, reruns_delay=10) @pytest.mark.parametrize('papersize', ['letter', 'figure']) @pytest.mark.parametrize('orientation', ['portrait', 'landscape']) @pytest.mark.parametrize('format, use_log, rcParams', [ diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index 1f96ad1308cb..2aee605374d0 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -129,7 +129,7 @@ def legitimate_quit(): @pytest.mark.skipif(platform.python_implementation() != 'CPython', reason='PyPy does not support Tkinter threading: ' 'https://foss.heptapod.net/pypy/pypy/-/issues/1929') -@pytest.mark.flaky(reruns=3) +@pytest.mark.flaky(reruns=3, reruns_delay=10) @_isolated_tk_test(success_count=1) def test_figuremanager_cleans_own_mainloop(): import tkinter @@ -156,7 +156,7 @@ def target(): thread.join() -@pytest.mark.flaky(reruns=3) +@pytest.mark.flaky(reruns=3, reruns_delay=10) @_isolated_tk_test(success_count=0) def test_never_update(): import tkinter diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 188983816372..79e51966e440 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -240,7 +240,7 @@ def check_alt_backend(alt_backend): @pytest.mark.parametrize("env", _get_testable_interactive_backends()) @pytest.mark.parametrize("toolbar", ["toolbar2", "toolmanager"]) -@pytest.mark.flaky(reruns=_retry_count) +@pytest.mark.flaky(reruns=_retry_count, reruns_delay=10) def test_interactive_backend(env, toolbar): if env["MPLBACKEND"] == "macosx": if toolbar == "toolmanager": @@ -335,7 +335,7 @@ def _test_thread_impl(): @pytest.mark.parametrize("env", _thread_safe_backends) -@pytest.mark.flaky(reruns=_retry_count) +@pytest.mark.flaky(reruns=_retry_count, reruns_delay=10) def test_interactive_thread_safety(env): proc = _run_helper(_test_thread_impl, timeout=_test_timeout, extra_env=env) assert proc.stdout.count("CloseEvent") == 1 @@ -623,7 +623,7 @@ def _test_number_of_draws_script(): @pytest.mark.parametrize("env", _blit_backends) # subprocesses can struggle to get the display, so rerun a few times -@pytest.mark.flaky(reruns=_retry_count) +@pytest.mark.flaky(reruns=_retry_count, reruns_delay=10) def test_blitting_events(env): proc = _run_helper( _test_number_of_draws_script, timeout=_test_timeout, extra_env=env) From 2f03bf6f125873b2b2d82426fd891e1280ea7658 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 7 Feb 2026 20:05:34 +0000 Subject: [PATCH 07/15] add __del__ method to classes that use Popen --- lib/matplotlib/animation.py | 12 ++++++++++++ lib/matplotlib/animation.pyi | 1 + 2 files changed, 13 insertions(+) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index be69dce4e927..064415b6a028 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -290,6 +290,7 @@ def __init__(self, fps=5, codec=None, bitrate=None, extra_args=None, bitrate=bitrate) self.frame_format = self.supported_formats[0] self.extra_args = extra_args + self._proc = None def _adjust_frame_size(self): if self.codec == 'h264': @@ -371,6 +372,16 @@ def isAvailable(cls): """Return whether a MovieWriter subclass is actually available.""" return shutil.which(cls.bin_path()) is not None + def __del__(self): + if self._proc: + self._proc.kill() + self._proc.wait() + for stream in filter(None, [self._proc.stdin, + self._proc.stdout, + self._proc.stderr]): + stream.close() + self._proc = None + class FileMovieWriter(MovieWriter): """ @@ -421,6 +432,7 @@ def setup(self, fig, outfile, dpi=None, frame_prefix=None): self.fname_format_str = '%s%%07d.%s' def __del__(self): + super().__del__() if hasattr(self, '_tmpdir') and self._tmpdir: self._tmpdir.cleanup() diff --git a/lib/matplotlib/animation.pyi b/lib/matplotlib/animation.pyi index e90a0103aefd..0ef14607c6d8 100644 --- a/lib/matplotlib/animation.pyi +++ b/lib/matplotlib/animation.pyi @@ -72,6 +72,7 @@ class MovieWriter(AbstractMovieWriter): def bin_path(cls) -> str: ... @classmethod def isAvailable(cls) -> bool: ... + def __del__(self) -> None: ... class FileMovieWriter(MovieWriter): fig: Figure From 3562731fe35e113edef3ec6cc6fa38fd85dd64bd Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:24:18 +0000 Subject: [PATCH 08/15] try garbage collecting after every test [skip doc] [skip appveyor] --- lib/matplotlib/testing/conftest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index 6f87d9826cc3..91a81f44172e 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -1,4 +1,6 @@ import os +import gc +import pytest import sys import pytest @@ -81,6 +83,7 @@ def mpl_test_settings(request): if backend is not None: plt.close("all") matplotlib.use(prev_backend) + gc.collect() @pytest.fixture From df6a37c8b7076ca2f3a62b1564e2d11f86a516a3 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:27:33 +0000 Subject: [PATCH 09/15] eliminate _LuatexKpsewhich from enquiries [skip doc] [skip appveyor] --- lib/matplotlib/dviread.py | 33 ++------------------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index f07157a63524..1c53214cd25c 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -23,10 +23,9 @@ import os import re import struct -import subprocess import sys from collections import namedtuple -from functools import cache, cached_property, lru_cache, partial, wraps +from functools import cached_property, lru_cache, partial, wraps from pathlib import Path import fontTools.agl @@ -1244,31 +1243,6 @@ def _parse_enc(path): raise ValueError(f"Failed to parse {path} as Postscript encoding") -class _LuatexKpsewhich: - @cache # A singleton. - def __new__(cls): - self = object.__new__(cls) - self._proc = self._new_proc() - return self - - def _new_proc(self): - return subprocess.Popen( - ["luatex", "--luaonly", str(cbook._get_data_path("kpsewhich.lua"))], - # mktexpk logs to stderr; suppress that. - stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, - # Store generated pk fonts in our own cache. - env={"MT_VARTEXFONTS": str(Path(mpl.get_cachedir(), "vartexfonts")), - **os.environ}) - - def search(self, filename): - if self._proc.poll() is not None: # Dead, restart it. - self._proc = self._new_proc() - self._proc.stdin.write(os.fsencode(filename) + b"\n") - self._proc.stdin.flush() - out = self._proc.stdout.readline().rstrip() - return None if out == b"nil" else os.fsdecode(out) - - @lru_cache def find_tex_file(filename): """ @@ -1295,10 +1269,7 @@ def find_tex_file(filename): if isinstance(filename, bytes): filename = filename.decode('utf-8', errors='replace') - try: - lk = _LuatexKpsewhich() - except FileNotFoundError: - lk = None # Fallback to directly calling kpsewhich, as below. + lk = None # Fallback to directly calling kpsewhich, as below. if lk: path = lk.search(filename) From e208058655733880b88046bd045ebf35fa22636f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jouni=20K=2E=20Sepp=C3=A4nen?= Date: Sat, 5 Jul 2025 10:56:42 +0300 Subject: [PATCH 10/15] Only run tests that start subprocesses [skip doc] [skip appveyor] --- .github/workflows/tests.yml | 2 +- azure-pipelines.yml | 2 +- lib/matplotlib/testing/_markers.py | 1 + lib/matplotlib/testing/conftest.py | 1 + lib/matplotlib/tests/test_backend_inline.py | 2 ++ lib/matplotlib/tests/test_backend_nbagg.py | 2 ++ lib/matplotlib/tests/test_backend_webagg.py | 2 ++ lib/matplotlib/tests/test_backends_interactive.py | 4 ++++ lib/matplotlib/tests/test_basic.py | 2 ++ lib/matplotlib/tests/test_determinism.py | 6 +++++- lib/matplotlib/tests/test_font_manager.py | 2 ++ lib/matplotlib/tests/test_matplotlib.py | 3 +++ lib/matplotlib/tests/test_preprocess_data.py | 3 +++ lib/matplotlib/tests/test_pyplot.py | 3 +++ lib/matplotlib/tests/test_rcparams.py | 4 ++++ lib/matplotlib/tests/test_sphinxext.py | 3 +++ lib/matplotlib/tests/test_texmanager.py | 2 ++ 17 files changed, 41 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7e95dc3229dd..f575e9c536c7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -308,7 +308,7 @@ jobs: if [[ "${{ matrix.python-version }}" == '3.13t' ]]; then export PYTHON_GIL=0 fi - pytest -vrfEsXR -n 2 \ + pytest -vrfEsXR -n 2 -m starts_subprocess\ --maxfail=50 --timeout=300 --durations=25 \ --cov-report=xml --cov=lib --log-level=DEBUG --color=yes diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5eb69258b671..0c57070953a6 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -120,7 +120,7 @@ stages: echo "##vso[task.setvariable variable=VS_COVERAGE_TOOL]$TOOL" - PYTHONFAULTHANDLER=1 pytest -vrfEsXR -n 2 \ + PYTHONFAULTHANDLER=1 pytest -vrfEsXR -n 2 -m starts_subprocess \ --maxfail=50 --timeout=300 --durations=25 \ --junitxml=junit/test-results.xml --cov-report=xml --cov=lib diff --git a/lib/matplotlib/testing/_markers.py b/lib/matplotlib/testing/_markers.py index c7ef8687a8b3..c8795ef8ee90 100644 --- a/lib/matplotlib/testing/_markers.py +++ b/lib/matplotlib/testing/_markers.py @@ -47,3 +47,4 @@ def _checkdep_usetex() -> bool: needs_usetex = pytest.mark.skipif( not _checkdep_usetex(), reason="This test needs a TeX installation") +starts_subprocess = pytest.mark.starts_subprocess diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index 91a81f44172e..84c8983a711c 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -20,6 +20,7 @@ def pytest_configure(config): ("markers", "backend: Set alternate Matplotlib backend temporarily."), ("markers", "baseline_images: Compare output against references."), ("markers", "pytz: Tests that require pytz to be installed."), + ("markers", "starts_subprocess: Tests that start subprocesses."), ("filterwarnings", "error"), ("filterwarnings", "ignore:.*The py23 module has been deprecated:DeprecationWarning"), diff --git a/lib/matplotlib/tests/test_backend_inline.py b/lib/matplotlib/tests/test_backend_inline.py index 997e1e7186b1..70e68ac2fb43 100644 --- a/lib/matplotlib/tests/test_backend_inline.py +++ b/lib/matplotlib/tests/test_backend_inline.py @@ -5,11 +5,13 @@ import pytest from matplotlib.testing import subprocess_run_for_testing +from matplotlib.testing._markers import starts_subprocess nbformat = pytest.importorskip('nbformat') pytest.importorskip('nbconvert') pytest.importorskip('ipykernel') pytest.importorskip('matplotlib_inline') +pytestmark = starts_subprocess def test_ipynb(): diff --git a/lib/matplotlib/tests/test_backend_nbagg.py b/lib/matplotlib/tests/test_backend_nbagg.py index ccf74df20aab..eaf711502c44 100644 --- a/lib/matplotlib/tests/test_backend_nbagg.py +++ b/lib/matplotlib/tests/test_backend_nbagg.py @@ -3,12 +3,14 @@ from tempfile import TemporaryDirectory import pytest +from matplotlib.testing._markers import starts_subprocess from matplotlib.testing import subprocess_run_for_testing nbformat = pytest.importorskip('nbformat') pytest.importorskip('nbconvert') pytest.importorskip('ipykernel') +pytestmark = starts_subprocess # From https://blog.thedataincubator.com/2016/06/testing-jupyter-notebooks/ diff --git a/lib/matplotlib/tests/test_backend_webagg.py b/lib/matplotlib/tests/test_backend_webagg.py index 1d6769494ef9..50100883747d 100644 --- a/lib/matplotlib/tests/test_backend_webagg.py +++ b/lib/matplotlib/tests/test_backend_webagg.py @@ -3,9 +3,11 @@ import pytest import matplotlib.backends.backend_webagg_core +from matplotlib.testing._markers import starts_subprocess from matplotlib.testing import subprocess_run_for_testing +@starts_subprocess @pytest.mark.parametrize("backend", ["webagg", "nbagg"]) def test_webagg_fallback(backend): pytest.importorskip("tornado") diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 79e51966e440..6344d62e2df8 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -20,6 +20,10 @@ from matplotlib import _c_internal_utils from matplotlib.backend_tools import ToolToggleBase from matplotlib.testing import subprocess_run_helper as _run_helper, is_ci_environment +from matplotlib.testing._markers import starts_subprocess + + +pytestmark = starts_subprocess class _WaitForStringPopen(subprocess.Popen): diff --git a/lib/matplotlib/tests/test_basic.py b/lib/matplotlib/tests/test_basic.py index f6aa1e458555..976fba23c0ef 100644 --- a/lib/matplotlib/tests/test_basic.py +++ b/lib/matplotlib/tests/test_basic.py @@ -3,6 +3,7 @@ import sys import textwrap +from matplotlib.testing._markers import starts_subprocess from matplotlib.testing import subprocess_run_for_testing @@ -28,6 +29,7 @@ def test_override_builtins(): assert overridden <= ok_to_override +@starts_subprocess def test_lazy_imports(): source = textwrap.dedent(""" import sys diff --git a/lib/matplotlib/tests/test_determinism.py b/lib/matplotlib/tests/test_determinism.py index c0e4adbef40b..9210cddc54a0 100644 --- a/lib/matplotlib/tests/test_determinism.py +++ b/lib/matplotlib/tests/test_determinism.py @@ -16,11 +16,15 @@ from matplotlib.patches import Circle, PathPatch from matplotlib.path import Path from matplotlib.testing import subprocess_run_for_testing -from matplotlib.testing._markers import needs_ghostscript, needs_usetex +from matplotlib.testing._markers import ( + needs_ghostscript, needs_usetex, starts_subprocess +) import matplotlib.testing.compare from matplotlib.text import TextPath from matplotlib.transforms import IdentityTransform +pytestmark = starts_subprocess + def _save_figure(objects='mhip', fmt="pdf", usetex=False): mpl.use(fmt) diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 1bca00e072a3..0d6f40d30801 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -19,6 +19,7 @@ _get_fontconfig_fonts, _normalize_weight) from matplotlib import cbook, ft2font, pyplot as plt, rc_context, figure as mfigure from matplotlib.testing import subprocess_run_helper, subprocess_run_for_testing +from matplotlib.testing._markers import starts_subprocess has_fclist = shutil.which('fc-list') is not None @@ -289,6 +290,7 @@ def test_fontcache_thread_safe(): subprocess_run_helper(_test_threading, timeout=10) +@starts_subprocess def test_lockfilefailure(tmp_path): # The logic here: # 1. get a temp directory from pytest diff --git a/lib/matplotlib/tests/test_matplotlib.py b/lib/matplotlib/tests/test_matplotlib.py index d0a3f8c617e1..911ccf65d20e 100644 --- a/lib/matplotlib/tests/test_matplotlib.py +++ b/lib/matplotlib/tests/test_matplotlib.py @@ -7,6 +7,7 @@ import matplotlib from matplotlib.testing import subprocess_run_for_testing +from matplotlib.testing._markers import starts_subprocess @pytest.mark.parametrize('version_str, version_tuple', [ @@ -19,6 +20,7 @@ def test_parse_to_version_info(version_str, version_tuple): assert matplotlib._parse_to_version_info(version_str) == version_tuple +@starts_subprocess @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, @@ -37,6 +39,7 @@ def test_tmpconfigdir_warning(tmp_path): os.chmod(tmp_path, mode) +@starts_subprocess def test_importable_with_no_home(tmp_path): subprocess_run_for_testing( [sys.executable, "-c", diff --git a/lib/matplotlib/tests/test_preprocess_data.py b/lib/matplotlib/tests/test_preprocess_data.py index c983d78786e1..44db2733f10d 100644 --- a/lib/matplotlib/tests/test_preprocess_data.py +++ b/lib/matplotlib/tests/test_preprocess_data.py @@ -8,6 +8,8 @@ from matplotlib.axes import Axes from matplotlib.testing import subprocess_run_for_testing from matplotlib.testing.decorators import check_figures_equal +from matplotlib.testing._markers import starts_subprocess + # Notes on testing the plotting functions itself # * the individual decorated plotting functions are tested in 'test_axes.py' @@ -245,6 +247,7 @@ def funcy(ax, x, y, z, t=None): funcy.__doc__) +@starts_subprocess def test_data_parameter_replacement(): """ Test that the docstring contains the correct *data* parameter stub diff --git a/lib/matplotlib/tests/test_pyplot.py b/lib/matplotlib/tests/test_pyplot.py index 1cca7332aa0c..7d3921dbbef7 100644 --- a/lib/matplotlib/tests/test_pyplot.py +++ b/lib/matplotlib/tests/test_pyplot.py @@ -11,7 +11,10 @@ from matplotlib.testing import subprocess_run_for_testing from matplotlib import pyplot as plt +from matplotlib.testing._markers import starts_subprocess + +@starts_subprocess def test_pyplot_up_to_date(tmp_path): pytest.importorskip("black", minversion="24.1") diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 525a9ff60d1a..61ff9ced44a6 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -32,6 +32,7 @@ _validate_linestyle, _listify_validator) from matplotlib.testing import subprocess_run_for_testing +from matplotlib.testing._markers import starts_subprocess def test_rcparams(tmp_path): @@ -529,6 +530,7 @@ def test_rcparams_reset_after_fail(): assert mpl.rcParams['text.usetex'] is False +@starts_subprocess @pytest.mark.skipif(sys.platform != "linux", reason="Linux only") def test_backend_fallback_headless_invalid_backend(tmp_path): env = {**os.environ, @@ -546,6 +548,7 @@ def test_backend_fallback_headless_invalid_backend(tmp_path): env=env, check=True, stderr=subprocess.DEVNULL) +@starts_subprocess @pytest.mark.skipif(sys.platform != "linux", reason="Linux only") def test_backend_fallback_headless_auto_backend(tmp_path): # specify a headless mpl environment, but request a graphical (tk) backend @@ -568,6 +571,7 @@ def test_backend_fallback_headless_auto_backend(tmp_path): assert backend.strip().lower() == "agg" +@starts_subprocess @pytest.mark.skipif( sys.platform == "linux" and not _c_internal_utils.xdisplay_is_valid(), reason="headless") diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index c6f4e13c74c2..91d436c52e42 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -7,6 +7,8 @@ import sys from matplotlib.testing import subprocess_run_for_testing +from matplotlib.testing._markers import starts_subprocess + import pytest @@ -16,6 +18,7 @@ tinypages = Path(__file__).parent / 'data/tinypages' +@starts_subprocess def build_sphinx_html(source_dir, doctree_dir, html_dir, extra_args=None): # Build the pages with warnings turned into errors extra_args = [] if extra_args is None else extra_args diff --git a/lib/matplotlib/tests/test_texmanager.py b/lib/matplotlib/tests/test_texmanager.py index 7e7b5632c7ac..4f957247bdad 100644 --- a/lib/matplotlib/tests/test_texmanager.py +++ b/lib/matplotlib/tests/test_texmanager.py @@ -9,6 +9,7 @@ from matplotlib.testing import subprocess_run_for_testing from matplotlib.testing._markers import needs_usetex from matplotlib.texmanager import TexManager +from matplotlib.testing._markers import starts_subprocess def test_fontconfig_preamble(): @@ -63,6 +64,7 @@ def test_unicode_characters(): fig.canvas.draw() +@starts_subprocess @needs_usetex def test_openin_any_paranoid(): completed = subprocess_run_for_testing( From 78535fdce98e554d94716109b7e77e7f76deb752 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:38:53 +0000 Subject: [PATCH 11/15] Revert "add __del__ method to classes that use Popen" This reverts commit 62be2154a5ce0841f727dad6572e5dce71e4b4b2. --- lib/matplotlib/animation.py | 12 ------------ lib/matplotlib/animation.pyi | 1 - 2 files changed, 13 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 064415b6a028..be69dce4e927 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -290,7 +290,6 @@ def __init__(self, fps=5, codec=None, bitrate=None, extra_args=None, bitrate=bitrate) self.frame_format = self.supported_formats[0] self.extra_args = extra_args - self._proc = None def _adjust_frame_size(self): if self.codec == 'h264': @@ -372,16 +371,6 @@ def isAvailable(cls): """Return whether a MovieWriter subclass is actually available.""" return shutil.which(cls.bin_path()) is not None - def __del__(self): - if self._proc: - self._proc.kill() - self._proc.wait() - for stream in filter(None, [self._proc.stdin, - self._proc.stdout, - self._proc.stderr]): - stream.close() - self._proc = None - class FileMovieWriter(MovieWriter): """ @@ -432,7 +421,6 @@ def setup(self, fig, outfile, dpi=None, frame_prefix=None): self.fname_format_str = '%s%%07d.%s' def __del__(self): - super().__del__() if hasattr(self, '_tmpdir') and self._tmpdir: self._tmpdir.cleanup() diff --git a/lib/matplotlib/animation.pyi b/lib/matplotlib/animation.pyi index 0ef14607c6d8..e90a0103aefd 100644 --- a/lib/matplotlib/animation.pyi +++ b/lib/matplotlib/animation.pyi @@ -72,7 +72,6 @@ class MovieWriter(AbstractMovieWriter): def bin_path(cls) -> str: ... @classmethod def isAvailable(cls) -> bool: ... - def __del__(self) -> None: ... class FileMovieWriter(MovieWriter): fig: Figure From fcee466927c6bef45e220081bbc3629f91b65048 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:40:31 +0000 Subject: [PATCH 12/15] skip sigint tests [skip doc] [skip appveyor] --- lib/matplotlib/tests/test_backends_interactive.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 6344d62e2df8..41edb3f63b03 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -729,6 +729,7 @@ def interrupter(): print('SUCCESS', flush=True) +@pytest.mark.skip(reason='eliminate test from enquiries') @pytest.mark.parametrize("env", _get_testable_interactive_backends()) @pytest.mark.parametrize("target, kwargs", [ ('show', {'block': True}), @@ -777,6 +778,7 @@ def custom_signal_handler(signum, frame): print('SUCCESS', flush=True) +@pytest.mark.skip(reason='eliminate test from enquiries') @pytest.mark.skipif(sys.platform == 'win32', reason='No other signal available to send on Windows') @pytest.mark.parametrize("env", _get_testable_interactive_backends()) From 1af2a9c8f08c818398ed4e484aacf5886a2054ae Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sun, 8 Feb 2026 21:03:13 +0000 Subject: [PATCH 13/15] only re-run on timeouts [skip doc] [skip appveyor] --- lib/matplotlib/tests/test_backend_ps.py | 2 +- lib/matplotlib/tests/test_backend_tk.py | 4 ++-- lib/matplotlib/tests/test_backends_interactive.py | 9 ++++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index 9d23550fe25e..1efbd4e479f8 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -19,7 +19,7 @@ # This tests tends to hit a TeX cache lock on AppVeyor. -@pytest.mark.flaky(reruns=3, reruns_delay=10) +@pytest.mark.flaky(reruns=3, reruns_delay=10, only_rerun=["subprocess.TimeoutExpired"]) @pytest.mark.parametrize('papersize', ['letter', 'figure']) @pytest.mark.parametrize('orientation', ['portrait', 'landscape']) @pytest.mark.parametrize('format, use_log, rcParams', [ diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index 2aee605374d0..65b78253fca2 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -129,7 +129,7 @@ def legitimate_quit(): @pytest.mark.skipif(platform.python_implementation() != 'CPython', reason='PyPy does not support Tkinter threading: ' 'https://foss.heptapod.net/pypy/pypy/-/issues/1929') -@pytest.mark.flaky(reruns=3, reruns_delay=10) +@pytest.mark.flaky(reruns=3, reruns_delay=10, only_rerun=["subprocess.TimeoutExpired"]) @_isolated_tk_test(success_count=1) def test_figuremanager_cleans_own_mainloop(): import tkinter @@ -156,7 +156,7 @@ def target(): thread.join() -@pytest.mark.flaky(reruns=3, reruns_delay=10) +@pytest.mark.flaky(reruns=3, reruns_delay=10, only_rerun=["subprocess.TimeoutExpired"]) @_isolated_tk_test(success_count=0) def test_never_update(): import tkinter diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 41edb3f63b03..ff704e5fd2bb 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -244,7 +244,8 @@ def check_alt_backend(alt_backend): @pytest.mark.parametrize("env", _get_testable_interactive_backends()) @pytest.mark.parametrize("toolbar", ["toolbar2", "toolmanager"]) -@pytest.mark.flaky(reruns=_retry_count, reruns_delay=10) +@pytest.mark.flaky(reruns=_retry_count, reruns_delay=10, + only_rerun=["subprocess.TimeoutExpired"]) def test_interactive_backend(env, toolbar): if env["MPLBACKEND"] == "macosx": if toolbar == "toolmanager": @@ -339,7 +340,8 @@ def _test_thread_impl(): @pytest.mark.parametrize("env", _thread_safe_backends) -@pytest.mark.flaky(reruns=_retry_count, reruns_delay=10) +@pytest.mark.flaky(reruns=_retry_count, reruns_delay=10, + only_rerun=["subprocess.TimeoutExpired"]) def test_interactive_thread_safety(env): proc = _run_helper(_test_thread_impl, timeout=_test_timeout, extra_env=env) assert proc.stdout.count("CloseEvent") == 1 @@ -627,7 +629,8 @@ def _test_number_of_draws_script(): @pytest.mark.parametrize("env", _blit_backends) # subprocesses can struggle to get the display, so rerun a few times -@pytest.mark.flaky(reruns=_retry_count, reruns_delay=10) +@pytest.mark.flaky(reruns=_retry_count, reruns_delay=10, + only_rerun=["subprocess.TimeoutExpired"]) def test_blitting_events(env): proc = _run_helper( _test_number_of_draws_script, timeout=_test_timeout, extra_env=env) From 7801a23de015ab1ee613c115ecb413845a969607 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sun, 8 Feb 2026 22:15:45 +0000 Subject: [PATCH 14/15] Run all tests on Azure; remove flaky markers [skip doc] [skip appveyor] --- azure-pipelines.yml | 2 +- lib/matplotlib/tests/test_backend_ps.py | 2 -- lib/matplotlib/tests/test_backend_tk.py | 2 -- lib/matplotlib/tests/test_backends_interactive.py | 7 ------- 4 files changed, 1 insertion(+), 12 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0c57070953a6..5eb69258b671 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -120,7 +120,7 @@ stages: echo "##vso[task.setvariable variable=VS_COVERAGE_TOOL]$TOOL" - PYTHONFAULTHANDLER=1 pytest -vrfEsXR -n 2 -m starts_subprocess \ + PYTHONFAULTHANDLER=1 pytest -vrfEsXR -n 2 \ --maxfail=50 --timeout=300 --durations=25 \ --junitxml=junit/test-results.xml --cov-report=xml --cov=lib diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index 1efbd4e479f8..dd811ae3f8f5 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -18,8 +18,6 @@ import matplotlib.pyplot as plt -# This tests tends to hit a TeX cache lock on AppVeyor. -@pytest.mark.flaky(reruns=3, reruns_delay=10, only_rerun=["subprocess.TimeoutExpired"]) @pytest.mark.parametrize('papersize', ['letter', 'figure']) @pytest.mark.parametrize('orientation', ['portrait', 'landscape']) @pytest.mark.parametrize('format, use_log, rcParams', [ diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index 65b78253fca2..54d90cd09644 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -129,7 +129,6 @@ def legitimate_quit(): @pytest.mark.skipif(platform.python_implementation() != 'CPython', reason='PyPy does not support Tkinter threading: ' 'https://foss.heptapod.net/pypy/pypy/-/issues/1929') -@pytest.mark.flaky(reruns=3, reruns_delay=10, only_rerun=["subprocess.TimeoutExpired"]) @_isolated_tk_test(success_count=1) def test_figuremanager_cleans_own_mainloop(): import tkinter @@ -156,7 +155,6 @@ def target(): thread.join() -@pytest.mark.flaky(reruns=3, reruns_delay=10, only_rerun=["subprocess.TimeoutExpired"]) @_isolated_tk_test(success_count=0) def test_never_update(): import tkinter diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index ff704e5fd2bb..df94de730204 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -244,8 +244,6 @@ def check_alt_backend(alt_backend): @pytest.mark.parametrize("env", _get_testable_interactive_backends()) @pytest.mark.parametrize("toolbar", ["toolbar2", "toolmanager"]) -@pytest.mark.flaky(reruns=_retry_count, reruns_delay=10, - only_rerun=["subprocess.TimeoutExpired"]) def test_interactive_backend(env, toolbar): if env["MPLBACKEND"] == "macosx": if toolbar == "toolmanager": @@ -340,8 +338,6 @@ def _test_thread_impl(): @pytest.mark.parametrize("env", _thread_safe_backends) -@pytest.mark.flaky(reruns=_retry_count, reruns_delay=10, - only_rerun=["subprocess.TimeoutExpired"]) def test_interactive_thread_safety(env): proc = _run_helper(_test_thread_impl, timeout=_test_timeout, extra_env=env) assert proc.stdout.count("CloseEvent") == 1 @@ -628,9 +624,6 @@ def _test_number_of_draws_script(): @pytest.mark.parametrize("env", _blit_backends) -# subprocesses can struggle to get the display, so rerun a few times -@pytest.mark.flaky(reruns=_retry_count, reruns_delay=10, - only_rerun=["subprocess.TimeoutExpired"]) def test_blitting_events(env): proc = _run_helper( _test_number_of_draws_script, timeout=_test_timeout, extra_env=env) From 8ea617e1bc77a2b7f6eee933d090c31d73441005 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 28 Feb 2026 16:32:05 +0000 Subject: [PATCH 15/15] post rebase linting fix [skip doc] [skip appveyor] --- lib/matplotlib/testing/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index 84c8983a711c..1bf57894151f 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -1,6 +1,5 @@ import os import gc -import pytest import sys import pytest