Skip to content

Commit b8f5dfc

Browse files
authored
Store display outputs in history for %notebook magic (#1435)
1 parent 93f11db commit b8f5dfc

File tree

2 files changed

+60
-1
lines changed

2 files changed

+60
-1
lines changed

ipykernel/zmqshell.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,17 @@
3535
from IPython.utils.process import arg_split, system # type:ignore[attr-defined]
3636
from jupyter_client.session import Session, extract_header
3737
from jupyter_core.paths import jupyter_runtime_dir
38-
from traitlets import Any, CBool, CBytes, Instance, Type, default, observe
38+
from traitlets import Any, Bool, CBool, CBytes, Instance, Type, default, observe
3939

4040
from ipykernel import connect_qtconsole, get_connection_file, get_connection_info
4141
from ipykernel.displayhook import ZMQShellDisplayHook
4242
from ipykernel.jsonutil import encode_images, json_clean
4343

44+
try:
45+
from IPython.core.history import HistoryOutput
46+
except ImportError:
47+
HistoryOutput = None # type: ignore[assignment,misc]
48+
4449
# -----------------------------------------------------------------------------
4550
# Functions and classes
4651
# -----------------------------------------------------------------------------
@@ -54,6 +59,11 @@ class ZMQDisplayPublisher(DisplayPublisher):
5459
_parent_header: contextvars.ContextVar[dict[str, Any]]
5560
topic = CBytes(b"display_data")
5661

62+
store_display_history = Bool(
63+
False,
64+
help="If set to True, store display outputs in the history manager. Default is False.",
65+
).tag(config=True)
66+
5767
# thread_local:
5868
# An attribute used to ensure the correct output message
5969
# is processed. See ipykernel Issue 113 for a discussion.
@@ -115,6 +125,21 @@ def publish( # type:ignore[override]
115125
update : bool, optional, keyword-only
116126
If True, send an update_display_data message instead of display_data.
117127
"""
128+
if (
129+
self.store_display_history
130+
and self.shell is not None
131+
and hasattr(self.shell, "history_manager")
132+
and HistoryOutput is not None
133+
):
134+
# Reference: github.com/ipython/ipython/pull/14998
135+
exec_count = self.shell.execution_count
136+
if getattr(self.shell.display_pub, "_in_post_execute", False):
137+
exec_count -= 1
138+
outputs = getattr(self.shell.history_manager, "outputs", None)
139+
if outputs is not None:
140+
outputs.setdefault(exec_count, []).append(
141+
HistoryOutput(output_type="display_data", bundle=data)
142+
)
118143
self._flush_streams()
119144
if metadata is None:
120145
metadata = {}

tests/test_zmq_shell.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
ZMQInteractiveShell,
2323
)
2424

25+
try:
26+
from IPython.core.history import HistoryOutput
27+
except ImportError:
28+
HistoryOutput = None # type: ignore[assignment,misc]
29+
2530

2631
class NoReturnDisplayHook:
2732
"""
@@ -209,6 +214,35 @@ def test_unregister_hook(self):
209214
second = self.disp_pub.unregister_hook(hook)
210215
assert not bool(second)
211216

217+
@unittest.skipIf(HistoryOutput is None, "HistoryOutput not available")
218+
def test_display_stored_in_history(self):
219+
"""
220+
Test that published display data gets stored in shell history
221+
for %notebook magic support, and not stored when disabled.
222+
"""
223+
for enable in [False, True]:
224+
# Mock shell with history manager
225+
mock_shell = MagicMock()
226+
mock_shell.execution_count = 1
227+
mock_shell.history_manager.outputs = dict()
228+
mock_shell.display_pub._in_post_execute = False
229+
230+
self.disp_pub.shell = mock_shell
231+
self.disp_pub.store_display_history = enable
232+
233+
data = {"text/plain": "test output"}
234+
self.disp_pub.publish(data)
235+
236+
if enable:
237+
# Check that output was stored in history
238+
stored_outputs = mock_shell.history_manager.outputs[1]
239+
assert len(stored_outputs) == 1
240+
assert stored_outputs[0].output_type == "display_data"
241+
assert stored_outputs[0].bundle == data
242+
else:
243+
# Should not store anything in history
244+
assert mock_shell.history_manager.outputs == {}
245+
212246

213247
def test_magics(tmp_path):
214248
context = zmq.Context()

0 commit comments

Comments
 (0)