From c4655e3fed6cdae0ac9ba94f953d7ce5177c36f3 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 22 Jan 2026 17:04:49 +0200 Subject: [PATCH] gh-143962: Improve name suggestions for not normalized names Suggest the normalized name or the closest name to the normalized name. If the suggested name is not ASCII, include also its ASCII representation. --- Lib/test/test_traceback.py | 27 +++++++++++++++++++ Lib/traceback.py | 21 +++++++++++++-- ...-01-22-17-04-30.gh-issue-143962.dQR1a9.rst | 3 +++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-01-22-17-04-30.gh-issue-143962.dQR1a9.rst 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.