Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ __pycache__
*.swp
.pytest_cache
.python-version
.venv*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You likely want that in your global gitignore to not have to do it for all your repos.

venv*/
.mypy_cache/

Expand Down
49 changes: 36 additions & 13 deletions IPython/core/tests/test_ultratb.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,24 +51,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", encoding="utf-8") 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", encoding="utf-8") 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 @@ -92,27 +92,27 @@ def test_nonascii_path(self):
fname = os.path.join(td, u"fooé.py")
with open(fname, "w", encoding="utf-8") 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 @@ -167,12 +167,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", encoding="utf-8") 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 @@ -363,6 +363,29 @@ def test_recursion_three_frames(self):
ip.run_cell("r3o2()")


class PEP678NotesReportingTest(unittest.TestCase):
ERROR_WITH_NOTE = """
try:
raise AssertionError("Message")
except Exception as e:
try:
e.add_note("This is a PEP-678 note.")
except AttributeError: # Python <= 3.10
e.__notes__ = ("This is a PEP-678 note.",)
raise
"""

def test_verbose_reports_notes(self):
with tt.AssertPrints(["AssertionError", "Message", "This is a PEP-678 note."]):
ip.run_cell(self.ERROR_WITH_NOTE)

def test_plain_reports_notes(self):
with tt.AssertPrints(["AssertionError", "Message", "This is a PEP-678 note."]):
ip.run_cell("%xmode Plain")
ip.run_cell(self.ERROR_WITH_NOTE)
ip.run_cell("%xmode Verbose")


#----------------------------------------------------------------------------

# module testing (minimal)
Expand Down
70 changes: 51 additions & 19 deletions IPython/core/ultratb.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
#*****************************************************************************


from collections.abc import Sequence
import functools
import inspect
import linecache
Expand Down Expand Up @@ -183,6 +184,14 @@ def get_line_number_of_frame(frame: types.FrameType) -> int:
return count_lines_in_py_file(filename)


def _safe_string(value, what, func=str):
# Copied from cpython/Lib/traceback.py
try:
return func(value)
except:
return f"<{what} {func.__name__}() failed>"


def _format_traceback_lines(lines, Colors, has_colors: bool, lvals):
"""
Format tracebacks lines with pointing arrow, leading numbers...
Expand Down Expand Up @@ -582,7 +591,7 @@ def _format_list(self, extracted_list):
"""

Colors = self.Colors
list = []
output_list = []
for ind, (filename, lineno, name, line) in enumerate(extracted_list):
normalCol, nameCol, fileCol, lineCol = (
# Emphasize the last entry
Expand All @@ -600,9 +609,9 @@ def _format_list(self, extracted_list):
item += "\n"
if line:
item += f"{lineCol} {line.strip()}{normalCol}\n"
list.append(item)
output_list.append(item)

return list
return output_list

def _format_exception_only(self, etype, value):
"""Format the exception part of a traceback.
Expand All @@ -619,11 +628,11 @@ def _format_exception_only(self, etype, value):
"""
have_filedata = False
Colors = self.Colors
list = []
output_list = []
stype = py3compat.cast_unicode(Colors.excName + etype.__name__ + Colors.Normal)
if value is None:
# Not sure if this can still happen in Python 2.6 and above
list.append(stype + '\n')
output_list.append(stype + "\n")
else:
if issubclass(etype, SyntaxError):
have_filedata = True
Expand All @@ -634,7 +643,7 @@ def _format_exception_only(self, etype, value):
else:
lineno = "unknown"
textline = ""
list.append(
output_list.append(
"%s %s%s\n"
% (
Colors.normalEm,
Expand All @@ -654,36 +663,41 @@ def _format_exception_only(self, etype, value):
i = 0
while i < len(textline) and textline[i].isspace():
i += 1
list.append('%s %s%s\n' % (Colors.line,
textline.strip(),
Colors.Normal))
output_list.append(
"%s %s%s\n" % (Colors.line, textline.strip(), Colors.Normal)
)
if value.offset is not None:
s = ' '
for c in textline[i:value.offset - 1]:
if c.isspace():
s += c
else:
s += ' '
list.append('%s%s^%s\n' % (Colors.caret, s,
Colors.Normal))
s += " "
output_list.append(
"%s%s^%s\n" % (Colors.caret, s, Colors.Normal)
)

try:
s = value.msg
except Exception:
s = self._some_str(value)
if s:
list.append('%s%s:%s %s\n' % (stype, Colors.excName,
Colors.Normal, s))
output_list.append(
"%s%s:%s %s\n" % (stype, Colors.excName, Colors.Normal, s)
)
else:
list.append('%s\n' % stype)
output_list.append("%s\n" % stype)

# PEP-678 notes
output_list.extend(f"{x}\n" for x in getattr(value, "__notes__", []))

# sync with user hooks
if have_filedata:
ipinst = get_ipython()
if ipinst is not None:
ipinst.hooks.synchronize_with_editor(value.filename, value.lineno, 0)

return list
return output_list

def get_exception_only(self, etype, value):
"""Only print the exception type and message, without a traceback.
Expand Down Expand Up @@ -999,9 +1013,27 @@ def format_exception(self, etype, evalue):
# User exception is improperly defined.
etype, evalue = str, sys.exc_info()[:2]
etype_str, evalue_str = map(str, (etype, evalue))

# PEP-678 notes
notes = getattr(evalue, "__notes__", [])
if not isinstance(notes, Sequence) or isinstance(notes, (str, bytes)):
notes = [_safe_string(notes, "__notes__", func=repr)]

# ... and format it
return ['%s%s%s: %s' % (colors.excName, etype_str,
colorsnormal, py3compat.cast_unicode(evalue_str))]
return [
"{}{}{}: {}".format(
colors.excName,
etype_str,
colorsnormal,
py3compat.cast_unicode(evalue_str),
),
*(
"{}{}".format(
colorsnormal, _safe_string(py3compat.cast_unicode(n), "note")
)
for n in notes
),
]

def format_exception_as_a_whole(
self,
Expand Down Expand Up @@ -1068,7 +1100,7 @@ def format_exception_as_a_whole(
if ipinst is not None:
ipinst.hooks.synchronize_with_editor(frame_info.filename, frame_info.lineno, 0)

return [[head] + frames + [''.join(formatted_exception[0])]]
return [[head] + frames + formatted_exception]

def get_records(
self, etb: TracebackType, number_of_lines_of_context: int, tb_offset: int
Expand Down
5 changes: 5 additions & 0 deletions docs/source/whatsnew/pr/pep678-notes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Support for PEP-678 Exception Notes
-----------------------------------

Ultratb now shows :pep:`678` notes, improving your debugging experience on
Python 3.11+ or with libraries such as Pytest and Hypothesis.