Skip to content
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
1 change: 1 addition & 0 deletions Lib/_colorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ class Traceback(ThemeSection):
frame: str = ANSIColors.MAGENTA
error_highlight: str = ANSIColors.BOLD_RED
error_range: str = ANSIColors.RED
exception_target: str = ANSIColors.YELLOW
reset: str = ANSIColors.RESET


Expand Down
35 changes: 24 additions & 11 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
test_frame = namedtuple('frame', ['f_code', 'f_globals', 'f_locals'])
test_tb = namedtuple('tb', ['tb_frame', 'tb_lineno', 'tb_next', 'tb_lasti'])

color_overrides = {"reset": "z", "filename": "fn", "error_highlight": "E", "note": "n"}
color_overrides = {"reset": "z", "filename": "fn", "error_highlight": "E", "note": "n", "exception_target": "Y"}
colors = {
color_overrides.get(k, k[0].lower()): v
for k, v in _colorize.default_theme.traceback.items()
Expand Down Expand Up @@ -248,13 +248,13 @@ class X(Exception):
def __str__(self):
1/0
err = traceback.format_exception_only(X, X())
self.assertEqual(len(err), 1)
self.assertEqual(len(err), 10)
str_value = '<exception str() failed>'
if X.__module__ in ('__main__', 'builtins'):
str_name = X.__qualname__
else:
str_name = '.'.join([X.__module__, X.__qualname__])
self.assertEqual(err[0], "%s: %s\n" % (str_name, str_value))
self.assertIn("%s: %s\n" % (str_name, str_value), err[0])

def test_format_exception_group_without_show_group(self):
eg = ExceptionGroup('A', [ValueError('B')])
Expand Down Expand Up @@ -2549,7 +2549,10 @@ def __repr__(self):

e.__notes__ = Unprintable()
err_msg = '<__notes__ repr() failed>'
self.assertEqual(self.get_report(e), vanilla + err_msg + '\n')
ignore_msg = "Exception ignored in __notes__ repr():"
msg = self.get_report(e)
self.assertIn(vanilla + err_msg + '\n', msg)
self.assertIn(ignore_msg, msg)

# non-string item in the __notes__ sequence
e.__notes__ = [BadThing(), 'Final Note']
Expand All @@ -2559,7 +2562,9 @@ def __repr__(self):
# unprintable, non-string item in the __notes__ sequence
e.__notes__ = [Unprintable(), 'Final Note']
err_msg = '<note str() failed>'
self.assertEqual(self.get_report(e), vanilla + err_msg + '\nFinal Note\n')
msg = self.get_report(e)
self.assertIn(vanilla + err_msg + '\nFinal Note\n', msg)
self.assertIn("Exception ignored in note str():", msg)

e.__notes__ = "please do not explode me"
err_msg = "'please do not explode me'"
Expand Down Expand Up @@ -2669,7 +2674,9 @@ def __str__(self):
err = self.get_report(X())
str_value = '<exception str() failed>'
str_name = '.'.join([X.__module__, X.__qualname__])
self.assertEqual(MODULE_PREFIX + err, f"{str_name}: {str_value}\n")
ignore_sentence = "Exception ignored in exception str():"
self.assertIn(f"{str_name}: {str_value}\n", MODULE_PREFIX + err)
self.assertIn(ignore_sentence, err)


# #### Exception Groups ####
Expand Down Expand Up @@ -4334,8 +4341,14 @@ def __getattr__(self, attr):
raise AttributeError(23)

for cls in [A, B, C]:
actual = self.get_suggestion(cls(), 'bluch')
self.assertIn("blech", actual)
try:
getattr(cls(), "bluch")
except AttributeError:
msg = traceback.format_exc()
self.assertIn("blech", msg)
# actual = self.get_suggestion(cls(), 'bluch')
# self.assertIn("blech", actual)
# The above using is changed because it will get the warning in the ignore exception


class DelattrSuggestionTests(BaseSuggestionTests):
Expand Down Expand Up @@ -5331,7 +5344,7 @@ def test_colorized_syntax_error(self):
e, capture_locals=True
)
actual = "".join(exc.format(colorize=True))
def expected(t, m, fn, l, f, E, e, z, n):
def expected(t, m, fn, l, f, E, e, z, n, Y):
return "".join(
[
f' File {fn}"<string>"{z}, line {l}1{z}\n',
Expand All @@ -5357,7 +5370,7 @@ def foo():
actual = tbstderr.getvalue().splitlines()

lno_foo = foo.__code__.co_firstlineno
def expected(t, m, fn, l, f, E, e, z, n):
def expected(t, m, fn, l, f, E, e, z, n, Y):
return [
'Traceback (most recent call last):',
f' File {fn}"{__file__}"{z}, '
Expand Down Expand Up @@ -5390,7 +5403,7 @@ def foo():

lno_foo = foo.__code__.co_firstlineno
actual = "".join(exc.format(colorize=True)).splitlines()
def expected(t, m, fn, l, f, E, e, z, n):
def expected(t, m, fn, l, f, E, e, z, n, Y):
return [
f" + Exception Group Traceback (most recent call last):",
f' | File {fn}"{__file__}"{z}, line {l}{lno_foo+9}{z}, in {f}test_colorized_traceback_from_exception_group{z}',
Expand Down
155 changes: 135 additions & 20 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import importlib.util
import pathlib
import _colorize
import threading

from contextlib import suppress

Expand Down Expand Up @@ -207,11 +208,89 @@ def _format_final_exc_line(etype, value, *, insert_final_newline=True, colorize=
return line


def _safe_string(value, what, func=str):
def _remove_exception(exc_value, other_exc_value, _seen=None):
if _seen is None:
_seen = set()
if id(exc_value) not in _seen:
_seen.add(id(exc_value))
if isinstance(exc_value.__cause__, BaseException):
if exc_value.__cause__ is other_exc_value:
exc_value.__cause__ = None
elif isinstance(exc_value.__cause__, BaseExceptionGroup):
if other_exc_value in exc_value.__cause__.exceptions:
exc_value.__cause__ = None
else:
_remove_exception(exc_value.__cause__, other_exc_value, _seen)
for i in exc_value.__cause__.exceptions:
_remove_exception(i, other_exc_value, _seen)
else:
_remove_exception(exc_value.__cause__, other_exc_value, _seen)
if isinstance(exc_value.__context__, BaseException):
if exc_value.__context__ is other_exc_value:
exc_value.__context__ = None
elif isinstance(exc_value.__context__, BaseExceptionGroup):
if other_exc_value in exc_value.__context__.exceptions:
exc_value.__context__ = None
else:
_remove_exception(
exc_value.__context__, other_exc_value, _seen
)
for i in exc_value.__context__.exceptions:
_remove_exception(i, other_exc_value, _seen)
else:
_remove_exception(
exc_value.__context__, other_exc_value, _seen
)


def _traceback_to_tuples(tb):
extracted = extract_tb(tb)
return tuple(
(f.filename, f.lineno, getattr(f, "name", None), f.line)
for f in extracted
) # handle SyntaxError


def _safe_string(value, what, func=str,
exception_target=None, exception_exclude=None):
try:
return func(value)
except:
return f'<{what} {func.__name__}() failed>'
if isinstance(exception_target, list):
typ, val, tb = sys.exc_info()
_add_exception_note(typ, val, tb, f"{what} {func.__name__}()",
exception_target, exception_exclude)
return f"<{what} {func.__name__}() failed>"


_ADD_EXC_NOTE_LIMIT = 10


def _add_exception_note(exc_type, exc_value, exc_tb, where,
exception_target, exception_exclude=None, _seen=threading.local()):
if not hasattr(_seen, "_seen"):
_seen._seen = set()
if not hasattr(_seen, "times"):
_seen.times = 0
if not isinstance(exception_target, list):
return
_seen.times += 1
tb_tuple = _traceback_to_tuples(exc_tb)
if tb_tuple not in _seen._seen and _seen.times <= _ADD_EXC_NOTE_LIMIT:
_seen._seen.add(tb_tuple)
if exception_exclude is not None:
_remove_exception(exc_value, exception_exclude)
msg = "".join(TracebackException(exc_type, exc_value, exc_tb).format())
while msg.endswith("\n") or msg.endswith(" "):
msg = msg[:-1]
exception_target.append(
f"\nException ignored in {where}:"
)
exception_target.append(msg)
_seen.times -= 1
if _seen.times <= 0:
_seen.times = 0
_seen._seen.clear()

# --

Expand Down Expand Up @@ -998,6 +1077,11 @@ def _format_note(note, indent, theme):
yield f"{indent}{theme.note}{l}{theme.reset}\n"


def _format_exception_target(note, indent, theme):
for l in note.split("\n"):
yield f"{indent}{theme.exception_target}{l}{theme.reset}\n"


class _ExceptionPrintContext:
def __init__(self):
self.seen = set()
Expand Down Expand Up @@ -1085,12 +1169,13 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,

# Capture now to permit freeing resources: only complication is in the
# unofficial API _format_final_exc_line
self._str = _safe_string(exc_value, 'exception')
try:
self.__notes__ = getattr(exc_value, '__notes__', None)
except Exception as e:
self.__notes__ = [
f'Ignored error getting __notes__: {_safe_string(e, '__notes__', repr)}']
exception_target = []
self._str = _safe_string(
exc_value,
"exception",
exception_target=exception_target,
exception_exclude=exc_value,
)

self._is_syntax_error = False
self._have_exc_type = exc_type is not None
Expand Down Expand Up @@ -1167,6 +1252,35 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
self._str += f" Or did you forget to import '{wrong_name}'?"
else:
self._str += f". Did you forget to import '{wrong_name}'?"
try:
original__notes__ = getattr(exc_value, "__notes__", None)
except Exception as e:
original__notes__ = [
f"Ignored error getting __notes__: {_safe_string(e, '__notes__', repr, exception_target, e)}"
]
if original__notes__ is not None and not (
isinstance(original__notes__, collections.abc.Sequence)
and not isinstance(original__notes__, (str, bytes))
):
original__notes__ = [
_safe_string(
original__notes__,
"__notes__",
repr,
exception_target,
exc_value,
)
]
final_string_list = []
if original__notes__ is not None: # avoid that __bool__ raise Exception
for i in original__notes__:
final_string_list.append(
_safe_string(
i, "note", str, exception_target, exc_value
)
)
self.__notes__ = final_string_list
self.exception_target = exception_target
if lookup_lines:
self._load_lines()
self.__suppress_context__ = \
Expand Down Expand Up @@ -1295,6 +1409,7 @@ def format_exception_only(self, *, show_group=False, _depth=0, **kwargs):
well, recursively, with indentation relative to their nesting depth.
"""
colorize = kwargs.get("colorize", False)
exception_target = kwargs.get("exception_target", True)
if colorize:
theme = _colorize.get_theme(force_color=True).traceback
else:
Expand All @@ -1321,16 +1436,11 @@ def format_exception_only(self, *, show_group=False, _depth=0, **kwargs):
else:
yield from [indent + l for l in self._format_syntax_error(stype, colorize=colorize)]

if (
isinstance(self.__notes__, collections.abc.Sequence)
and not isinstance(self.__notes__, (str, bytes))
):
for note in self.__notes__:
note = _safe_string(note, 'note')
yield from _format_note(note, indent, theme)
elif self.__notes__ is not None:
note = _safe_string(self.__notes__, '__notes__', func=repr)
for note in self.__notes__:
yield from _format_note(note, indent, theme)
if exception_target:
for note in self.exception_target:
yield from _format_exception_target(note, indent, theme)

if self.exceptions and show_group:
for ex in self.exceptions:
Expand Down Expand Up @@ -1547,6 +1657,7 @@ def format(self, *, chain=True, _ctx=None, **kwargs):
string in the output.
"""
colorize = kwargs.get("colorize", False)
exception_target = kwargs.get("exception_target", True)
if _ctx is None:
_ctx = _ExceptionPrintContext()

Expand Down Expand Up @@ -1577,7 +1688,7 @@ def format(self, *, chain=True, _ctx=None, **kwargs):
if exc.stack:
yield from _ctx.emit('Traceback (most recent call last):\n')
yield from _ctx.emit(exc.stack.format(colorize=colorize))
yield from _ctx.emit(exc.format_exception_only(colorize=colorize))
yield from _ctx.emit(exc.format_exception_only(colorize=colorize, exception_target=exception_target))
elif _ctx.exception_group_depth > self.max_group_depth:
# exception group, but depth exceeds limit
yield from _ctx.emit(
Expand All @@ -1594,7 +1705,7 @@ def format(self, *, chain=True, _ctx=None, **kwargs):
margin_char = '+' if is_toplevel else None)
yield from _ctx.emit(exc.stack.format(colorize=colorize))

yield from _ctx.emit(exc.format_exception_only(colorize=colorize))
yield from _ctx.emit(exc.format_exception_only(colorize=colorize, exception_target=exception_target))
num_excs = len(exc.exceptions)
if num_excs <= self.max_group_width:
n = num_excs
Expand All @@ -1617,7 +1728,7 @@ def format(self, *, chain=True, _ctx=None, **kwargs):
f'+---------------- {title} ----------------\n')
_ctx.exception_group_depth += 1
if not truncated:
yield from exc.exceptions[i].format(chain=chain, _ctx=_ctx, colorize=colorize)
yield from exc.exceptions[i].format(chain=chain, _ctx=_ctx, colorize=colorize, exception_target=exception_target)
else:
remaining = num_excs - self.max_group_width
plural = 's' if remaining > 1 else ''
Expand Down Expand Up @@ -1711,6 +1822,10 @@ def _check_for_nested_attribute(obj, wrong_name, attrs):
return None


# add "exception_target=None"
# then we can handle the exception raised from suggestion
# use function "_add_exception_note"
# it won't add in gh-135660
def _get_safe___dir__(obj):
# Use obj.__dir__() to avoid a TypeError when calling dir(obj).
# See gh-131001 and gh-139933.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Print the exception as warning (ignore) when handle a transmitted anomalies