From 1d4b0c980849d0cd61ce918ecd845bc5e212f3ad Mon Sep 17 00:00:00 2001 From: Darshan808 Date: Mon, 22 Sep 2025 12:10:26 +0545 Subject: [PATCH 01/14] add-displays-to-history --- ipykernel/zmqshell.py | 6 ++++++ tests/test_zmq_shell.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index 37575ee2d..01c2ed79a 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -30,6 +30,7 @@ from IPython.core.magic import Magics, line_magic, magics_class from IPython.core.magics import CodeMagics, MacroToEdit # type:ignore[attr-defined] from IPython.core.usage import default_banner +from IPython.core.history import HistoryOutput from IPython.display import Javascript, display from IPython.utils import openpy from IPython.utils.process import arg_split, system # type:ignore[attr-defined] @@ -115,6 +116,11 @@ def publish( # type:ignore[override] update : bool, optional, keyword-only If True, send an update_display_data message instead of display_data. """ + if self.shell is not None and hasattr(self.shell, 'history_manager'): + outputs = self.shell.history_manager.outputs + outputs[self.shell.execution_count].append( + HistoryOutput(output_type="display_data", bundle=data) + ) self._flush_streams() if metadata is None: metadata = {} diff --git a/tests/test_zmq_shell.py b/tests/test_zmq_shell.py index bc5e3f556..3e05174cb 100644 --- a/tests/test_zmq_shell.py +++ b/tests/test_zmq_shell.py @@ -209,6 +209,26 @@ def test_unregister_hook(self): second = self.disp_pub.unregister_hook(hook) assert not bool(second) + def test_display_stored_in_history(self): + """ + Test that published display data gets stored in shell history + for %notebook magic support. + """ + # Mock shell with history manager + mock_shell = MagicMock() + mock_shell.execution_count = 1 + mock_shell.history_manager.outputs = {1: []} + + self.disp_pub.shell = mock_shell + + data = {'text/plain': 'test output'} + self.disp_pub.publish(data) + + # Check that output was stored in history + stored_outputs = mock_shell.history_manager.outputs[1] + assert len(stored_outputs) == 1 + assert stored_outputs[0].output_type == "display_data" + assert stored_outputs[0].bundle == data def test_magics(tmp_path): context = zmq.Context() From 8dc02a807257840379dff12f073afc857d55ee64 Mon Sep 17 00:00:00 2001 From: Darshan808 Date: Mon, 22 Sep 2025 12:12:18 +0545 Subject: [PATCH 02/14] run-pre-commit --- ipykernel/zmqshell.py | 4 ++-- tests/test_zmq_shell.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index 01c2ed79a..4d20f9699 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -26,11 +26,11 @@ from IPython.core.autocall import ZMQExitAutocall from IPython.core.displaypub import DisplayPublisher from IPython.core.error import UsageError +from IPython.core.history import HistoryOutput from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC from IPython.core.magic import Magics, line_magic, magics_class from IPython.core.magics import CodeMagics, MacroToEdit # type:ignore[attr-defined] from IPython.core.usage import default_banner -from IPython.core.history import HistoryOutput from IPython.display import Javascript, display from IPython.utils import openpy from IPython.utils.process import arg_split, system # type:ignore[attr-defined] @@ -116,7 +116,7 @@ def publish( # type:ignore[override] update : bool, optional, keyword-only If True, send an update_display_data message instead of display_data. """ - if self.shell is not None and hasattr(self.shell, 'history_manager'): + if self.shell is not None and hasattr(self.shell, "history_manager"): outputs = self.shell.history_manager.outputs outputs[self.shell.execution_count].append( HistoryOutput(output_type="display_data", bundle=data) diff --git a/tests/test_zmq_shell.py b/tests/test_zmq_shell.py index 3e05174cb..1d076d917 100644 --- a/tests/test_zmq_shell.py +++ b/tests/test_zmq_shell.py @@ -221,7 +221,7 @@ def test_display_stored_in_history(self): self.disp_pub.shell = mock_shell - data = {'text/plain': 'test output'} + data = {"text/plain": "test output"} self.disp_pub.publish(data) # Check that output was stored in history @@ -230,6 +230,7 @@ def test_display_stored_in_history(self): assert stored_outputs[0].output_type == "display_data" assert stored_outputs[0].bundle == data + def test_magics(tmp_path): context = zmq.Context() socket = context.socket(zmq.PUB) From e4c59afc4f861f89ccd4f5ae15c160be7d980c93 Mon Sep 17 00:00:00 2001 From: Darshan808 Date: Sat, 27 Sep 2025 12:55:30 +0545 Subject: [PATCH 03/14] add-version-based-conditional-import --- ipykernel/zmqshell.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index 4d20f9699..6baa9a920 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -22,11 +22,10 @@ import warnings from pathlib import Path -from IPython.core import page +from IPython.core import page, version_info from IPython.core.autocall import ZMQExitAutocall from IPython.core.displaypub import DisplayPublisher from IPython.core.error import UsageError -from IPython.core.history import HistoryOutput from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC from IPython.core.magic import Magics, line_magic, magics_class from IPython.core.magics import CodeMagics, MacroToEdit # type:ignore[attr-defined] @@ -42,6 +41,11 @@ from ipykernel.displayhook import ZMQShellDisplayHook from ipykernel.jsonutil import encode_images, json_clean +if version_info >= (9, 1, 0): + from IPython.core.history import HistoryOutput +else: + HistoryOutput = None + # ----------------------------------------------------------------------------- # Functions and classes # ----------------------------------------------------------------------------- @@ -116,7 +120,11 @@ def publish( # type:ignore[override] update : bool, optional, keyword-only If True, send an update_display_data message instead of display_data. """ - if self.shell is not None and hasattr(self.shell, "history_manager"): + if ( + self.shell is not None + and hasattr(self.shell, "history_manager") + and HistoryOutput is not None + ): outputs = self.shell.history_manager.outputs outputs[self.shell.execution_count].append( HistoryOutput(output_type="display_data", bundle=data) From f056ba2110f0e140102691a4cb068a48e574d57a Mon Sep 17 00:00:00 2001 From: Darshan808 Date: Sat, 27 Sep 2025 21:51:50 +0545 Subject: [PATCH 04/14] fix-import-issue --- ipykernel/zmqshell.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index 6baa9a920..e4c96c037 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -22,7 +22,7 @@ import warnings from pathlib import Path -from IPython.core import page, version_info +from IPython.core import page from IPython.core.autocall import ZMQExitAutocall from IPython.core.displaypub import DisplayPublisher from IPython.core.error import UsageError @@ -41,9 +41,9 @@ from ipykernel.displayhook import ZMQShellDisplayHook from ipykernel.jsonutil import encode_images, json_clean -if version_info >= (9, 1, 0): +try: from IPython.core.history import HistoryOutput -else: +except ImportError: HistoryOutput = None # ----------------------------------------------------------------------------- From d89019f0ebbc2fecb6b49180f4aa2b0a0455bf08 Mon Sep 17 00:00:00 2001 From: Darshan808 Date: Mon, 29 Sep 2025 21:52:04 +0545 Subject: [PATCH 05/14] fix-display-outputs --- ipykernel/zmqshell.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index e4c96c037..457838361 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -125,10 +125,13 @@ def publish( # type:ignore[override] and hasattr(self.shell, "history_manager") and HistoryOutput is not None ): - outputs = self.shell.history_manager.outputs - outputs[self.shell.execution_count].append( - HistoryOutput(output_type="display_data", bundle=data) - ) + # Reference: github.com/ipython/ipython/pull/14998 + exec_count = self.shell.execution_count + if getattr(self.shell.display_pub, "_in_post_execute", False): + exec_count -= 1 + outputs = getattr(self.shell.history_manager, "outputs", None) + if outputs is not None: + outputs.append(HistoryOutput(output_type="display_data", bundle=data)) self._flush_streams() if metadata is None: metadata = {} From ac1025f10b472f6c43b690827a6717d0e562d26f Mon Sep 17 00:00:00 2001 From: Darshan808 Date: Mon, 29 Sep 2025 21:56:06 +0545 Subject: [PATCH 06/14] ignore-mypy-error --- ipykernel/zmqshell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index 457838361..40363afcc 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -44,7 +44,7 @@ try: from IPython.core.history import HistoryOutput except ImportError: - HistoryOutput = None + HistoryOutput = None # type: ignore[assignment] # ----------------------------------------------------------------------------- # Functions and classes From 990bc164472ceee2654df9e826042400b13b28a2 Mon Sep 17 00:00:00 2001 From: Darshan808 Date: Mon, 29 Sep 2025 22:53:55 +0545 Subject: [PATCH 07/14] fix-code-and-test --- ipykernel/zmqshell.py | 4 +++- tests/test_zmq_shell.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index 40363afcc..ebdf4465a 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -131,7 +131,9 @@ def publish( # type:ignore[override] exec_count -= 1 outputs = getattr(self.shell.history_manager, "outputs", None) if outputs is not None: - outputs.append(HistoryOutput(output_type="display_data", bundle=data)) + outputs.setdefault(exec_count, []).append( + HistoryOutput(output_type="display_data", bundle=data) + ) self._flush_streams() if metadata is None: metadata = {} diff --git a/tests/test_zmq_shell.py b/tests/test_zmq_shell.py index 1d076d917..f764f8ef0 100644 --- a/tests/test_zmq_shell.py +++ b/tests/test_zmq_shell.py @@ -217,7 +217,8 @@ def test_display_stored_in_history(self): # Mock shell with history manager mock_shell = MagicMock() mock_shell.execution_count = 1 - mock_shell.history_manager.outputs = {1: []} + mock_shell.history_manager.outputs = dict() + mock_shell.display_pub._in_post_execute = False self.disp_pub.shell = mock_shell From 5fe0a8f748f4f2b973ec5350272d3ae69ec704c0 Mon Sep 17 00:00:00 2001 From: Darshan808 Date: Mon, 29 Sep 2025 23:00:24 +0545 Subject: [PATCH 08/14] fix-lint --- ipykernel/zmqshell.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index ebdf4465a..ae9e2e2a4 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -35,7 +35,7 @@ from IPython.utils.process import arg_split, system # type:ignore[attr-defined] from jupyter_client.session import Session, extract_header from jupyter_core.paths import jupyter_runtime_dir -from traitlets import Any, CBool, CBytes, Instance, Type, default, observe +from traitlets import Any, CBool, CBytes, Instance, default, observe from ipykernel import connect_qtconsole, get_connection_file, get_connection_info from ipykernel.displayhook import ZMQShellDisplayHook @@ -44,7 +44,7 @@ try: from IPython.core.history import HistoryOutput except ImportError: - HistoryOutput = None # type: ignore[assignment] + HistoryOutput: type | None = None # ----------------------------------------------------------------------------- # Functions and classes @@ -517,8 +517,8 @@ def __init__(self, *args, **kwargs): self._parent_header = contextvars.ContextVar("parent_header") self._parent_header.set({}) - displayhook_class = Type(ZMQShellDisplayHook) - display_pub_class = Type(ZMQDisplayPublisher) + displayhook_class = type(ZMQShellDisplayHook) + display_pub_class = type(ZMQDisplayPublisher) data_pub_class = Any() kernel = Any() _parent_header: contextvars.ContextVar[dict[str, Any]] From 44fc0011817efaabf389af979d6e2f5604273fdc Mon Sep 17 00:00:00 2001 From: Darshan808 Date: Mon, 29 Sep 2025 23:02:09 +0545 Subject: [PATCH 09/14] fix-lint --- ipykernel/zmqshell.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index ae9e2e2a4..cc994dae6 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -35,7 +35,7 @@ from IPython.utils.process import arg_split, system # type:ignore[attr-defined] from jupyter_client.session import Session, extract_header from jupyter_core.paths import jupyter_runtime_dir -from traitlets import Any, CBool, CBytes, Instance, default, observe +from traitlets import Any, CBool, CBytes, Instance, Type, default, observe from ipykernel import connect_qtconsole, get_connection_file, get_connection_info from ipykernel.displayhook import ZMQShellDisplayHook @@ -517,8 +517,8 @@ def __init__(self, *args, **kwargs): self._parent_header = contextvars.ContextVar("parent_header") self._parent_header.set({}) - displayhook_class = type(ZMQShellDisplayHook) - display_pub_class = type(ZMQDisplayPublisher) + displayhook_class = Type(ZMQShellDisplayHook) + display_pub_class = Type(ZMQDisplayPublisher) data_pub_class = Any() kernel = Any() _parent_header: contextvars.ContextVar[dict[str, Any]] From 58b448ba6ad268c030fe14104b4e2db6710224f1 Mon Sep 17 00:00:00 2001 From: Darshan808 Date: Mon, 29 Sep 2025 23:08:28 +0545 Subject: [PATCH 10/14] try-fix-import --- ipykernel/zmqshell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index cc994dae6..41c883186 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -44,7 +44,7 @@ try: from IPython.core.history import HistoryOutput except ImportError: - HistoryOutput: type | None = None + HistoryOutput = None # type: ignore[assignment,misc] # ----------------------------------------------------------------------------- # Functions and classes From 5d459feb3a054826430874957f153796059883a8 Mon Sep 17 00:00:00 2001 From: Darshan808 Date: Mon, 29 Sep 2025 23:19:23 +0545 Subject: [PATCH 11/14] skip-on-older-versions --- tests/test_zmq_shell.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_zmq_shell.py b/tests/test_zmq_shell.py index f764f8ef0..6e0f9048e 100644 --- a/tests/test_zmq_shell.py +++ b/tests/test_zmq_shell.py @@ -22,6 +22,11 @@ ZMQInteractiveShell, ) +try: + from IPython.core.history import HistoryOutput +except ImportError: + HistoryOutput = None # type: ignore[assignment,misc] + class NoReturnDisplayHook: """ @@ -209,6 +214,7 @@ def test_unregister_hook(self): second = self.disp_pub.unregister_hook(hook) assert not bool(second) + @unittest.skipIf(HistoryOutput is None, "HistoryOutput not available") def test_display_stored_in_history(self): """ Test that published display data gets stored in shell history From 9477c5c1e3796f4d974f8332da06a973ea5f876a Mon Sep 17 00:00:00 2001 From: Darshan808 Date: Tue, 14 Oct 2025 18:02:14 +0545 Subject: [PATCH 12/14] add-traitlet-for-storing-display-ouputs --- ipykernel/zmqshell.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ipykernel/zmqshell.py b/ipykernel/zmqshell.py index 41c883186..ba707d481 100644 --- a/ipykernel/zmqshell.py +++ b/ipykernel/zmqshell.py @@ -35,7 +35,7 @@ from IPython.utils.process import arg_split, system # type:ignore[attr-defined] from jupyter_client.session import Session, extract_header from jupyter_core.paths import jupyter_runtime_dir -from traitlets import Any, CBool, CBytes, Instance, Type, default, observe +from traitlets import Any, Bool, CBool, CBytes, Instance, Type, default, observe from ipykernel import connect_qtconsole, get_connection_file, get_connection_info from ipykernel.displayhook import ZMQShellDisplayHook @@ -59,6 +59,11 @@ class ZMQDisplayPublisher(DisplayPublisher): _parent_header: contextvars.ContextVar[dict[str, Any]] topic = CBytes(b"display_data") + store_display_history = Bool( + False, + help="If set to True, store display outputs in the history manager. Default is False.", + ).tag(config=True) + # thread_local: # An attribute used to ensure the correct output message # is processed. See ipykernel Issue 113 for a discussion. @@ -121,7 +126,8 @@ def publish( # type:ignore[override] If True, send an update_display_data message instead of display_data. """ if ( - self.shell is not None + self.store_display_history + and self.shell is not None and hasattr(self.shell, "history_manager") and HistoryOutput is not None ): From ceed90d6ab329aa835abd53daf97bb0617a5b117 Mon Sep 17 00:00:00 2001 From: Darshan808 Date: Tue, 14 Oct 2025 20:54:37 +0545 Subject: [PATCH 13/14] fix-tests --- tests/test_zmq_shell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_zmq_shell.py b/tests/test_zmq_shell.py index 6e0f9048e..a017d8124 100644 --- a/tests/test_zmq_shell.py +++ b/tests/test_zmq_shell.py @@ -227,6 +227,7 @@ def test_display_stored_in_history(self): mock_shell.display_pub._in_post_execute = False self.disp_pub.shell = mock_shell + self.disp_pub.store_display_history = True data = {"text/plain": "test output"} self.disp_pub.publish(data) From a2a0360312801ece9bf6fb040c13024f228430a8 Mon Sep 17 00:00:00 2001 From: Darshan808 Date: Wed, 15 Oct 2025 17:55:02 +0545 Subject: [PATCH 14/14] refactor-test --- tests/test_zmq_shell.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/tests/test_zmq_shell.py b/tests/test_zmq_shell.py index a017d8124..c4859cfa0 100644 --- a/tests/test_zmq_shell.py +++ b/tests/test_zmq_shell.py @@ -218,25 +218,30 @@ def test_unregister_hook(self): def test_display_stored_in_history(self): """ Test that published display data gets stored in shell history - for %notebook magic support. + for %notebook magic support, and not stored when disabled. """ - # Mock shell with history manager - mock_shell = MagicMock() - mock_shell.execution_count = 1 - mock_shell.history_manager.outputs = dict() - mock_shell.display_pub._in_post_execute = False - - self.disp_pub.shell = mock_shell - self.disp_pub.store_display_history = True - - data = {"text/plain": "test output"} - self.disp_pub.publish(data) - - # Check that output was stored in history - stored_outputs = mock_shell.history_manager.outputs[1] - assert len(stored_outputs) == 1 - assert stored_outputs[0].output_type == "display_data" - assert stored_outputs[0].bundle == data + for enable in [False, True]: + # Mock shell with history manager + mock_shell = MagicMock() + mock_shell.execution_count = 1 + mock_shell.history_manager.outputs = dict() + mock_shell.display_pub._in_post_execute = False + + self.disp_pub.shell = mock_shell + self.disp_pub.store_display_history = enable + + data = {"text/plain": "test output"} + self.disp_pub.publish(data) + + if enable: + # Check that output was stored in history + stored_outputs = mock_shell.history_manager.outputs[1] + assert len(stored_outputs) == 1 + assert stored_outputs[0].output_type == "display_data" + assert stored_outputs[0].bundle == data + else: + # Should not store anything in history + assert mock_shell.history_manager.outputs == {} def test_magics(tmp_path):