Skip to content
Closed
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
75 changes: 62 additions & 13 deletions IPython/core/tests/test_ultratb.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import traceback
import unittest

import pytest

from IPython.core.ultratb import ColorTB, VerboseTB


Expand Down Expand Up @@ -53,24 +55,24 @@ def wrapper(*args, **kwargs):
class ChangedPyFileTest(unittest.TestCase):
def test_changing_py_file(self):
"""Traceback produced if the line where the error occurred is missing?

https://github.com/ipython/ipython/issues/1456
"""
with TemporaryDirectory() as td:
fname = os.path.join(td, "foo.py")
with open(fname, "w") as f:
f.write(file_1)

with prepended_to_syspath(td):
ip.run_cell("import foo")

with tt.AssertPrints("ZeroDivisionError"):
ip.run_cell("foo.f()")

# Make the file shorter, so the line of the error is missing.
with open(fname, "w") as f:
f.write(file_2)

# For some reason, this was failing on the *second* call after
# changing the file, so we call f() twice.
with tt.AssertNotPrints("Internal Python error", channel='stderr'):
Expand All @@ -94,27 +96,27 @@ def test_nonascii_path(self):
fname = os.path.join(td, u"fooé.py")
with open(fname, "w") as f:
f.write(file_1)

with prepended_to_syspath(td):
ip.run_cell("import foo")

with tt.AssertPrints("ZeroDivisionError"):
ip.run_cell("foo.f()")

def test_iso8859_5(self):
with TemporaryDirectory() as td:
fname = os.path.join(td, 'dfghjkl.py')

with io.open(fname, 'w', encoding='iso-8859-5') as f:
f.write(iso_8859_5_file)

with prepended_to_syspath(td):
ip.run_cell("from dfghjkl import fail")

with tt.AssertPrints("ZeroDivisionError"):
with tt.AssertPrints(u'дбИЖ', suppress=False):
ip.run_cell('fail()')

def test_nonascii_msg(self):
cell = u"raise Exception('é')"
expected = u"Exception('é')"
Expand Down Expand Up @@ -169,12 +171,12 @@ def test_indentationerror_shows_line(self):
with tt.AssertPrints("IndentationError"):
with tt.AssertPrints("zoon()", suppress=False):
ip.run_cell(indentationerror_file)

with TemporaryDirectory() as td:
fname = os.path.join(td, "foo.py")
with open(fname, "w") as f:
f.write(indentationerror_file)

with tt.AssertPrints("IndentationError"):
with tt.AssertPrints("zoon()", suppress=False):
ip.magic('run %s' % fname)
Expand Down Expand Up @@ -257,6 +259,53 @@ def test_memoryerror(self):
ip.run_cell(memoryerror_code)


attributeerror_file = """
import collections

collections.defaultdict1
"""

nameerror_file = """
myfoo = 1
print(myfooa)
"""


@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10")
class ErrorSuggestionTest(unittest.TestCase):
def test_attribute_error_suggestion(self):
suggestion = "AttributeError: AttributeError: module 'collections' has no attribute 'defaultdict1'. Did you mean: 'defaultdict'"
with tt.AssertPrints("AttributeError"):
with tt.AssertPrints(suggestion, suppress=False):
ip.run_cell(attributeerror_file)

with TemporaryDirectory() as td:
fname = os.path.join(td, "foo.py")
with open(fname, "w") as f:
f.write(attributeerror_file)

with tt.AssertPrints("AttributeError"):
with tt.AssertPrints(suggestion, suppress=False):
ip.magic("run %s" % fname)

def test_name_error_suggestion(self):
suggestion = (
"NameError: NameError: name 'myfooa' is not defined. Did you mean: 'myfoo'?"
)
with tt.AssertPrints("NameError"):
with tt.AssertPrints(suggestion, suppress=False):
ip.run_cell(nameerror_file)

with TemporaryDirectory() as td:
fname = os.path.join(td, "foo.py")
with open(fname, "w") as f:
f.write(nameerror_file)

with tt.AssertPrints("NameError"):
with tt.AssertPrints(suggestion, suppress=False):
ip.magic("run %s" % fname)


class Python3ChainedExceptionsTest(unittest.TestCase):
DIRECT_CAUSE_ERROR_CODE = """
try:
Expand Down
24 changes: 23 additions & 1 deletion IPython/core/ultratb.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@
#*****************************************************************************


import contextlib
import inspect
import io
import linecache
import pydoc
import sys
Expand Down Expand Up @@ -759,6 +761,22 @@ def format_exception(self, etype, evalue):
return ['%s%s%s: %s' % (colors.excName, etype_str,
colorsnormal, py3compat.cast_unicode(evalue_str))]


def _get_suggestions(
self, typ: type, exc: BaseException, tb: TracebackType
) -> List[str]:
"""Return suggestions for exception in Python 3.10"""
if sys.version_info < (3, 10):
return []

if typ in (AttributeError, NameError):
err = io.StringIO()
with contextlib.redirect_stderr(err):
sys.__excepthook__(typ, exc, tb)
return [err.getvalue().split("\n")[-2] + "\n"]
else:
return []

def format_exception_as_a_whole(
self,
etype: type,
Expand All @@ -780,6 +798,7 @@ def format_exception_as_a_whole(
except AttributeError:
pass

suggestions = self._get_suggestions(orig_etype, evalue, etb)
tb_offset = self.tb_offset if tb_offset is None else tb_offset
assert isinstance(tb_offset, int)
head = self.prepare_header(etype, self.long_header)
Expand Down Expand Up @@ -810,7 +829,10 @@ def format_exception_as_a_whole(
% (Colors.excName, skipped, ColorsNormal)
)

formatted_exception = self.format_exception(etype, evalue)
if suggestions:
formatted_exception = self.format_exception(etype, suggestions[0])
else:
formatted_exception = self.format_exception(etype, evalue)
if records:
frame_info = records[-1]
ipinst = get_ipython()
Expand Down