Skip to content

Commit 01650df

Browse files
committed
pythongh-99139: Improve NameError error suggestion for instances
1 parent 47ab848 commit 01650df

File tree

5 files changed

+86
-0
lines changed

5 files changed

+86
-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. Controbuted 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

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

+25
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#define NEEDS_PY_IDENTIFIER
2+
13
#include "Python.h"
24
#include "pycore_frame.h"
35

@@ -226,6 +228,25 @@ get_suggestions_for_name_error(PyObject* name, PyFrameObject* frame)
226228
return NULL;
227229
}
228230

231+
// Are we inside a method and the instance has an attribute called 'name'?
232+
_Py_IDENTIFIER(self);
233+
PyObject* self_str = _PyUnicode_FromId(&PyId_self); /* borrowed */
234+
if (PySequence_Contains(dir, self_str) > 0) {
235+
PyObject* locals = PyFrame_GetLocals(frame);
236+
if (!locals) {
237+
goto error;
238+
}
239+
PyObject* self = PyDict_GetItemString(locals, "self"); /* borrowed */
240+
Py_DECREF(locals);
241+
if (!self) {
242+
goto error;
243+
}
244+
245+
if (PyObject_HasAttr(self, name)) {
246+
return PyUnicode_FromFormat("self.%S", name);
247+
}
248+
}
249+
229250
PyObject *suggestions = calculate_suggestions(dir, name);
230251
Py_DECREF(dir);
231252
if (suggestions != NULL) {
@@ -250,6 +271,10 @@ get_suggestions_for_name_error(PyObject* name, PyFrameObject* frame)
250271
Py_DECREF(dir);
251272

252273
return suggestions;
274+
275+
error:
276+
Py_XDECREF(dir);
277+
return NULL;
253278
}
254279

255280
static bool

0 commit comments

Comments
 (0)