Skip to content

Commit 364416f

Browse files
committed
fix: show() now displays in a plain Python REPL (#68)
The show=None default keyed off `not hasattr(sys, "ps1")`, which suppressed plt.show() in any REPL. A plain Python console sets sys.ps1 but leaves matplotlib non-interactive (unlike Jupyter/IPython), so the figure never rendered. Decide purely on matplotlib.is_interactive(): interactive backends auto-render, scripts and plain REPLs need the explicit plt.show().
1 parent 227434d commit 364416f

2 files changed

Lines changed: 24 additions & 9 deletions

File tree

src/spatialdata_plot/pl/basic.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
import contextlib
4-
import sys
54
import warnings
65
from collections import OrderedDict
76
from collections.abc import Callable, Sequence
@@ -1606,15 +1605,13 @@ def show(
16061605
if fig_params.fig is not None and save is not None:
16071606
save_fig(fig_params.fig, path=save)
16081607

1609-
# Show the plot unless the caller opted out.
1610-
# Default (show=None): display in non-interactive mode (scripts), suppress in interactive
1611-
# sessions. We check both sys.ps1 (standard REPL) and matplotlib.is_interactive()
1612-
# (covers IPython, Jupyter, plt.ion(), and IDE consoles like PyCharm).
1613-
# When the user supplies their own axes, they manage the figure lifecycle, so we
1614-
# default to not calling plt.show(). This allows multiple .pl.show(ax=...) calls
1615-
# to accumulate content on the same axes (see #362, #71).
1608+
# Default (show=None): show only when matplotlib is non-interactive. Interactive backends
1609+
# (Jupyter, IPython, plt.ion()) auto-render, so plt.show() is redundant; scripts and a plain
1610+
# REPL don't -- and a REPL sets sys.ps1 while staying non-interactive, so we can't key off
1611+
# sys.ps1. With a user-supplied ax the caller owns the figure, so default to no show (lets
1612+
# repeated .pl.show(ax=...) accumulate on one axes).
16161613
if show is None:
1617-
show = False if user_supplied_ax else (not hasattr(sys, "ps1") and not matplotlib.is_interactive())
1614+
show = False if user_supplied_ax else not matplotlib.is_interactive()
16181615
if show:
16191616
plt.show()
16201617
return (fig_params.ax if fig_params.axs is None else fig_params.axs) if return_ax else None # shuts up ruff

tests/pl/test_show.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,24 @@ def test_plt_show_when_ax_provided_and_show_true(self, sdata_blobs: SpatialData)
101101
mock_show.assert_called_once()
102102
plt.close("all")
103103

104+
@pytest.mark.parametrize("interactive,expected_calls", [(False, 1), (True, 0)])
105+
def test_show_default_keys_off_is_interactive(
106+
self, sdata_blobs: SpatialData, monkeypatch, interactive: bool, expected_calls: int
107+
):
108+
"""show=None calls plt.show() iff matplotlib is non-interactive, ignoring sys.ps1.
109+
110+
sys.ps1 is set in both cases to simulate a REPL; only matplotlib.is_interactive() may
111+
decide, so a plain (non-interactive) REPL still displays the figure (regression for #68).
112+
"""
113+
monkeypatch.setattr("sys.ps1", ">>> ", raising=False)
114+
with (
115+
patch("spatialdata_plot.pl.basic.matplotlib.is_interactive", return_value=interactive),
116+
patch("spatialdata_plot.pl.basic.plt.show") as mock_show,
117+
):
118+
sdata_blobs.pl.render_images(element="blobs_image").pl.show()
119+
assert mock_show.call_count == expected_calls
120+
plt.close("all")
121+
104122
def test_frameon_false_hides_axes_decorations(self, sdata_blobs: SpatialData):
105123
"""frameon=False should turn off axes decorations (regression for #204)."""
106124
ax = sdata_blobs.pl.render_images(element="blobs_image").pl.show(frameon=False, return_ax=True, show=False)

0 commit comments

Comments
 (0)