Skip to content

fix: do not force matplotlib Agg backend on import#53

Merged
jmrplens merged 4 commits into
mainfrom
fix/matplotlib-backend-issue-52
May 30, 2026
Merged

fix: do not force matplotlib Agg backend on import#53
jmrplens merged 4 commits into
mainfrom
fix/matplotlib-backend-issue-52

Conversation

@jmrplens
Copy link
Copy Markdown
Owner

@jmrplens jmrplens commented May 30, 2026

Summary

Importing the package called matplotlib.use("Agg") at module level, which globally hijacked the matplotlib backend for the whole Python process. This broke interactive use (IPython / Jupyter) and made the show=True option a no-op.

This PR removes the global backend override and lets matplotlib auto-select the appropriate backend.

Why this is safe

  • Headless / CI: matplotlib automatically falls back to Agg when no display is available, so saving plots via plot_file (plt.savefig) is unaffected. The full suite passes in this headless environment, confirming it.
  • show=True: now actually displays figures in environments with a display (previously suppressed by the forced Agg backend).
  • Interactive use: the user's chosen backend is preserved, fixing the reported issue.

Changes

  • src/pyoctaveband/__init__.py: remove matplotlib.use("Agg") and the now-unused import matplotlib.
  • tests/test_matplotlib_backend.py: regression test verifying that importing the package preserves the user's chosen backend (sets svg in a clean subprocess, imports, asserts it is unchanged).

Tests

All 125 tests pass (including the new regression test).

Refs #52

Summary by Sourcery

Stop forcing a global non-interactive matplotlib backend on import so that the library respects the user’s chosen backend and works correctly in interactive environments.

Bug Fixes:

  • Preserve the user’s configured matplotlib backend when importing the package instead of always switching to the Agg backend.

Tests:

  • Add a regression test that verifies importing the package does not change matplotlib’s backend in a clean subprocess.

Summary by CodeRabbit

  • Bug Fixes

    • Importing the library no longer overrides the user's Matplotlib backend; users' Matplotlib settings are preserved.
  • Tests

    • Added a test to ensure importing the library does not change the user's Matplotlib backend.
  • Chores

    • Release bumped to version 1.2.3.

Review Change Stack

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
Copilot AI review requested due to automatic review settings May 30, 2026 09:38
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 30, 2026

Reviewer's Guide

Removes the forced global matplotlib Agg backend selection on package import and adds a regression test to ensure the user’s chosen backend is preserved when importing the library.

Sequence diagram for matplotlib backend behavior on pyoctaveband import

sequenceDiagram
    actor User
    participant IPython
    participant matplotlib
    participant pyoctaveband

    User->>IPython: run %matplotlib svg
    IPython->>matplotlib: use("svg")

    User->>IPython: import pyoctaveband
    IPython->>pyoctaveband: import

    alt before_PR
        pyoctaveband->>matplotlib: use("Agg")
        matplotlib-->>IPython: backend Agg
    else after_PR
        pyoctaveband-->>IPython: no backend change
        matplotlib-->>IPython: backend svg
    end
Loading

File-Level Changes

Change Details Files
Stop forcing the matplotlib Agg backend on package import so the user’s configured backend is respected.
  • Remove the module-level call that set the matplotlib backend to a non-interactive backend.
  • Remove the now-unused matplotlib import that only existed to support the backend override.
src/pyoctaveband/__init__.py
Add a regression test to verify importing the package does not change the matplotlib backend.
  • Introduce a subprocess-based test that sets matplotlib to the svg backend, imports the package, and asserts the backend is unchanged.
  • Capture subprocess output and ensure the test fails with a helpful message if the backend is modified on import.
tests/test_matplotlib_backend.py

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

Warning

Review limit reached

@jmrplens, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 2 minutes and 16 seconds. Learn how PR review limits work.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 650e3af5-284a-45c6-b27f-4d880c1c38ac

📥 Commits

Reviewing files that changed from the base of the PR and between 956ab2c and cba69d3.

📒 Files selected for processing (2)
  • tests/conftest.py
  • tests/test_matplotlib_backend.py
📝 Walkthrough

Walkthrough

The PR removes matplotlib import and backend configuration from package initialization. It adds a subprocess-based test to ensure importing pyoctaveband does not change the user's matplotlib backend. The package version is bumped to 1.2.3.

Changes

Matplotlib Import Side Effects

