Skip to content
Open
22 changes: 22 additions & 0 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -4878,6 +4878,28 @@ class PurePythonSuggestionFormattingTests(
traceback printing in traceback.py.
"""

def test_exception_punctuation_handling_with_suggestions(self):
def raise_with_period(): raise NameError("Error.", name='time')
def raise_with_exclamation(): raise NameError("Error!", name='time')
def raise_with_question(): raise NameError("Error?", name='time')
def raise_without_punctuation(): raise NameError("Error", name='time')

test_cases = [
(raise_with_period, "."),
(raise_with_exclamation, "!"),
(raise_with_question, "?"),
(raise_without_punctuation, "."),
]

for raise_function, punctuation in test_cases:
with self.subTest(raise_func=raise_function.__name__):
result_lines = self.get_exception(
raise_function, slice_start=-1, slice_end=None
)
expected = f"NameError: Error{punctuation} Did you forget to import 'time'?"
self.assertEqual(result_lines[0], expected)



@cpython_only
class CPythonSuggestionFormattingTests(
Expand Down
91 changes: 53 additions & 38 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -1081,50 +1081,33 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,

self._is_syntax_error = False
self._have_exc_type = exc_type is not None
if exc_type is not None:

if self._have_exc_type:
self.exc_type_qualname = exc_type.__qualname__
self.exc_type_module = exc_type.__module__
if issubclass(exc_type, SyntaxError):
# Handle SyntaxErrors specially
self.filename = exc_value.filename
lno = exc_value.lineno
self.lineno = str(lno) if lno is not None else None
end_lno = exc_value.end_lineno
self.end_lineno = str(end_lno) if end_lno is not None else None
self.text = exc_value.text
self.offset = exc_value.offset
self.end_offset = exc_value.end_offset
self.msg = exc_value.msg
self._is_syntax_error = True
self._exc_metadata = getattr(exc_value, "_metadata", None)
elif suggestion := _suggestion_message(exc_type, exc_value, exc_traceback):
if self._str.endswith(('.', '?', '!')):
punctuation = ''
else:
punctuation = '.'
self._str += f"{punctuation} {suggestion}"
else:
self.exc_type_qualname = None
self.exc_type_module = None

if exc_type and issubclass(exc_type, SyntaxError):
# Handle SyntaxError's specially
self.filename = exc_value.filename
lno = exc_value.lineno
self.lineno = str(lno) if lno is not None else None
end_lno = exc_value.end_lineno
self.end_lineno = str(end_lno) if end_lno is not None else None
self.text = exc_value.text
self.offset = exc_value.offset
self.end_offset = exc_value.end_offset
self.msg = exc_value.msg
self._is_syntax_error = True
self._exc_metadata = getattr(exc_value, "_metadata", None)
elif exc_type and issubclass(exc_type, ImportError) and \
getattr(exc_value, "name_from", None) is not None:
wrong_name = getattr(exc_value, "name_from", None)
suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
if suggestion:
self._str += f". Did you mean: '{suggestion}'?"
elif exc_type and issubclass(exc_type, ModuleNotFoundError) and \
sys.flags.no_site and \
getattr(exc_value, "name", None) not in sys.stdlib_module_names:
self._str += (". Site initialization is disabled, did you forget to "
+ "add the site-packages directory to sys.path?")
elif exc_type and issubclass(exc_type, (NameError, AttributeError)) and \
getattr(exc_value, "name", None) is not None:
wrong_name = getattr(exc_value, "name", None)
suggestion = _compute_suggestion_error(exc_value, exc_traceback, wrong_name)
if suggestion:
self._str += f". Did you mean: '{suggestion}'?"
if issubclass(exc_type, NameError):
wrong_name = getattr(exc_value, "name", None)
if wrong_name is not None and wrong_name in sys.stdlib_module_names:
if suggestion:
self._str += f" Or did you forget to import '{wrong_name}'?"
else:
self._str += f". Did you forget to import '{wrong_name}'?"
if lookup_lines:
self._load_lines()
self.__suppress_context__ = \
Expand Down Expand Up @@ -1732,6 +1715,38 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
return suggestion


def _suggestion_message(exc_type, exc_value, exc_traceback):
if (
issubclass(exc_type, ModuleNotFoundError)
and sys.flags.no_site
and getattr(exc_value, "name", None) not in sys.stdlib_module_names
):
return ("Site initialization is disabled, did you forget to "
"add the site-packages directory to sys.path?")
if issubclass(exc_type, (ImportError, NameError, AttributeError)):
if issubclass(exc_type, ImportError):
wrong_name = getattr(exc_value, "name_from", None)
else:
wrong_name = getattr(exc_value, "name", None)
if wrong_name:
other_name = _compute_suggestion_error(
exc_value, exc_traceback, wrong_name
)
maybe_builtin_import = (
issubclass(exc_type, NameError)
and wrong_name in sys.stdlib_module_names
)
if not other_name:
if maybe_builtin_import:
return f"Did you forget to import '{wrong_name}'?"
return None
text = f"Did you mean: '{other_name}'?"
if maybe_builtin_import:
return f"{text} Or did you forget to import '{wrong_name}'?"
return text
return None


def _levenshtein_distance(a, b, max_cost):
# A Python implementation of Python/suggestions.c:levenshtein_distance.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Avoid double punctuation in :class:`~traceback.TracebackException` messages
Loading