Skip to content

Commit 99e2e60

Browse files
authored
gh-99139: Improve NameError error suggestion for instances (#99140)
1 parent a0bc75e commit 99e2e60

File tree

7 files changed

+92
-0
lines changed

7 files changed

+92
-0
lines changed

Doc/whatsnew/3.12.rst

+21
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,27 @@ Important deprecations, removals or restrictions:
7575
Improved Error Messages
7676
=======================
7777

78+
* Improve the error suggestion for :exc:`NameError` exceptions for instances.
79+
Now if a :exc:`NameError` is raised in a method and the instance has an
80+
attribute that's exactly equal to the name in the exception, the suggestion
81+
will include ``self.<NAME>`` instead of the closest match in the method
82+
scope. Contributed by Pablo Galindo in :gh:`99139`.
83+
84+
>>> class A:
85+
... def __init__(self):
86+
... self.blech = 1
87+
...
88+
... def foo(self):
89+
... somethin = blech
90+
91+
>>> A().foo()
92+
Traceback (most recent call last):
93+
File "<stdin>", line 1
94+
somethin = blech
95+
^^^^^
96+
NameError: name 'blech' is not defined. Did you mean: 'self.blech'?
97+
98+
7899
* Improve the :exc:`SyntaxError` error message when the user types ``import x
79100
from y`` instead of ``from y import x``. Contributed by Pablo Galindo in :gh:`98931`.
80101

Include/internal/pycore_global_strings.h

+1
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,7 @@ struct _Py_global_strings {
600600
STRUCT_FOR_ID(seek)
601601
STRUCT_FOR_ID(seekable)
602602
STRUCT_FOR_ID(selectors)
603+
STRUCT_FOR_ID(self)
603604
STRUCT_FOR_ID(send)
604605
STRUCT_FOR_ID(sep)
605606
STRUCT_FOR_ID(sequence)

Include/internal/pycore_runtime_init_generated.h

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_traceback.py

+25
Original file line numberDiff line numberDiff line change
@@ -3356,6 +3356,31 @@ def func():
33563356

33573357
actual = self.get_suggestion(func)
33583358
self.assertNotIn("blech", actual)
3359+
3360+
def test_name_error_with_instance(self):
3361+
class A:
3362+
def __init__(self):
3363+
self.blech = None
3364+
def foo(self):
3365+
blich = 1
3366+
x = blech
3367+
3368+
instance = A()
3369+
actual = self.get_suggestion(instance.foo)
3370+
self.assertIn("self.blech", actual)
3371+
3372+
def test_unbound_local_error_with_instance(self):
3373+
class A:
3374+
def __init__(self):
3375+
self.blech = None
3376+
def foo(self):
3377+
blich = 1
3378+
x = blech
3379+
blech = 1
3380+
3381+
instance = A()
3382+
actual = self.get_suggestion(instance.foo)
3383+
self.assertNotIn("self.blech", actual)
33593384

33603385
def test_unbound_local_error_does_not_match(self):
33613386
def func():

Lib/traceback.py

+10
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,16 @@ def _compute_suggestion_error(exc_value, tb, wrong_name):
10371037
+ list(frame.f_globals)
10381038
+ list(frame.f_builtins)
10391039
)
1040+
1041+
# Check first if we are in a method and the instance
1042+
# has the wrong name as attribute
1043+
if 'self' in frame.f_locals:
1044+
self = frame.f_locals['self']
1045+
if hasattr(self, wrong_name):
1046+
return f"self.{wrong_name}"
1047+
1048+
# Compute closest match
1049+
10401050
if len(d) > _MAX_CANDIDATE_ITEMS:
10411051
return None
10421052
wrong_name_len = len(wrong_name)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Improve the error suggestion for :exc:`NameError` exceptions for instances.
2+
Now if a :exc:`NameError` is raised in a method and the instance has an
3+
attribute that's exactly equal to the name in the exception, the suggestion
4+
will include ``self.<NAME>`` instead of the closest match in the method
5+
scope. Patch by Pablo Galindo

Python/suggestions.c

+23
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "Python.h"
22
#include "pycore_frame.h"
3+
#include "pycore_runtime_init.h" // _Py_ID()
34

45
#include "pycore_pyerrors.h"
56
#include "pycore_code.h" // _PyCode_GetVarnames()
@@ -226,6 +227,24 @@ get_suggestions_for_name_error(PyObject* name, PyFrameObject* frame)
226227
return NULL;
227228
}
228229

230+
// Are we inside a method and the instance has an attribute called 'name'?
231+
if (PySequence_Contains(dir, &_Py_ID(self)) > 0) {
232+
PyObject* locals = PyFrame_GetLocals(frame);
233+
if (!locals) {
234+
goto error;
235+
}
236+
PyObject* self = PyDict_GetItem(locals, &_Py_ID(self)); /* borrowed */
237+
Py_DECREF(locals);
238+
if (!self) {
239+
goto error;
240+
}
241+
242+
if (PyObject_HasAttr(self, name)) {
243+
Py_DECREF(dir);
244+
return PyUnicode_FromFormat("self.%S", name);
245+
}
246+
}
247+
229248
PyObject *suggestions = calculate_suggestions(dir, name);
230249
Py_DECREF(dir);
231250
if (suggestions != NULL) {
@@ -250,6 +269,10 @@ get_suggestions_for_name_error(PyObject* name, PyFrameObject* frame)
250269
Py_DECREF(dir);
251270

252271
return suggestions;
272+
273+
error:
274+
Py_DECREF(dir);
275+
return NULL;
253276
}
254277

255278
static bool

0 commit comments

Comments
 (0)