-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Enhance %notebook
to save outputs, including MIME types and exceptions
#14780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
IPython/core/interactiveshell.py
Outdated
|
||
# Capture MIME outputs and exceptions | ||
if result.result: | ||
hm.output_mime_bundles[exec_count] = deepcopy(result.result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should prefer to:
a) perform the conversion to mime bundle here (call self.shell.display_formatter.format
), or
b) store a deep copy (as now).
I think (a) would be better because otherwise user with a large data frame (say few GB) will quickly exhaust their RAM, unless.
IPython/core/interactiveshell.py
Outdated
if result.error_in_exec: | ||
hm.exceptions[exec_count] = result.error_in_exec |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I think I would store MIME bundle representation of exceptions instead of exception objects to avoid memory leaks.
Could you please take a look at the feedback request on my PR when you get a chance? Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is going in the right direction, I think the next steps in order would be:
- Add tests:
- for returned value gets included
- the returned value with custom reprs (you can reuse this dummy object for testing) get included with all these reprs in the MIME bundle
- outputs displayed with
display()
get included - values printed with
print()
get included - exceptions get included
- matplotlib integration (I would say lower priority, let's get the basics fist)
- Add typing annotations
- Implement logic for intercepting output displayed using
display()
- Implement interception of stoud and stderr (
print()
andprint(file=sys.stderr)
) - Rethink how we capture matplotlib figures (i.e. possibly remove the special-casing, as (3) might allow use to just document that users need to use inline backend).
IPython/core/interactiveshell.py
Outdated
mime_obj = ( | ||
result.result[0] | ||
if isinstance(result.result, list) | ||
else result.result | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To capture the result passed to display()
we might want to also populate output_mime_bundles
in:
ipython/IPython/core/displaypub.py
Lines 132 to 138 in 5a183ac
for mime, handler in handlers.items(): | |
if mime in data: | |
handler(data[mime], metadata.get(mime, None)) | |
return | |
if "text/plain" in data: | |
print(data["text/plain"]) |
I am not yet 100% sure if this is the best place, but seems fine for first iteration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm facing a slight issue while capturing mime_bundles
here.
When only display
is called, the MIME output is shown before execution_count
is updated. However, when both an output and display
are present, the output appears first, then execution_count
is updated, and only after that is the MIME output displayed.
How can we determine whether the MIME bundle should be associated with the current or previous execution_count
in such cases?
In the screenshot below, in both cases <Figure size ...>
and 1
, self.shell.execution_count
is 5.
If I include stdout outputs, everything printed to the console (errors, tracebacks, etc.) gets captured, but we don’t need to capture them again since we already have logic for it. I’m looking into solutions to capture just print statements and others if required, but would really appreciate any feedback or guidance you have! Thanks! |
tests/test_magic.py
Outdated
@@ -19,6 +19,7 @@ | |||
from time import sleep | |||
from threading import Thread | |||
from unittest import TestCase, mock | |||
import nbformat |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be imported after pytest.importorskip("nbformat")
as in:
Lines 396 to 399 in 6cc4d06
def test_run_nb(self): | |
"""Test %run notebook.ipynb""" | |
pytest.importorskip("nbformat") | |
from nbformat import v4, writes |
tests/test_magic.py
Outdated
_ip.history_manager.exceptions.clear() | ||
_ip.history_manager.output_mime_bundles.clear() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should not be needed if reset()
gets called, right?
_ip.history_manager.exceptions.clear() | |
_ip.history_manager.output_mime_bundles.clear() |
tests/test_magic.py
Outdated
if cell.get("source", "").strip() == "display('test')": | ||
for output in cell.get("outputs", []): | ||
if output.get("output_type") in ("display_data", "execute_result"): | ||
data = output.get("data", {}) | ||
text = data.get("text/plain", "").strip() | ||
assert text == "test", f"Expected 'test', got: {text}" | ||
elif output.get("output_type") == "stream": | ||
text = output.get("text", "").strip() | ||
assert text == "test", f"Expected 'test', got: {text}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test will not fail if there are no outputs. Something similar to error_found
is needed here.
tests/test_magic.py
Outdated
_ip.history_manager.exceptions.clear() | ||
_ip.history_manager.output_mime_bundles.clear() | ||
|
||
cmds = ["1/0", "display('test')"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also need something like "1+1", and "print('hello')".
Can you give an example? It might be best to demonstrate by writing a test. |
The test now effectively checks for display outputs, errors, normal outputs, and stream outputs from |
The most straightforward way to test this would be to directly execute the commands within IPython and then export the session to a notebook, verifying the output cells. The code for this approach is: for cmd in cmds:
_ip.run_cell(cmd, store_history=True) However, it seems that the IPython environment used in CI is not the same version as the one from this branch. As a result, the outputs are correctly recorded and included when running the test locally (where I have the IPython version from this branch installed), but they are not included in the CI environment. |
Stream outputs can be captured using this approach. However, this method creates and closes a new I'm currently facing a few challenges:
|
And the first exception is incorrect as it is stream and the text renderer instead of error renderer will be used: which will mean links to paths would not show up. Here is an idea to simplify the test: instead of having a bespoke logic for each cell, let's use nbformat to create a notebook with expected code cells first, and then run it and compare the results. |
It feels like Tee should not be needed because IPython/IPykernel already has a way to get the
Agree, that's should be enough. I will dig into this. |
fix duplicate outputs, improve test
tests/test_magic.py
Outdated
if command == "1/0": | ||
# remove traceback from comparison, as traceback formatting will vary | ||
actual["outputs"][0].pop("traceback") | ||
expected["outputs"][0].pop("traceback") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Darshan808 when you get a chance - do you think we should change the traceback logic to make these two match, or keep this exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@krassowski
Got the issue! Ipython is using nocolor
as color value in the test environment, while nbclient was set to neutral
coloring. Once I updated ipython to use neutral
colors, the test started passing smoothly.
The failure with |
Can we do something like storing the previous self.ostream = getattr(sys, channel) And when attribute lookup fails, we return the original attribute from previously stored def __getattr__(self, name):
"""Delegate any other attribute access to the original stream."""
if name in self._original_attrs:
return getattr(self.ostream, name)
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") Tried this in https://github.com/Darshan808/ipython/actions/runs/13872664945/job/38821201850?pr=2, The downstream test passed on Ubuntu but stalled on macOS. |
I think we can try that. Still, any subclass checks would fail. I do not see better options right now. |
As expected, downstream tests are passing! 👍
I'm looking into this, but haven't yet found the root cause. |
So there are three failures: FAILED tests/test_magic.py::test_notebook_export_json_with_output - AssertionError: Outputs do not match for cell 1 with source "display('test')"
assert [] == [{'data': {'t...isplay_data'}]
Right contains one more item: {'data': {'text/plain': "'test'"}, 'metadata': {}, 'output_type': 'display_data'}
Full diff:
[
+ ,
- {'data': {'text/plain': "'test'"},
- 'metadata': {},
- 'output_type': 'display_data'},
]
FAILED tests/test_magic_terminal.py::PasteTestCase::test_paste_echo - AssertionError: assert '\n a ...ted text --\n' == '\n a ...ted text --\n'
- a = 100
+ a = 100
? +++++ +++++
- b = 200
+ b = 200
? +++++ +++++
+
## -- End pasted text --
FAILED tests/test_oinspect.py::test_pinfo_docstring_dynamic - AssertionError: assert None
+ where None = <function search at 0x7f7cdd416840>('Docstring:\\s+cdoc for prop', '\x1b[31mType:\x1b[39m property\n\x1b[31mString form:\x1b[39m <property object at 0x7f7cce2509f0>\n\x1b[31mDocstring:\x1b[39m cdoc for prop\n')
+ where <function search at 0x7f7cdd416840> = re.search
+ and '\x1b[31mType:\x1b[39m property\n\x1b[31mString form:\x1b[39m <property object at 0x7f7cce2509f0>\n\x1b[31mDocstring:\x1b[39m cdoc for prop\n' = CaptureResult(out='\x1b[31mType:\x1b[39m property\n\x1b[31mString form:\x1b[39m <property object at 0x7f7cce2509f0>\n\x1b[31mDocstring:\x1b[39m cdoc for prop\n', err='').out
ipython/tests/test_magic_terminal.py Lines 182 to 197 in 0c9949c
ipython/tests/test_oinspect.py Lines 508 to 531 in 0c9949c
We could replace capsys with |
Or we could try to guess if we are inside a test environment which tries to capture things and then avoid activating |
These tests swap self.ostream.write(data) Also the CI logs indicated an assertion mismatch rather than no output being captured. Regarding the outputs not being captured, logging in CI revealed that the But Downstream Tests |
Exploring some things in krassowski#4 but I do not have many good ideas yet.
|
Maybe
|
Yes it was. Should be all green now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @Darshan808!
%notebook
to save outputs, including MIME types and exceptions
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
andexceptions
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 thehistory_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. Ininteractiveshell.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.