From 66a9a9b8178f807202088b4d1ec2fd61ca2ba2dd Mon Sep 17 00:00:00 2001 From: jjscripts Date: Wed, 3 Jun 2026 15:46:36 +0800 Subject: [PATCH 1/4] test: force Agg backend in test session via conftest --- tests/conftest.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..849edd4 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +"""Test-session setup: force a non-interactive matplotlib backend. + +The library deliberately does NOT force a backend (see issue #2). The test +environment is responsible for declaring itself headless, which is what this +does — before matplotlib resolves an interactive backend. +""" + +import os + +# Set before matplotlib is imported anywhere, so it never picks a GUI backend. +os.environ.setdefault("MPLBACKEND", "Agg") + +import matplotlib # noqa: E402 + +matplotlib.use("Agg", force=True) From f97bc6cdaf7e1107472209c7c1cd758baa1c0231 Mon Sep 17 00:00:00 2001 From: jjscripts Date: Wed, 3 Jun 2026 15:50:19 +0800 Subject: [PATCH 2/4] fix: stop forcing matplotlib Agg backend on import (#2) --- entroscope/_core.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/entroscope/_core.py b/entroscope/_core.py index 9146c3e..e43bf43 100644 --- a/entroscope/_core.py +++ b/entroscope/_core.py @@ -3,16 +3,18 @@ Every measure module delegates its standard methods here so the "Series in -> Series out, array in -> array out" contract and the windowing logic live in exactly one place. -""" -import matplotlib +This module does NOT force a matplotlib backend. Plot helpers build a Figure and +return it (never calling ``plt.show()``), so they work under whatever backend the +environment provides. Headless / Docker / CI users who want a guaranteed +non-interactive backend should set ``MPLBACKEND=Agg`` in their environment. +""" -matplotlib.use("Agg") # headless-safe; never opens a window -import matplotlib.pyplot as plt # noqa: E402 -import numpy as np # noqa: E402 -import pandas as pd # noqa: E402 +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd -from .utils.windows import sliding_windows # noqa: E402 +from .utils.windows import sliding_windows def as_array(x): From 5ff282c7c19151f5a5ecd716cdb382dc3c81000d Mon Sep 17 00:00:00 2001 From: jjscripts Date: Wed, 3 Jun 2026 15:52:20 +0800 Subject: [PATCH 3/4] test: regression guard that import does not force a backend (#2) --- tests/test_backend.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/test_backend.py diff --git a/tests/test_backend.py b/tests/test_backend.py new file mode 100644 index 0000000..abfe37f --- /dev/null +++ b/tests/test_backend.py @@ -0,0 +1,30 @@ +"""Regression test for issue #2: importing entroscope must not force a backend.""" + +import subprocess +import sys + + +def test_import_does_not_change_backend(): + # Run in a subprocess with a known non-Agg backend selected via MPLBACKEND. + # If importing entroscope calls matplotlib.use("Agg"), the backend will flip + # to "agg" and the assertion in the child fails. + code = ( + "import matplotlib\n" + "before = matplotlib.get_backend().lower()\n" + "import entroscope\n" + "from entroscope import shannon, transfer, divergence\n" + "after = matplotlib.get_backend().lower()\n" + "assert before == 'template', f'unexpected start backend {before!r}'\n" + "assert after == 'template', f'import changed backend to {after!r}'\n" + "print('backend-stable')\n" + ) + result = subprocess.run( + [sys.executable, "-c", code], + env={"MPLBACKEND": "template", "PATH": __import__("os").environ["PATH"]}, + capture_output=True, + text=True, + ) + assert result.returncode == 0, ( + f"subprocess failed:\nSTDOUT:{result.stdout}\nSTDERR:{result.stderr}" + ) + assert "backend-stable" in result.stdout From fded3ae5adbcc5284f5bde7bc53a5528b7691c6d Mon Sep 17 00:00:00 2001 From: jjscripts Date: Wed, 3 Jun 2026 15:56:01 +0800 Subject: [PATCH 4/4] docs: document MPLBACKEND=Agg for headless users; set it in CI (#2) --- .github/workflows/ci.yml | 2 ++ README.md | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4826fff..dd2dd5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,8 @@ jobs: test: name: Test (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest + env: + MPLBACKEND: Agg strategy: fail-fast: false matrix: diff --git a/README.md b/README.md index 202a24c..4a117a0 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,17 @@ shannon.plot(s, window=20) # a matplotlib Figure Every method accepts a `pd.Series` **or** a `np.ndarray`. Pass a Series and you get a Series back with its index preserved; pass an array and you get an array. +### Headless environments (Docker, CI) + +`entroscope` does not change your matplotlib backend on import, so interactive +plotting in notebooks keeps working. In a headless environment (a Docker +container or CI runner) where you want a guaranteed non-interactive backend, set +the standard environment variable: + +```bash +export MPLBACKEND=Agg # or, in a Dockerfile: ENV MPLBACKEND=Agg +``` + ## The seven measures | Measure | Import | Captures |