diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 96510eeec54640..e15fa0d66aa2aa 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4250,6 +4250,21 @@ def __dir__(self): actual = self.get_suggestion(A(), 'blech') self.assertNotIn("Did you mean", actual) + def test_suggestions_not_normalized(self): + class A: + analization = None + fiⁿₐˡᵢᶻₐᵗᵢᵒₙ = None + + self.assertIn("'finalization'", self.get_suggestion(A(), 'fiⁿₐˡᵢᶻₐᵗᵢᵒₙ')) + + class B: + attr_a = None + attr_µ = None + + suggestion = self.get_suggestion(B(), 'attr_\xb5') + self.assertIn("'attr_\u03bc'", suggestion) + self.assertIn(r"'attr_\u03bc'", suggestion) + class GetattrSuggestionTests(BaseSuggestionTests): def test_suggestions_no_args(self): @@ -4872,6 +4887,18 @@ def foo(self): actual = self.get_suggestion(instance.foo) self.assertIn("self.blech", actual) + def test_name_error_with_instance_not_normalized(self): + class A: + def __init__(self): + self.fiⁿₐˡᵢᶻₐᵗᵢᵒₙ = None + def foo(self): + analization = 1 + x = fiⁿₐˡᵢᶻₐᵗᵢᵒₙ + + instance = A() + actual = self.get_suggestion(instance.foo) + self.assertIn("self.finalization", actual) + def test_unbound_local_error_with_instance(self): class A: def __init__(self): diff --git a/Lib/traceback.py b/Lib/traceback.py index f95d6bdbd016ac..97d83f3ddd3297 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1111,7 +1111,10 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=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}'?" + if suggestion.isascii(): + self._str += f". Did you mean: '{suggestion}'?" + else: + self._str += f". Did you mean: '{suggestion}' ({suggestion!a})?" elif exc_type and issubclass(exc_type, ModuleNotFoundError): module_name = getattr(exc_value, "name", None) if module_name in sys.stdlib_module_names: @@ -1129,7 +1132,10 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=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 suggestion.isascii(): + self._str += f". Did you mean: '{suggestion}'?" + else: + self._str += f". Did you mean: '{suggestion}' ({suggestion!a})?" 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: @@ -1654,6 +1660,13 @@ def _check_for_nested_attribute(obj, wrong_name, attrs): def _compute_suggestion_error(exc_value, tb, wrong_name): if wrong_name is None or not isinstance(wrong_name, str): return None + not_normalized = False + if not wrong_name.isascii(): + from unicodedata import normalize + normalized_name = normalize('NFKC', wrong_name) + if normalized_name != wrong_name: + not_normalized = True + wrong_name = normalized_name if isinstance(exc_value, AttributeError): obj = exc_value.obj try: @@ -1699,6 +1712,8 @@ def _compute_suggestion_error(exc_value, tb, wrong_name): + list(frame.f_builtins) ) d = [x for x in d if isinstance(x, str)] + if not_normalized and wrong_name in d: + return wrong_name # Check first if we are in a method and the instance # has the wrong name as attribute @@ -1711,6 +1726,8 @@ def _compute_suggestion_error(exc_value, tb, wrong_name): if has_wrong_name: return f"self.{wrong_name}" + if not_normalized and wrong_name in d: + return wrong_name try: import _suggestions except ImportError: diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-01-22-17-04-30.gh-issue-143962.dQR1a9.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-01-22-17-04-30.gh-issue-143962.dQR1a9.rst new file mode 100644 index 00000000000000..71c2476c02b89d --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-01-22-17-04-30.gh-issue-143962.dQR1a9.rst @@ -0,0 +1,3 @@ +Name suggestion for not normalized name suggests now the normalized name or +the closest name to the normalized name. If the suggested name is not ASCII, +include also its ASCII representation.