Skip to content

Commit 036d1fd

Browse files
authored
Fix matplotlib plots displaying in wrong cells during %notebook export (ipython#14998)
## Fixes ipython#14990 **Problem:** When using `%notebook` magic to export IPython sessions, auto flushed matplotlib plots appeared one cell later than where they were created. This happened because matplotlib's `flush_figures()` runs during the `post_execute` event, after the cell's output capture phase. **Solution:** Added execution state tracking to the display publisher: - Track when we're in `post_execute` phase vs normal execution - When in `post_execute`: use `execution_count - 1` (associate with originating cell) - When in normal execution: use current `execution_count` (for explicit `plt.show()`) **Result:** - Plots without `plt.show()` now appear in the correct cell - Plots with explicit `plt.show()` continue to work correctly - No breaking changes to existing behavior **Testing:** Added test to verify both auto-flush and manual-flush scenarios place plots in the correct cells.
2 parents 54f5282 + bc732bf commit 036d1fd

File tree

2 files changed

+94
-10
lines changed

2 files changed

+94
-10
lines changed

IPython/core/displaypub.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ class DisplayPublisher(Configurable):
4343
def __init__(self, shell=None, *args, **kwargs):
4444
self.shell = shell
4545
self._is_publishing = False
46+
self._in_post_execute = False
47+
if self.shell:
48+
self._setup_execution_tracking()
4649
super().__init__(*args, **kwargs)
4750

4851
def _validate_data(self, data, metadata=None):
@@ -62,6 +65,19 @@ def _validate_data(self, data, metadata=None):
6265
if not isinstance(metadata, dict):
6366
raise TypeError("metadata must be a dict, got: %r" % data)
6467

68+
def _setup_execution_tracking(self):
69+
"""Set up hooks to track execution state"""
70+
self.shell.events.register("post_execute", self._on_post_execute)
71+
self.shell.events.register("pre_execute", self._on_pre_execute)
72+
73+
def _on_post_execute(self):
74+
"""Called at start of post_execute phase"""
75+
self._in_post_execute = True
76+
77+
def _on_pre_execute(self):
78+
"""Called at start of pre_execute phase"""
79+
self._in_post_execute = False
80+
6581
# use * to indicate transient, update are keyword-only
6682
def publish(
6783
self,
@@ -133,7 +149,13 @@ def publish(
133149

134150
outputs = self.shell.history_manager.outputs
135151

136-
outputs[self.shell.execution_count].append(
152+
target_execution_count = self.shell.execution_count
153+
if self._in_post_execute:
154+
# We're in post_execute, so this is likely a matplotlib flush
155+
# Use execution_count - 1 to associate with the cell that created the plot
156+
target_execution_count = self.shell.execution_count - 1
157+
158+
outputs[target_execution_count].append(
137159
HistoryOutput(output_type="display_data", bundle=data)
138160
)
139161

tests/test_display_2.py

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -201,15 +201,77 @@ def test_set_matplotlib_formats_kwargs():
201201
cfg = _get_inline_config()
202202
cfg.print_figure_kwargs.update(dict(foo="bar"))
203203
kwargs = dict(dpi=150)
204-
set_matplotlib_formats("png", **kwargs)
205-
formatter = ip.display_formatter.formatters["image/png"]
206-
f = formatter.lookup_by_type(Figure)
207-
formatter_kwargs = f.keywords
208-
expected = kwargs
209-
expected["base64"] = True
210-
expected["fmt"] = "png"
211-
expected.update(cfg.print_figure_kwargs)
212-
assert formatter_kwargs == expected
204+
try:
205+
set_matplotlib_formats("png", **kwargs)
206+
formatter = ip.display_formatter.formatters["image/png"]
207+
f = formatter.lookup_by_type(Figure)
208+
formatter_kwargs = f.keywords
209+
expected = kwargs
210+
expected["base64"] = True
211+
expected["fmt"] = "png"
212+
expected.update(cfg.print_figure_kwargs)
213+
assert formatter_kwargs == expected
214+
finally:
215+
cfg.print_figure_kwargs.clear()
216+
217+
218+
@dec.skip_without("matplotlib")
219+
def test_matplotlib_positioning():
220+
_ip = get_ipython()
221+
222+
prev_active_types = _ip.display_formatter.active_types.copy()
223+
prev_execution_count = _ip.execution_count
224+
prev_user_ns_underscore = _ip.user_ns.get("_", None)
225+
226+
_ip.history_manager.reset()
227+
_ip.display_formatter.active_types = ["text/plain", "image/png"]
228+
229+
_ip.run_cell("import matplotlib")
230+
prev_mpl_backend = _ip.run_cell("matplotlib.get_backend()").result
231+
232+
try:
233+
_ip.run_line_magic("matplotlib", "inline")
234+
_ip.execution_count = 1
235+
_ip.run_cell("'no plot here'", store_history=True)
236+
237+
# Cell 2: No manual flush
238+
_ip.run_cell(
239+
"import matplotlib.pyplot as plt;plt.plot([0, 1])", store_history=True
240+
)
241+
242+
_ip.run_cell("'no plot here'", store_history=True)
243+
244+
# Cell 4: Manual flush
245+
_ip.run_cell("plt.plot([1, 0])\nplt.show()", store_history=True)
246+
247+
_ip.run_cell("'no plot here'", store_history=True)
248+
249+
outputs = _ip.history_manager.outputs
250+
251+
# Only cells 2 and 4 should have plots
252+
for cell_num in [1, 3, 5]:
253+
assert not any(
254+
"image/png" in out.bundle for out in outputs.get(cell_num, [])
255+
), f"Cell {cell_num} should not have plot"
256+
257+
cell_2_has_plot = any("image/png" in out.bundle for out in outputs.get(2, []))
258+
cell_4_has_plot = any("image/png" in out.bundle for out in outputs.get(4, []))
259+
260+
assert cell_2_has_plot, "Cell 2 should have plot (auto-flush)"
261+
assert cell_4_has_plot, "Cell 4 should have plot (manual flush)"
262+
263+
finally:
264+
_ip.run_cell("plt.close('all')")
265+
_ip.run_line_magic("matplotlib", prev_mpl_backend)
266+
_ip.history_manager.reset()
267+
_ip.display_formatter.active_types = prev_active_types
268+
_ip.displayhook.flush()
269+
270+
_ip.execution_count = prev_execution_count
271+
if prev_user_ns_underscore is not None:
272+
_ip.user_ns["_"] = prev_user_ns_underscore
273+
elif "_" in _ip.user_ns:
274+
del _ip.user_ns["_"]
213275

214276

215277
def test_display_available():

0 commit comments

Comments
 (0)