]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-143962: Improve name suggestions for not normalized names (GH-144154)
authorSerhiy Storchaka <storchaka@gmail.com>
Wed, 4 Feb 2026 17:23:09 +0000 (19:23 +0200)
committerGitHub <noreply@github.com>
Wed, 4 Feb 2026 17:23:09 +0000 (17:23 +0000)
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
Lib/traceback.py
Misc/NEWS.d/next/Core_and_Builtins/2026-01-22-17-04-30.gh-issue-143962.dQR1a9.rst [new file with mode: 0644]

index 96510eeec5464082efdf4a8280287d0584c6b666..a4a49fd44bb2e07b490a48b9d60f40bd92fa4353 100644 (file)
@@ -4250,6 +4250,24 @@ class BaseSuggestionTests(SuggestionFormattingTestMixin):
         actual = self.get_suggestion(A(), 'blech')
         self.assertNotIn("Did you mean", actual)
 
+    def test_suggestions_not_normalized(self):
+        class A:
+            analization = None
+            fiⁿₐˡᵢᶻₐᵗᵢᵒₙ = None
+
+        suggestion = self.get_suggestion(A(), 'fiⁿₐˡᵢᶻₐᵗᵢᵒₙ')
+        self.assertIn("'finalization'", suggestion)
+        self.assertNotIn("analization", suggestion)
+
+        class B:
+            attr_a = None
+            attr_µ = None  # attr_\xb5
+
+        suggestion = self.get_suggestion(B(), 'attr_\xb5')
+        self.assertIn("'attr_\u03bc'", suggestion)
+        self.assertIn(r"'attr_\u03bc'", suggestion)
+        self.assertNotIn("attr_a", suggestion)
+
 
 class GetattrSuggestionTests(BaseSuggestionTests):
     def test_suggestions_no_args(self):
@@ -4872,6 +4890,34 @@ class SuggestionFormattingTestBase(SuggestionFormattingTestMixin):
         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)
+        self.assertNotIn("fiⁿₐˡᵢᶻₐᵗᵢᵒₙ", actual)
+        self.assertNotIn("analization", actual)
+
+        class B:
+            def __init__(self):
+                self.attr_µ = None  # attr_\xb5
+            def foo(self):
+                attr_a = 1
+                x = attr_µ  # attr_\xb5
+
+        instance = B()
+        actual = self.get_suggestion(instance.foo)
+        self.assertIn("self.attr_\u03bc", actual)
+        self.assertIn(r"self.attr_\u03bc", actual)
+        self.assertNotIn("attr_\xb5", actual)
+        self.assertNotIn("attr_a", actual)
+
     def test_unbound_local_error_with_instance(self):
         class A:
             def __init__(self):
index f95d6bdbd016acfb980bda8518e36cd087a752e8..97d83f3ddd3297aaac155147b35cbde3b4ee53b4 100644 (file)
@@ -1111,7 +1111,10 @@ class TracebackException:
             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 @@ class TracebackException:
             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 (file)
index 0000000..71c2476
--- /dev/null
@@ -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.