]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-99139: Improve NameError error suggestion for instances (#99140)
authorPablo Galindo Salgado <Pablogsal@gmail.com>
Sun, 6 Nov 2022 13:52:06 +0000 (13:52 +0000)
committerGitHub <noreply@github.com>
Sun, 6 Nov 2022 13:52:06 +0000 (13:52 +0000)
Doc/whatsnew/3.12.rst
Include/internal/pycore_global_strings.h
Include/internal/pycore_runtime_init_generated.h
Lib/test/test_traceback.py
Lib/traceback.py
Misc/NEWS.d/next/Core and Builtins/2022-11-05-18-36-27.gh-issue-99139.cI9vV1.rst [new file with mode: 0644]
Python/suggestions.c

index b6daa6d5c9d9689637954ce63f73f5a5a55205e5..fb28d6758d3c5faff88a6643f34f94aac1b5c207 100644 (file)
@@ -75,6 +75,27 @@ Important deprecations, removals or restrictions:
 Improved Error Messages
 =======================
 
+* Improve the error suggestion for :exc:`NameError` exceptions for instances.
+  Now if a :exc:`NameError` is raised in a method and the instance has an
+  attribute that's exactly equal to the name in the exception, the suggestion
+  will include ``self.<NAME>`` instead of the closest match in the method
+  scope. Contributed by Pablo Galindo in :gh:`99139`.
+
+    >>> class A:
+    ...    def __init__(self):
+    ...        self.blech = 1
+    ...
+    ...    def foo(self):
+    ...        somethin = blech
+
+    >>> A().foo()
+    Traceback (most recent call last):
+      File "<stdin>", line 1
+        somethin = blech
+                   ^^^^^
+    NameError: name 'blech' is not defined. Did you mean: 'self.blech'?
+
+
 * Improve the :exc:`SyntaxError` error message when the user types ``import x
   from y`` instead of ``from y import x``. Contributed by Pablo Galindo in :gh:`98931`.
 
index e60bd4b94bbcdf7234a2b2b23ce2bb220ef25590..8912895b0de846c8fa4a062821bdbf826b24291c 100644 (file)
@@ -600,6 +600,7 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(seek)
         STRUCT_FOR_ID(seekable)
         STRUCT_FOR_ID(selectors)
+        STRUCT_FOR_ID(self)
         STRUCT_FOR_ID(send)
         STRUCT_FOR_ID(sep)
         STRUCT_FOR_ID(sequence)
index 8ce103700d69668329a3ec8de326b041a40a2e40..a1f1efdf43b765fe916634f8c35a8e237aee778e 100644 (file)
@@ -1109,6 +1109,7 @@ extern "C" {
                 INIT_ID(seek), \
                 INIT_ID(seekable), \
                 INIT_ID(selectors), \
+                INIT_ID(self), \
                 INIT_ID(send), \
                 INIT_ID(sep), \
                 INIT_ID(sequence), \
@@ -2567,6 +2568,8 @@ _PyUnicode_InitStaticStrings(void) {
     PyUnicode_InternInPlace(&string);
     string = &_Py_ID(selectors);
     PyUnicode_InternInPlace(&string);
+    string = &_Py_ID(self);
+    PyUnicode_InternInPlace(&string);
     string = &_Py_ID(send);
     PyUnicode_InternInPlace(&string);
     string = &_Py_ID(sep);
@@ -7104,6 +7107,10 @@ _PyStaticObjects_CheckRefcnt(void) {
         _PyObject_Dump((PyObject *)&_Py_ID(selectors));
         Py_FatalError("immortal object has less refcnt than expected _PyObject_IMMORTAL_REFCNT");
     };
+    if (Py_REFCNT((PyObject *)&_Py_ID(self)) < _PyObject_IMMORTAL_REFCNT) {
+        _PyObject_Dump((PyObject *)&_Py_ID(self));
+        Py_FatalError("immortal object has less refcnt than expected _PyObject_IMMORTAL_REFCNT");
+    };
     if (Py_REFCNT((PyObject *)&_Py_ID(send)) < _PyObject_IMMORTAL_REFCNT) {
         _PyObject_Dump((PyObject *)&_Py_ID(send));
         Py_FatalError("immortal object has less refcnt than expected _PyObject_IMMORTAL_REFCNT");
index 149d0234fe8a72b77a440943e0f503af1576b526..430daf69d2955d7ecd8e2bb4a9b3f47ca2be8366 100644 (file)
@@ -3356,6 +3356,31 @@ class SuggestionFormattingTestBase:
 
         actual = self.get_suggestion(func)
         self.assertNotIn("blech", actual)
+    
+    def test_name_error_with_instance(self):
+        class A:
+            def __init__(self):
+                self.blech = None
+            def foo(self):
+                blich = 1
+                x = blech
+
+        instance = A()
+        actual = self.get_suggestion(instance.foo)
+        self.assertIn("self.blech", actual)
+
+    def test_unbound_local_error_with_instance(self):
+        class A:
+            def __init__(self):
+                self.blech = None
+            def foo(self):
+                blich = 1
+                x = blech
+                blech = 1
+
+        instance = A()
+        actual = self.get_suggestion(instance.foo)
+        self.assertNotIn("self.blech", actual)
 
     def test_unbound_local_error_does_not_match(self):
         def func():
index cf5f355ff04c3b359cc9c0da529d536842f521bb..8d518728fa1b100a2f130e32a14b24bb2fa44885 100644 (file)
@@ -1037,6 +1037,16 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
             + list(frame.f_globals)
             + list(frame.f_builtins)
         )
+
+        # Check first if we are in a method and the instance
+        # has the wrong name as attribute
+        if 'self' in frame.f_locals:
+            self = frame.f_locals['self']
+            if hasattr(self, wrong_name):
+                return f"self.{wrong_name}"
+    
+    # Compute closest match
+
     if len(d) > _MAX_CANDIDATE_ITEMS:
         return None
     wrong_name_len = len(wrong_name)
diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-11-05-18-36-27.gh-issue-99139.cI9vV1.rst b/Misc/NEWS.d/next/Core and Builtins/2022-11-05-18-36-27.gh-issue-99139.cI9vV1.rst
new file mode 100644 (file)
index 0000000..62b78b9
--- /dev/null
@@ -0,0 +1,5 @@
+Improve the error suggestion for :exc:`NameError` exceptions for instances.
+Now if a :exc:`NameError` is raised in a method and the instance has an
+attribute that's exactly equal to the name in the exception, the suggestion
+will include ``self.<NAME>`` instead of the closest match in the method
+scope. Patch by Pablo Galindo
index 82376b6cd985a3fe6c53ced1fcb1138629d5cd4e..239245d73defc773d7968c2321be4074712be5ac 100644 (file)
@@ -1,5 +1,6 @@
 #include "Python.h"
 #include "pycore_frame.h"
+#include "pycore_runtime_init.h"  // _Py_ID()
 
 #include "pycore_pyerrors.h"
 #include "pycore_code.h"        // _PyCode_GetVarnames()
@@ -226,6 +227,24 @@ get_suggestions_for_name_error(PyObject* name, PyFrameObject* frame)
         return NULL;
     }
 
+    // Are we inside a method and the instance has an attribute called 'name'?
+    if (PySequence_Contains(dir, &_Py_ID(self)) > 0) {
+        PyObject* locals = PyFrame_GetLocals(frame);
+        if (!locals) {
+            goto error;
+        }
+        PyObject* self = PyDict_GetItem(locals, &_Py_ID(self)); /* borrowed */
+        Py_DECREF(locals);
+        if (!self) {
+            goto error;
+        }
+        
+        if (PyObject_HasAttr(self, name)) {
+            Py_DECREF(dir);
+            return PyUnicode_FromFormat("self.%S", name);
+        }
+    }
+
     PyObject *suggestions = calculate_suggestions(dir, name);
     Py_DECREF(dir);
     if (suggestions != NULL) {
@@ -250,6 +269,10 @@ get_suggestions_for_name_error(PyObject* name, PyFrameObject* frame)
     Py_DECREF(dir);
 
     return suggestions;
+
+error:
+    Py_DECREF(dir);
+    return NULL;
 }
 
 static bool