Layer / File(s) Summary
Remove matplotlib import and backend configuration
src/pyoctaveband/__init__.py
Top-level matplotlib import and forced Agg backend configuration removed; public API (__all__, octavefilter signatures) unchanged.
Add backend override verification test
tests/test_matplotlib_backend.py
New subprocess-based test sets a matplotlib backend, imports pyoctaveband in an isolated interpreter, and asserts the backend remains the same.
Version bump
src/pyoctaveband/_version.py
__version__ incremented from 1.2.2 to 1.2.3.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped in quiet, light and small,
I nudged no backends, disturbed no call.
Imports gentle as morning dew,
Your plots remain decided by you. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: do not force matplotlib Agg backend on import' is clear, concise, and directly summarizes the main change: removing forced matplotlib backend configuration at module import time.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/matplotlib-backend-issue-52

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request removes the global configuration of the matplotlib backend (matplotlib.use("Agg")) from the package initialization to prevent overriding the user's chosen backend, and adds a test to verify this behavior. The review feedback suggests propagating the parent process's sys.path via the PYTHONPATH environment variable to the test's subprocess to ensure the test is robust and does not fail with a ModuleNotFoundError in environments where the package is not pre-installed.

Comment on lines +11 to +31
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The subprocess is executed without propagating the current sys.path or setting PYTHONPATH. If the package is not installed in the environment (for example, when running tests locally via pytest where the path is dynamically added to sys.path), the subprocess will fail with ModuleNotFoundError: No module named 'pyoctaveband'.

To ensure the test is robust across different test runners and environments, pass the parent process's sys.path via the PYTHONPATH environment variable to the subprocess.

