gh-99139: Improve NameError error suggestion for instances (#99140)

This commit is contained in:
Pablo Galindo Salgado 2022-11-06 13:52:06 +00:00 committed by GitHub
parent a0bc75e2fd
commit 99e2e60cb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 92 additions and 0 deletions

View File

@ -75,6 +75,27 @@ Important deprecations, removals or restrictions:
Improved Error Messages 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 * 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`. from y`` instead of ``from y import x``. Contributed by Pablo Galindo in :gh:`98931`.

View File

@ -600,6 +600,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(seek) STRUCT_FOR_ID(seek)
STRUCT_FOR_ID(seekable) STRUCT_FOR_ID(seekable)
STRUCT_FOR_ID(selectors) STRUCT_FOR_ID(selectors)
STRUCT_FOR_ID(self)
STRUCT_FOR_ID(send) STRUCT_FOR_ID(send)
STRUCT_FOR_ID(sep) STRUCT_FOR_ID(sep)
STRUCT_FOR_ID(sequence) STRUCT_FOR_ID(sequence)

View File

@ -1109,6 +1109,7 @@ extern "C" {
INIT_ID(seek), \ INIT_ID(seek), \
INIT_ID(seekable), \ INIT_ID(seekable), \
INIT_ID(selectors), \ INIT_ID(selectors), \
INIT_ID(self), \
INIT_ID(send), \ INIT_ID(send), \
INIT_ID(sep), \ INIT_ID(sep), \
INIT_ID(sequence), \ INIT_ID(sequence), \
@ -2567,6 +2568,8 @@ _PyUnicode_InitStaticStrings(void) {
PyUnicode_InternInPlace(&string); PyUnicode_InternInPlace(&string);
string = &_Py_ID(selectors); string = &_Py_ID(selectors);
PyUnicode_InternInPlace(&string); PyUnicode_InternInPlace(&string);
string = &_Py_ID(self);
PyUnicode_InternInPlace(&string);
string = &_Py_ID(send); string = &_Py_ID(send);
PyUnicode_InternInPlace(&string); PyUnicode_InternInPlace(&string);
string = &_Py_ID(sep); string = &_Py_ID(sep);
@ -7104,6 +7107,10 @@ _PyStaticObjects_CheckRefcnt(void) {
_PyObject_Dump((PyObject *)&_Py_ID(selectors)); _PyObject_Dump((PyObject *)&_Py_ID(selectors));
Py_FatalError("immortal object has less refcnt than expected _PyObject_IMMORTAL_REFCNT"); 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) { if (Py_REFCNT((PyObject *)&_Py_ID(send)) < _PyObject_IMMORTAL_REFCNT) {
_PyObject_Dump((PyObject *)&_Py_ID(send)); _PyObject_Dump((PyObject *)&_Py_ID(send));
Py_FatalError("immortal object has less refcnt than expected _PyObject_IMMORTAL_REFCNT"); Py_FatalError("immortal object has less refcnt than expected _PyObject_IMMORTAL_REFCNT");

View File

@ -3356,6 +3356,31 @@ class SuggestionFormattingTestBase:
actual = self.get_suggestion(func) actual = self.get_suggestion(func)
self.assertNotIn("blech", actual) 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 test_unbound_local_error_does_not_match(self):
def func(): def func():

View File

@ -1037,6 +1037,16 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
+ list(frame.f_globals) + list(frame.f_globals)
+ list(frame.f_builtins) + 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: if len(d) > _MAX_CANDIDATE_ITEMS:
return None return None
wrong_name_len = len(wrong_name) wrong_name_len = len(wrong_name)

View File

@ -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

View File

@ -1,5 +1,6 @@
#include "Python.h" #include "Python.h"
#include "pycore_frame.h" #include "pycore_frame.h"
#include "pycore_runtime_init.h" // _Py_ID()
#include "pycore_pyerrors.h" #include "pycore_pyerrors.h"
#include "pycore_code.h" // _PyCode_GetVarnames() #include "pycore_code.h" // _PyCode_GetVarnames()
@ -226,6 +227,24 @@ get_suggestions_for_name_error(PyObject* name, PyFrameObject* frame)
return NULL; 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); PyObject *suggestions = calculate_suggestions(dir, name);
Py_DECREF(dir); Py_DECREF(dir);
if (suggestions != NULL) { if (suggestions != NULL) {
@ -250,6 +269,10 @@ get_suggestions_for_name_error(PyObject* name, PyFrameObject* frame)
Py_DECREF(dir); Py_DECREF(dir);
return suggestions; return suggestions;
error:
Py_DECREF(dir);
return NULL;
} }
static bool static bool