Skip to content
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

Do not suppress exceptions in Output widget context #3417

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion python/ipywidgets/ipywidgets/widgets/tests/test_widget_output.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import traceback
import pytest
import sys
from unittest import TestCase
from contextlib import contextmanager
Expand Down Expand Up @@ -33,7 +35,13 @@ def _mock_get_ipython(self, msg_id):
# is still printed to screen
def showtraceback(self_, exc_tuple, *args, **kwargs):
etype, evalue, tb = exc_tuple
raise etype(evalue)
# Ipython notebook displays traceback and does not re-raise.
# This mock ipython object will mimic the behavior.
print("")
print("---------------------------------------------")
print("<mocked ipython Exception>")
traceback.print_exception(etype, evalue, tb)
print("---------------------------------------------")

ipython = type(
'mock_ipython',
Expand Down Expand Up @@ -64,6 +72,53 @@ def test_set_msg_id_when_capturing(self):
assert widget.msg_id == msg_id
assert widget.msg_id == ''

def test_exception_when_capturing(self):
class CustomException(Exception):
pass

def _run_code(widget, msg):
with widget:
raise CustomException(msg)
raise AssertionError(
"This line should be never executed. "
"The output widget probably have suppressed the exception.")

# 1. without ipython (plain python)
no_ipython = lambda: None
with self._mocked_ipython(no_ipython, no_ipython):
# 1-1. without ipython
widget = widget_output.Output()
with pytest.raises(CustomException):
_run_code(widget, "NO ipython")

# 1-2. without ipython, catch_exception (no effect)
widget = widget_output.Output(catch_exception=True)
with pytest.raises(CustomException):
_run_code(widget, "NO ipython, catch_exception")

# 2. with ipython
msg_id = 'msg-id'
get_ipython = self._mock_get_ipython(msg_id)
clear_output = self._mock_clear_output()
with self._mocked_ipython(get_ipython, clear_output):
# 2-1. with ipython (throws outside)
# An exception should be thrown outside the capturing block.
widget = widget_output.Output(catch_exception=False)
with pytest.raises(CustomException):
_run_code(widget, "ipython + NO catch_exception")

# 2-2. with ipython + catch_exception (suppress exception)
widget = widget_output.Output(catch_exception=True)
with pytest.raises(AssertionError,
match='.*should be never executed.*'):
_run_code(widget, "ipython + catch_exception")

# the default behavior: suppress exception (see #3417)
widget = widget_output.Output()
with pytest.raises(AssertionError,
match='.*should be never executed.*'):
_run_code(widget, "ipython + catch_exception")

def test_clear_output(self):
msg_id = 'msg-id'
get_ipython = self._mock_get_ipython(msg_id)
Expand Down
26 changes: 19 additions & 7 deletions python/ipywidgets/ipywidgets/widgets/widget_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from .widget import register
from .._version import __jupyter_widgets_output_version__

from traitlets import Unicode, Dict
from traitlets import Unicode, Dict, Bool
from IPython.core.interactiveshell import InteractiveShell
from IPython.display import clear_output
from IPython import get_ipython
Expand All @@ -29,11 +29,13 @@ class Output(DOMWidget):

You can then use the widget as a context manager: any output produced while in the
context will be captured and displayed in the widget instead of the standard output
area.
area. Any exception from the context will be thrown outside by default, but if
catch_exception is set to True, the context manager will catch and suppress the
exceptions.

You can also use the .capture() method to decorate a function or a method. Any output
produced by the function will then go to the output widget. This is useful for
debugging widget callbacks, for example.
debugging widget callbacks, for example:

Example::
import ipywidgets as widgets
Expand All @@ -49,6 +51,11 @@ class Output(DOMWidget):
@out.capture()
def func():
print('prints to output widget')

Parameters
----------
catch_exception: {True,False}
Whether exceptions will be suppressed or not. Default is False.
"""
_view_name = Unicode('OutputView').tag(sync=True)
_model_name = Unicode('OutputModel').tag(sync=True)
Expand All @@ -60,6 +67,8 @@ def func():
msg_id = Unicode('', help="Parent message id of messages to capture").tag(sync=True)
outputs = TypedTuple(trait=Dict(), help="The output messages synced from the frontend.").tag(sync=True)

catch_exception = Bool(True, help="Whether to catch and suppress all exceptions.")

__counter = 0

def clear_output(self, *pargs, **kwargs):
Expand Down Expand Up @@ -113,7 +122,7 @@ def __enter__(self):
kernel = ip.kernel
elif self.comm is not None and self.comm.kernel is not None:
kernel = self.comm.kernel

if kernel:
parent = None
if hasattr(kernel, "get_parent"):
Expand Down Expand Up @@ -147,9 +156,12 @@ def __exit__(self, etype, evalue, tb):
self.__counter -= 1
if self.__counter == 0:
self.msg_id = ''
# suppress exceptions when in IPython, since they are shown above,
# otherwise let someone else handle it
return True if kernel else None

# suppress exceptions when in IPython and capture_exception is set,
# since they are shown above. Otherwise let the exception rethrown
# so that someone else handles it.
if kernel and self.catch_exception:
return True

def _flush(self):
"""Flush stdout and stderr buffers."""
Expand Down