import os
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"
    )
    env = os.environ.copy()
    env["PYTHONPATH"] = os.path.pathsep.join(sys.path)
    result = subprocess.run(
        [sys.executable, "-c", code],
        capture_output=True,
        text=True,
        env=env,
    )
    assert result.returncode == 0, result.stderr

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_matplotlib_backend.py`:
- Around line 26-30: The subprocess.run call that assigns to result is missing a
timeout and can hang; add a timeout argument (e.g., timeout=10) to the
subprocess.run invocation in tests/test_matplotlib_backend.py and wrap the call
in a try/except catching subprocess.TimeoutExpired to fail the test with a clear
message (use the same variable name result and reference
subprocess.TimeoutExpired in the handler) so the test won't hang indefinitely.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 27ba4be7-99b0-4d57-b446-defa5f667290

📥 Commits

Reviewing files that changed from the base of the PR and between aacb9c0 and 0930968.

📒 Files selected for processing (2)
  • src/pyoctaveband/__init__.py
  • tests/test_matplotlib_backend.py
💤 Files with no reviewable changes (1)
  • src/pyoctaveband/init.py

Comment thread tests/test_matplotlib_backend.py
@github-actions
Copy link
Copy Markdown
Contributor

CI Results ❌

Test Summary

Python Version Tests Failures Coverage Status
macos-latest-3.13 125 0 100.0% ✅ Passed
ubuntu-latest-3.13 125 0 100.0% ✅ Passed
windows-latest-3.13 125 1 99.8% ❌ Failed

Technical Benchmark Summary

📊 View Benchmark Details

PyOctaveBand: Technical Benchmark Report

Generated: 2026-05-30 09:43:57

1. Test Signal Parameters

  • Sample Rate: 96.0 kHz
  • Duration: 10.0 seconds
  • Signal Types: White Noise (Stability) / Pure Sine (Precision)
  • Precision: 64-bit Floating Point

2. Crossover (Linkwitz-Riley)

Crossover

  • Flatness Error: 0.000000 dB (Target < 0.01)

3. Precision & Isolation

Precision

Type Error (dB) Isolation Ripple GD Std (ms)
butter 2.46e-03 31.3 dB 0.2705 dB 2847.826
cheby1 3.38e-03 40.5 dB 0.1000 dB 3551.677
cheby2 3.26e-03 57.8 dB 29.4187 dB 4790.013
ellip 9.41e-03 54.2 dB 0.1000 dB 4700.881
bessel 5.20e-01 32.5 dB 5.9845 dB 1380.212

4. Performance

Performance

Channels Exec Time (s) Speedup
1 0.769 1.00x
2 1.515 1.01x
4 3.003 1.02x
8 5.985 1.03x
16 11.940 1.03x

View Full Artifacts

@github-actions
Copy link
Copy Markdown
Contributor

CI Results 🚀

Test Summary

Python Version Tests Failures Coverage Status
macos-latest-3.13 125 0 100.0% ✅ Passed
ubuntu-latest-3.13 125 0 100.0% ✅ Passed
windows-latest-3.13 125 0 100.0% ✅ Passed

Technical Benchmark Summary

📊 View Benchmark Details

PyOctaveBand: Technical Benchmark Report

Generated: 2026-05-30 09:44:50

1. Test Signal Parameters

  • Sample Rate: 96.0 kHz
  • Duration: 10.0 seconds
  • Signal Types: White Noise (Stability) / Pure Sine (Precision)
  • Precision: 64-bit Floating Point

2. Crossover (Linkwitz-Riley)

Crossover

  • Flatness Error: 0.000000 dB (Target < 0.01)

3. Precision & Isolation

Precision

Type Error (dB) Isolation Ripple GD Std (ms)
butter 2.46e-03 31.3 dB 0.2705 dB 2847.826
cheby1 3.38e-03 40.5 dB 0.1000 dB 3551.677
cheby2 3.26e-03 57.8 dB 29.4187 dB 4790.013
ellip 9.41e-03 54.2 dB 0.1000 dB 4700.881
bessel 5.20e-01 32.5 dB 5.9845 dB 1380.212

4. Performance

Performance

Channels Exec Time (s) Speedup
1 0.698 1.00x
2 1.365 1.02x
4 2.704 1.03x
8 5.391 1.04x
16 10.734 1.04x

View Full Artifacts

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.
@github-actions
Copy link
Copy Markdown
Contributor

CI Results ❌

Test Summary

Python Version Tests Failures Coverage Status
macos-latest-3.13 125 0 100.0% ✅ Passed
ubuntu-latest-3.13 125 0 100.0% ✅ Passed
windows-latest-3.13 125 1 100.0% ❌ Failed

Technical Benchmark Summary

📊 View Benchmark Details

PyOctaveBand: Technical Benchmark Report

Generated: 2026-05-30 09:51:20

1. Test Signal Parameters

  • Sample Rate: 96.0 kHz
  • Duration: 10.0 seconds
  • Signal Types: White Noise (Stability) / Pure Sine (Precision)
  • Precision: 64-bit Floating Point

2. Crossover (Linkwitz-Riley)

Crossover

  • Flatness Error: 0.000000 dB (Target < 0.01)

3. Precision & Isolation

Precision

Type Error (dB) Isolation Ripple GD Std (ms)
butter 2.46e-03 31.3 dB 0.2705 dB 2847.826
cheby1 3.38e-03 40.5 dB 0.1000 dB 3551.677
cheby2 3.26e-03 57.8 dB 29.4187 dB 4790.013
ellip 9.41e-03 54.2 dB 0.1000 dB 4700.881
bessel 5.20e-01 32.5 dB 5.9845 dB 1380.212

4. Performance

Performance

Channels Exec Time (s) Speedup
1 0.703 1.00x
2 1.375 1.02x
4 2.727 1.03x
8 5.391 1.04x
16 10.744 1.05x

View Full Artifacts

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.
@sonarqubecloud
Copy link
Copy Markdown

@github-actions
Copy link
Copy Markdown
Contributor

CI Results 🚀

Test Summary

Python Version Tests Failures Coverage Status
macos-latest-3.13 125 0 100.0% ✅ Passed
ubuntu-latest-3.13 125 0 100.0% ✅ Passed
windows-latest-3.13 125 0 100.0% ✅ Passed

Technical Benchmark Summary

📊 View Benchmark Details

PyOctaveBand: Technical Benchmark Report

Generated: 2026-05-30 10:13:19

1. Test Signal Parameters

  • Sample Rate: 96.0 kHz
  • Duration: 10.0 seconds
  • Signal Types: White Noise (Stability) / Pure Sine (Precision)
  • Precision: 64-bit Floating Point

2. Crossover (Linkwitz-Riley)

Crossover

  • Flatness Error: 0.000000 dB (Target < 0.01)

3. Precision & Isolation

Precision

Type Error (dB) Isolation Ripple GD Std (ms)
butter 2.46e-03 31.3 dB 0.2705 dB 2847.826
cheby1 3.38e-03 40.5 dB 0.1000 dB 3551.677
cheby2 3.26e-03 57.8 dB 29.4187 dB 4790.013
ellip 9.41e-03 54.2 dB 0.1000 dB 4700.881
bessel 5.20e-01 32.5 dB 5.9845 dB 1380.212

4. Performance

Performance

Channels Exec Time (s) Speedup
1 0.769 1.00x
2 1.513 1.02x
4 3.001 1.02x
8 5.965 1.03x
16 11.927 1.03x

View Full Artifacts

@jmrplens jmrplens merged commit d72001f into main May 30, 2026
14 checks passed
@jmrplens jmrplens deleted the fix/matplotlib-backend-issue-52 branch May 30, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants