From 0930968ec7be6de188a2717bbf42a1aba1d8d533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M=2E=20Requena=20Plens?= Date: Sat, 30 May 2026 11:38:27 +0200 Subject: [PATCH 1/4] fix: do not force matplotlib Agg backend on import Importing the package called matplotlib.use("Agg") at module level, which globally hijacked the backend for the whole Python process and broke interactive use (IPython/Jupyter) and the show=True option. Remove the global backend override and let matplotlib auto-select the appropriate backend (it still falls back to Agg in headless/CI environments, so file saving via plot_file is unaffected). Adds a regression test verifying that importing the package preserves the user's chosen backend. Refs #52 --- src/pyoctaveband/__init__.py | 4 ---- tests/test_matplotlib_backend.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 tests/test_matplotlib_backend.py diff --git a/src/pyoctaveband/__init__.py b/src/pyoctaveband/__init__.py index 422dbdf..0524992 100644 --- a/src/pyoctaveband/__init__.py +++ b/src/pyoctaveband/__init__.py @@ -8,7 +8,6 @@ from typing import List, Tuple, overload, Literal -import matplotlib import numpy as np from .calibration import calculate_sensitivity @@ -17,9 +16,6 @@ from .parametric_filters import WeightingFilter, linkwitz_riley, time_weighting, weighting_filter from ._version import __version__ -# Use non-interactive backend for plots -matplotlib.use("Agg") - # Public methods __all__ = [ "__version__", diff --git a/tests/test_matplotlib_backend.py b/tests/test_matplotlib_backend.py new file mode 100644 index 0000000..dc2c98f --- /dev/null +++ b/tests/test_matplotlib_backend.py @@ -0,0 +1,31 @@ +# Copyright (c) 2026. Jose M. Requena-Plens + +""" +Tests ensuring the package does not hijack the global matplotlib backend. + +Importing pyoctaveband must not force a specific (e.g. non-interactive) +backend, so the package can be used during interactive exploration +(IPython, Jupyter). See issue #52. +""" + +import subprocess +import sys + + +def test_import_does_not_override_matplotlib_backend() -> None: + """Importing pyoctaveband must preserve the user's chosen backend.""" + code = ( + "import matplotlib\n" + # Pick an explicit, always-available backend the user might have set. + "matplotlib.use('svg')\n" + "before = matplotlib.get_backend()\n" + "import pyoctaveband\n" + "after = matplotlib.get_backend()\n" + "assert before == after, f'backend changed: {before!r} -> {after!r}'\n" + ) + result = subprocess.run( + [sys.executable, "-c", code], + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr From 956ab2ce98854fc2541f5d3dd41252b9ba574d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M=2E=20Requena=20Plens?= Date: Sat, 30 May 2026 11:41:01 +0200 Subject: [PATCH 2/4] chore: bump version to 1.2.3 --- src/pyoctaveband/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyoctaveband/_version.py b/src/pyoctaveband/_version.py index bc86c94..10aa336 100644 --- a/src/pyoctaveband/_version.py +++ b/src/pyoctaveband/_version.py @@ -1 +1 @@ -__version__ = "1.2.2" +__version__ = "1.2.3" From 424cd9d224949e6c81a5f1eca15175a7b2361056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M=2E=20Requena=20Plens?= Date: Sat, 30 May 2026 11:46:36 +0200 Subject: [PATCH 3/4] test: harden backend test with PYTHONPATH and timeout Propagate the parent's sys.path to the subprocess so it can import the package when running without an installed build, and add a timeout so a hung import cannot block the suite indefinitely. Addresses review feedback on PR #53. --- tests/test_matplotlib_backend.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_matplotlib_backend.py b/tests/test_matplotlib_backend.py index dc2c98f..af1d823 100644 --- a/tests/test_matplotlib_backend.py +++ b/tests/test_matplotlib_backend.py @@ -8,6 +8,7 @@ (IPython, Jupyter). See issue #52. """ +import os import subprocess import sys @@ -23,9 +24,15 @@ def test_import_does_not_override_matplotlib_backend() -> None: "after = matplotlib.get_backend()\n" "assert before == after, f'backend changed: {before!r} -> {after!r}'\n" ) + # Propagate the parent's sys.path so the subprocess can import the package + # even when it is only on sys.path (e.g. pytest without an installed build). + env = os.environ.copy() + env["PYTHONPATH"] = os.pathsep.join(sys.path) result = subprocess.run( [sys.executable, "-c", code], capture_output=True, text=True, + env=env, + timeout=30, ) assert result.returncode == 0, result.stderr From cba69d3942945aeba6e4e9f9b7564d063f048236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20M=2E=20Requena=20Plens?= Date: Sat, 30 May 2026 12:08:09 +0200 Subject: [PATCH 4/4] test: select Agg backend for the headless test suite Removing the forced backend (issue #52) exposed that the test suite never selected its own matplotlib backend. On Windows runners matplotlib defaults to the interactive TkAgg backend, so plt.subplots() in the plot tests tried to create a Tk window and failed (broken Tcl/Tk on the runner). Linux/macOS happened to fall back to Agg, hiding this. Set MPLBACKEND=Agg in conftest (the consumer opting into a headless backend, mirroring scripts/benchmark_filters.py) instead of forcing it in the library. Uses setdefault so it can still be overridden locally. --- tests/conftest.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 74d3d1a..d737c7c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,13 @@ import os import pytest +# Select a non-interactive matplotlib backend for the headless test suite. +# The library no longer forces a backend (see issue #52), so the test harness +# must opt into Agg itself; otherwise matplotlib picks a GUI backend (e.g. +# TkAgg on Windows runners) and figure creation fails without a display/Tcl. +# Set at import time so it takes effect before any test module imports pyplot. +os.environ.setdefault("MPLBACKEND", "Agg") + def pytest_configure(config): """ Configure environment variables for the test session.