diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index bd3ecfd9a3863d..5476d8ea5e02d4 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -225,13 +225,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 = '' 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')]) @@ -2482,7 +2482,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'] @@ -2492,7 +2495,9 @@ def __repr__(self): # unprintable, non-string item in the __notes__ sequence e.__notes__ = [Unprintable(), 'Final Note'] err_msg = '' - 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'" @@ -2602,7 +2607,9 @@ def __str__(self): err = self.get_report(X()) str_value = '' 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 #### @@ -4226,8 +4233,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): diff --git a/Lib/traceback.py b/Lib/traceback.py index 692d44837936ee..1d8eba2043733b 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -11,6 +11,7 @@ import tokenize import io import _colorize +import threading from contextlib import suppress @@ -198,11 +199,73 @@ 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 exc_value.__cause__: + if exc_value.__cause__ is other_exc_value: + exc_value.__cause__ = None + else: + _remove_exception(exc_value.__cause__, other_exc_value, _seen) + if exc_value.__context__: + if exc_value.__context__ is other_exc_value: + exc_value.__context__ = None + 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: + _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() # -- @@ -1072,12 +1135,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 @@ -1125,6 +1189,38 @@ 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, exc_value)}" + ] + if original__notes__ is not None and not isinstance( + original__notes__, list): + original__notes__ = [ + _safe_string( + original__notes__, + "__notes__", + repr, + exception_target, + exc_value, + ) + ] + final_string_list = [] + if original__notes__: + 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__ = ( + exc_value.__suppress_context__ if exc_value is not None else False + ) if lookup_lines: self._load_lines() self.__suppress_context__ = \ @@ -1253,6 +1349,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) indent = 3 * _depth * ' ' if not self._have_exc_type: @@ -1275,15 +1372,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') + for note in self.__notes__: + yield from [indent + l + '\n' for l in note.split('\n')] + if exception_target: + for note in self.exception_target: yield from [indent + l + '\n' for l in note.split('\n')] - elif self.__notes__ is not None: - yield indent + "{}\n".format(_safe_string(self.__notes__, '__notes__', func=repr)) if self.exceptions and show_group: for ex in self.exceptions: diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-12-00-33-09.gh-issue-135660.KwoIbG.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-12-00-33-09.gh-issue-135660.KwoIbG.rst new file mode 100644 index 00000000000000..1e53ad99f8a654 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-12-00-33-09.gh-issue-135660.KwoIbG.rst @@ -0,0 +1 @@ +Print the exception as warning (ignore) when handle a transmitted anomalies