Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b554fde

Browse files
authored
Enhance %notebook to save outputs, including MIME types and exceptions (#14780)
### Description: This PR implements an enhancement to the `%notebook` magic command, enabling it to optionally save the output, including the MIME types and exceptions. ### Changes: Introduced two new dictionaries, `output_mime_bundles` and `exceptions` in history manager, to store output MIME types and exceptions separately. This prevents inflating the history. When the `%notebook` command is invoked, the code extracts MIME types and exceptions from the `history_manager` and appends them to the respective output cells in the generated notebook. ### Feedback Request: Currently, we are using `mime_obj.get_figure()` to retrieve the MIME data for Matplotlib objects. However, this approach only works for the figure object. Are there any other approaches to capture MIME data ? I am using `VerboseTB` to get a structured traceback. While this works for most errors, it doesn't handle syntax errors and some other edge cases very well. In `interactiveshell.py`, different types of errors are handled by using different traceback formats. Should we take the same approach here to handle different types of exceptions? It might result in some repetition, but it could improve error reporting for edge cases.
2 parents 116ee9e + 71862af commit b554fde

9 files changed

Lines changed: 293 additions & 34 deletions

File tree

IPython/core/display_trap.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ def __exit__(self, type, value, traceback):
5858
# Returning False will cause exceptions to propagate
5959
return False
6060

61+
@property
62+
def is_active(self) -> bool:
63+
return self._nested_level != 0
64+
6165
def set(self):
6266
"""Set the hook."""
6367
if sys.displayhook is not self.hook:

IPython/core/displayhook.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from traitlets import Instance, Float
1717
from warnings import warn
1818

19+
from .history import HistoryOutput
20+
1921
# TODO: Move the various attributes (cache_size, [others now moved]). Some
2022
# of these are also attributes of InteractiveShell. They should be on ONE object
2123
# only and the other objects should ask that one object for their values.
@@ -35,6 +37,7 @@ class DisplayHook(Configurable):
3537

3638
def __init__(self, shell=None, cache_size=1000, **kwargs):
3739
super(DisplayHook, self).__init__(shell=shell, **kwargs)
40+
self._is_active = False
3841
cache_size_min = 3
3942
if cache_size <= 0:
4043
self.do_full_cache = 0
@@ -51,7 +54,7 @@ def __init__(self, shell=None, cache_size=1000, **kwargs):
5154

5255
# we need a reference to the user-level namespace
5356
self.shell = shell
54-
57+
5558
self._,self.__,self.___ = '','',''
5659

5760
# these are deliberately global:
@@ -84,13 +87,13 @@ def check_for_underscore(self):
8487
def quiet(self):
8588
"""Should we silence the display hook because of ';'?"""
8689
# do not print output if input ends in ';'
87-
90+
8891
try:
8992
cell = self.shell.history_manager.input_hist_parsed[-1]
9093
except IndexError:
9194
# some uses of ipshellembed may fail here
9295
return False
93-
96+
9497
return self.semicolon_at_end_of_expression(cell)
9598

9699
@staticmethod
@@ -110,7 +113,11 @@ def semicolon_at_end_of_expression(expression):
110113

111114
def start_displayhook(self):
112115
"""Start the displayhook, initializing resources."""
113-
pass
116+
self._is_active = True
117+
118+
@property
119+
def is_active(self):
120+
return self._is_active
114121

115122
def write_output_prompt(self):
116123
"""Write the output prompt.
@@ -242,7 +249,10 @@ def fill_exec_result(self, result):
242249

243250
def log_output(self, format_dict):
244251
"""Log the output."""
245-
if 'text/plain' not in format_dict:
252+
self.shell.history_manager.outputs[self.prompt_count].append(
253+
HistoryOutput(output_type="execute_result", bundle=format_dict)
254+
)
255+
if "text/plain" not in format_dict:
246256
# nothing to do
247257
return
248258
if self.shell.logger.log_output:
@@ -254,6 +264,7 @@ def finish_displayhook(self):
254264
"""Finish up all displayhook activities."""
255265
sys.stdout.write(self.shell.separate_out2)
256266
sys.stdout.flush()
267+
self._is_active = False
257268

258269
def __call__(self, result=None):
259270
"""Printing with history cache management.
@@ -280,13 +291,12 @@ def cull_cache(self):
280291
cull_count = max(int(sz * self.cull_fraction), 2)
281292
warn('Output cache limit (currently {sz} entries) hit.\n'
282293
'Flushing oldest {cull_count} entries.'.format(sz=sz, cull_count=cull_count))
283-
294+
284295
for i, n in enumerate(sorted(oh)):
285296
if i >= cull_count:
286297
break
287298
self.shell.user_ns.pop('_%i' % n, None)
288299
oh.pop(n, None)
289-
290300

291301
def flush(self):
292302
if not self.do_full_cache:

IPython/core/displaypub.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
# This used to be defined here - it is imported for backwards compatibility
2424
from .display_functions import publish_display_data
25+
from .history import HistoryOutput
2526

2627
import typing as t
2728

@@ -41,6 +42,7 @@ class DisplayPublisher(Configurable):
4142

4243
def __init__(self, shell=None, *args, **kwargs):
4344
self.shell = shell
45+
self._is_publishing = False
4446
super().__init__(*args, **kwargs)
4547

4648
def _validate_data(self, data, metadata=None):
@@ -129,13 +131,25 @@ def publish(
129131
if self.shell is not None:
130132
handlers = getattr(self.shell, "mime_renderers", {})
131133

134+
outputs = self.shell.history_manager.outputs
135+
136+
outputs[self.shell.execution_count].append(
137+
HistoryOutput(output_type="display_data", bundle=data)
138+
)
139+
132140
for mime, handler in handlers.items():
133141
if mime in data:
134142
handler(data[mime], metadata.get(mime, None))
135143
return
136144

145+
self._is_publishing = True
137146
if "text/plain" in data:
138147
print(data["text/plain"])
148+
self._is_publishing = False
149+
150+
@property
151+
def is_publishing(self):
152+
return self._is_publishing
139153

140154
def clear_output(self, wait=False):
141155
"""Clear the output of the cell receiving output."""

IPython/core/history.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
import threading
1515
from pathlib import Path
1616

17+
from collections import defaultdict
1718
from contextlib import contextmanager
19+
from dataclasses import dataclass
1820
from decorator import decorator
1921
from traitlets import (
2022
Any,
@@ -583,6 +585,14 @@ def get_range_by_str(
583585
yield from self.get_range(sess, s, e, raw=raw, output=output)
584586

585587

588+
@dataclass
589+
class HistoryOutput:
590+
output_type: typing.Literal[
591+
"out_stream", "err_stream", "display_data", "execute_result"
592+
]
593+
bundle: typing.Dict[str, str]
594+
595+
586596
class HistoryManager(HistoryAccessor):
587597
"""A class to organize all history-related functionality in one place."""
588598

@@ -610,7 +620,11 @@ def _dir_hist_default(self) -> list[Path]:
610620
# execution count.
611621
output_hist = Dict()
612622
# The text/plain repr of outputs.
613-
output_hist_reprs: dict[int, str] = Dict() # type: ignore [assignment]
623+
output_hist_reprs: typing.Dict[int, str] = Dict() # type: ignore [assignment]
624+
# Maps execution_count to MIME bundles
625+
outputs: typing.Dict[int, typing.List[HistoryOutput]] = defaultdict(list)
626+
# Maps execution_count to exception tracebacks
627+
exceptions: typing.Dict[int, typing.Dict[str, Any]] = Dict() # type: ignore [assignment]
614628

615629
# The number of the current session in the history database
616630
session_number: int = Integer() # type: ignore [assignment]
@@ -749,6 +763,9 @@ def reset(self, new_session: bool = True) -> None:
749763
"""Clear the session history, releasing all object references, and
750764
optionally open a new session."""
751765
self.output_hist.clear()
766+
self.outputs.clear()
767+
self.exceptions.clear()
768+
752769
# The directory history can't be completely empty
753770
self.dir_hist[:] = [Path.cwd()]
754771

IPython/core/historyapp.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ def start(self):
5959
print("There are already at most %d entries in the history database." % self.keep)
6060
print("Not doing anything. Use --keep= argument to keep fewer entries")
6161
return
62-
62+
6363
print("Trimming history to the most recent %d entries." % self.keep)
64-
64+
6565
inputs.pop() # Remove the extra element we got to check the length.
6666
inputs.reverse()
6767
if inputs:
@@ -71,7 +71,7 @@ def start(self):
7171
sessions = list(con.execute('SELECT session, start, end, num_cmds, remark FROM '
7272
'sessions WHERE session >= ?', (first_session,)))
7373
con.close()
74-
74+
7575
# Create the new history database.
7676
new_hist_file = profile_dir / "history.sqlite.new"
7777
i = 0

0 commit comments

Comments
 (0)