Skip to content
Open
52 changes: 52 additions & 0 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -3802,6 +3802,58 @@ def test_traceback_header(self):
exc = traceback.TracebackException(Exception, Exception("haven"), None)
self.assertEqual(list(exc.format()), ["Exception: haven\n"])

def test_name_error_punctuation_with_suggestions(self):
def raise_mssage(message, name, name_from=None):
try:
raise NameError(message, name=name)
except NameError as e:
exc = traceback.TracebackException.from_exception(e)
return list(exc.format())[-1]

test_cases = [
("a.", "time", "NameError: a. Did you forget to import 'time'?\n"),
("b?", "time", "NameError: b? Did you forget to import 'time'?\n"),
("c!", "time", "NameError: c! Did you forget to import 'time'?\n"),
("d", "time", "NameError: d. Did you forget to import 'time'?\n"),
("e", "foo123", "NameError: e\n"),
]
for message, name, expected in test_cases:
with self.subTest(message=message):
messsage = raise_mssage(message, name)
self.assertEqual(messsage, expected)

with self.subTest("combined suggestion"):
messsage = raise_mssage("foo", "abc")
expected_message = (
"NameError: foo. Did you mean: 'abs'? "
"Or did you forget to import 'abc'?\n"
)
self.assertEqual(messsage, expected_message)

with self.subTest("'did you mean' suggestion"):
messsage = raise_mssage("bar", "flaot")
expected_message = "NameError: bar. Did you mean: 'float'?\n"
self.assertEqual(messsage, expected_message)

def test_import_error_punctuation_handling_with_suggestions(self):
def raise_mssage(message):
try:
raise ImportError(message, name="math", name_from="sinq")
except ImportError as e:
exc = traceback.TracebackException.from_exception(e)
return list(exc.format())[-1]

test_cases = [
("a.", "ImportError: a. Did you mean: 'sin'?\n"),
("b?", "ImportError: b? Did you mean: 'sin'?\n"),
("c!", "ImportError: c! Did you mean: 'sin'?\n"),
("d", "ImportError: d. Did you mean: 'sin'?\n"),
]
for message, expected in test_cases:
with self.subTest(message=message):
messsage = raise_mssage(message)
self.assertEqual(messsage, expected)

@requires_debug_ranges()
def test_print(self):
def f():
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_stdlib_import = (
issubclass(exc_type, NameError)
and wrong_name in sys.stdlib_module_names
)
if not other_name:
if maybe_stdlib_import:
return f"Did you forget to import '{wrong_name}'?"
return None
text = f"Did you mean: '{other_name}'?"
if maybe_stdlib_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