Skip to content

Commit

Permalink
fix(OSX): Matplotlib backend leaks in App Switcher (#1232)
Browse files Browse the repository at this point in the history
  • Loading branch information
rouk1 authored Jan 30, 2025
1 parent bf9009e commit 4f72706
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 1 deletion.
20 changes: 19 additions & 1 deletion skore/src/skore/persistence/item/matplotlib_figure_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from __future__ import annotations

from contextlib import contextmanager
from io import BytesIO
from typing import TYPE_CHECKING, Optional

Expand All @@ -18,6 +19,19 @@
from matplotlib.figure import Figure


@contextmanager
def mpl_backend(backend="agg"):
"""Context manager for switching matplotlib backend."""
import matplotlib

original_backend = matplotlib.get_backend()
matplotlib.use(backend)
try:
yield
finally:
matplotlib.use(original_backend)


class MatplotlibFigureItem(Item):
"""A class used to persist a Matplotlib figure."""

Expand Down Expand Up @@ -75,9 +89,13 @@ def factory(cls, figure: Figure, /, **kwargs) -> MatplotlibFigureItem:
@property
def figure(self) -> Figure:
"""The figure from the persistence."""
# switch mpl backend to avoid opening windows in a background thread
figure_bytes = b64_str_to_bytes(self.figure_b64_str)

with BytesIO(figure_bytes) as stream:
with (
BytesIO(figure_bytes) as stream,
mpl_backend(backend="agg"),
):
return joblib.load(stream)

def as_serializable_dict(self) -> dict:
Expand Down
29 changes: 29 additions & 0 deletions skore/tests/unit/item/test_matplotlib_figure_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

import joblib
import pytest
from matplotlib import get_backend
from matplotlib.figure import Figure
from matplotlib.pyplot import subplots
from matplotlib.testing.compare import compare_images
from skore.persistence.item import ItemTypeError, MatplotlibFigureItem
from skore.persistence.item.matplotlib_figure_item import mpl_backend
from skore.utils import b64_str_to_bytes, bytes_to_b64_str


Expand Down Expand Up @@ -93,3 +95,30 @@ def test_as_serializable_dict(self, mock_nowstr):
"media_type": "image/svg+xml;base64",
"value": figure_b64_str,
}

def test_backend_switch(self):
backend = get_backend()
# hoppefuly PostScript is never the default in tests ^^
with mpl_backend(backend="ps"):
assert get_backend() == "ps"
assert get_backend() == backend

def test_backend_switch_in_thread(self):
# as default osx backend could crash the interpreter
# if it tries to open a window in a background thread
# test that the figure property is usable in a thread
backend = get_backend()

def foo():
figure, ax = subplots()
ax.plot([1, 2, 3, 4], [1, 4, 2, 3])
φ = MatplotlibFigureItem.factory(figure)
assert isinstance(φ.figure, Figure)

import threading

t = threading.Thread(target=foo)
t.start()
t.join()

assert get_backend() == backend

0 comments on commit 4f72706

Please sign in to comment.