Skip to content

Commit b1d979e

Browse files
committed
bpo-38530: Offer suggestions on NameError
1 parent 37494b4 commit b1d979e

File tree

8 files changed

+287
-10
lines changed

8 files changed

+287
-10
lines changed

Doc/library/exceptions.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,13 @@ The following exceptions are the exceptions that are usually raised.
242242
unqualified names. The associated value is an error message that includes the
243243
name that could not be found.
244244

245+
The :attr:`name` attribute can be set using a keyword-only argument to the
246+
constructor. When set it represent the name of the variable that was attempted
247+
to be accessed.
248+
249+
.. versionchanged:: 3.10
250+
Added the :attr:`name` attribute.
251+
245252

246253
.. exception:: NotImplementedError
247254

Doc/whatsnew/3.10.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,23 @@ raised from:
187187
188188
(Contributed by Pablo Galindo in :issue:`38530`.)
189189
190+
NameErrors
191+
~~~~~~~~~~
192+
193+
When printing :exc:`NameError` raised by the interpreter, :c:func:`PyErr_Display`
194+
will offer suggestions of simmilar variable names in the function that the exception
195+
was raised from:
196+
197+
.. code-block:: python
198+
199+
>>> schwarzschild_black_hole = None
200+
>>> schwarschild_black_hole
201+
Traceback (most recent call last):
202+
File "<stdin>", line 1, in <module>
203+
NameError: name 'schwarschild_black_hole' is not defined. Did you mean: schwarzschild_black_hole?
204+
205+
(Contributed by Pablo Galindo in :issue:`38530`.)
206+
190207
PEP 626: Precise line numbers for debugging and other tools
191208
-----------------------------------------------------------
192209

Include/cpython/pyerrors.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ typedef struct {
6262
PyObject *value;
6363
} PyStopIterationObject;
6464

65+
typedef struct {
66+
PyException_HEAD
67+
PyObject *name;
68+
} PyNameErrorObject;
69+
6570
typedef struct {
6671
PyException_HEAD
6772
PyObject *obj;

Lib/test/test_exceptions.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,129 @@ class TestException(MemoryError):
14131413

14141414
gc_collect()
14151415

1416+
global_for_suggestions = None
1417+
1418+
class NameErrorTests(unittest.TestCase):
1419+
def test_name_error_has_name(self):
1420+
try:
1421+
bluch
1422+
except NameError as exc:
1423+
self.assertEqual("bluch", exc.name)
1424+
1425+
def test_name_error_suggestions(self):
1426+
def Substitution():
1427+
noise = more_noise = a = bc = None
1428+
blech = None
1429+
print(bluch)
1430+
1431+
def Elimination():
1432+
noise = more_noise = a = bc = None
1433+
blch = None
1434+
print(bluch)
1435+
1436+
def Addition():
1437+
noise = more_noise = a = bc = None
1438+
bluchin = None
1439+
print(bluch)
1440+
1441+
def SubstitutionOverElimination():
1442+
blach = None
1443+
bluc = None
1444+
print(bluch)
1445+
1446+
def SubstitutionOverAddition():
1447+
blach = None
1448+
bluchi = None
1449+
print(bluch)
1450+
1451+
def EliminationOverAddition():
1452+
blucha = None
1453+
bluc = None
1454+
print(bluch)
1455+
1456+
for func, suggestion in [(Substitution, "blech?"),
1457+
(Elimination, "blch?"),
1458+
(Addition, "bluchin?"),
1459+
(EliminationOverAddition, "blucha?"),
1460+
(SubstitutionOverElimination, "blach?"),
1461+
(SubstitutionOverAddition, "blach?")]:
1462+
err = None
1463+
try:
1464+
func()
1465+
except NameError as exc:
1466+
with support.captured_stderr() as err:
1467+
sys.__excepthook__(*sys.exc_info())
1468+
self.assertIn(suggestion, err.getvalue())
1469+
1470+
def test_name_error_suggestions_from_globals(self):
1471+
def func():
1472+
print(global_for_suggestio)
1473+
try:
1474+
func()
1475+
except NameError as exc:
1476+
with support.captured_stderr() as err:
1477+
sys.__excepthook__(*sys.exc_info())
1478+
self.assertIn("global_for_suggestions?", err.getvalue())
1479+
1480+
def test_name_error_suggestions_do_not_trigger_for_long_attributes(self):
1481+
def f():
1482+
somethingverywronghehehehehehe = None
1483+
print(somethingverywronghe)
1484+
1485+
try:
1486+
f()
1487+
except NameError as exc:
1488+
with support.captured_stderr() as err:
1489+
sys.__excepthook__(*sys.exc_info())
1490+
1491+
self.assertNotIn("somethingverywronghehe", err.getvalue())
1492+
1493+
def test_name_error_suggestions_do_not_trigger_for_big_dicts(self):
1494+
def f():
1495+
# Mutating locals() is unreliable, so we need to do it by hand
1496+
a1 = a2 = a3 = a4 = a5 = a6 = a7 = a8 = a9 = a10 = a11 = a12 = a13 = \
1497+
a14 = a15 = a16 = a17 = a18 = a19 = a20 = a21 = a22 = a23 = a24 = a25 = \
1498+
a26 = a27 = a28 = a29 = a30 = a31 = a32 = a33 = a34 = a35 = a36 = a37 = \
1499+
a38 = a39 = a40 = a41 = a42 = a43 = a44 = a45 = a46 = a47 = a48 = a49 = \
1500+
a50 = a51 = a52 = a53 = a54 = a55 = a56 = a57 = a58 = a59 = a60 = a61 = \
1501+
a62 = a63 = a64 = a65 = a66 = a67 = a68 = a69 = a70 = a71 = a72 = a73 = \
1502+
a74 = a75 = a76 = a77 = a78 = a79 = a80 = a81 = a82 = a83 = a84 = a85 = \
1503+
a86 = a87 = a88 = a89 = a90 = a91 = a92 = a93 = a94 = a95 = a96 = a97 = \
1504+
a98 = a99 = a100 = a101 = a102 = a103 = None
1505+
print(a0)
1506+
1507+
try:
1508+
f()
1509+
except NameError as exc:
1510+
with support.captured_stderr() as err:
1511+
sys.__excepthook__(*sys.exc_info())
1512+
1513+
self.assertNotIn("a10", err.getvalue())
1514+
1515+
def test_name_error_with_custom_exceptions(self):
1516+
def f():
1517+
blech = None
1518+
raise NameError()
1519+
1520+
try:
1521+
f()
1522+
except NameError as exc:
1523+
with support.captured_stderr() as err:
1524+
sys.__excepthook__(*sys.exc_info())
1525+
1526+
self.assertNotIn("blech", err.getvalue())
1527+
1528+
def f():
1529+
blech = None
1530+
raise NameError
1531+
1532+
try:
1533+
f()
1534+
except NameError as exc:
1535+
with support.captured_stderr() as err:
1536+
sys.__excepthook__(*sys.exc_info())
1537+
1538+
self.assertNotIn("blech", err.getvalue())
14161539

14171540
class AttributeErrorTests(unittest.TestCase):
14181541
def test_attributes(self):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
When printing :exc:`NameError` raised by the interpreter,
2+
:c:func:`PyErr_Display` will offer suggestions of simmilar variable names in
3+
the function that the exception was raised from. Patch by Pablo Galindo

Objects/exceptions.c

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,8 +1326,69 @@ SimpleExtendsException(PyExc_RuntimeError, NotImplementedError,
13261326
/*
13271327
* NameError extends Exception
13281328
*/
1329-
SimpleExtendsException(PyExc_Exception, NameError,
1330-
"Name not found globally.");
1329+
1330+
static int
1331+
NameError_init(PyNameErrorObject *self, PyObject *args, PyObject *kwds)
1332+
{
1333+
static char *kwlist[] = {"name", NULL};
1334+
PyObject *name = NULL;
1335+
1336+
if (BaseException_init((PyBaseExceptionObject *)self, args, NULL) == -1) {
1337+
return -1;
1338+
}
1339+
1340+
PyObject *empty_tuple = PyTuple_New(0);
1341+
if (!empty_tuple) {
1342+
return -1;
1343+
}
1344+
if (!PyArg_ParseTupleAndKeywords(empty_tuple, kwds, "|$O:NameError", kwlist,
1345+
&name)) {
1346+
Py_DECREF(empty_tuple);
1347+
return -1;
1348+
}
1349+
Py_DECREF(empty_tuple);
1350+
1351+
Py_XINCREF(name);
1352+
Py_XSETREF(self->name, name);
1353+
1354+
return 0;
1355+
}
1356+
1357+
static int
1358+
NameError_clear(PyNameErrorObject *self)
1359+
{
1360+
Py_CLEAR(self->name);
1361+
return BaseException_clear((PyBaseExceptionObject *)self);
1362+
}
1363+
1364+
static void
1365+
NameError_dealloc(PyNameErrorObject *self)
1366+
{
1367+
_PyObject_GC_UNTRACK(self);
1368+
NameError_clear(self);
1369+
Py_TYPE(self)->tp_free((PyObject *)self);
1370+
}
1371+
1372+
static int
1373+
NameError_traverse(PyNameErrorObject *self, visitproc visit, void *arg)
1374+
{
1375+
Py_VISIT(self->name);
1376+
return BaseException_traverse((PyBaseExceptionObject *)self, visit, arg);
1377+
}
1378+
1379+
static PyMemberDef NameError_members[] = {
1380+
{"name", T_OBJECT, offsetof(PyNameErrorObject, name), 0, PyDoc_STR("name")},
1381+
{NULL} /* Sentinel */
1382+
};
1383+
1384+
static PyMethodDef NameError_methods[] = {
1385+
{NULL} /* Sentinel */
1386+
};
1387+
1388+
ComplexExtendsException(PyExc_Exception, NameError,
1389+
NameError, 0,
1390+
NameError_methods, NameError_members,
1391+
0, BaseException_str, "Name not found globally.");
13311392

13321393
/*
13331394
* UnboundLocalError extends NameError

Python/ceval.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6319,6 +6319,20 @@ format_exc_check_arg(PyThreadState *tstate, PyObject *exc,
63196319
return;
63206320

63216321
_PyErr_Format(tstate, exc, format_str, obj_str);
6322+
6323+
if (exc == PyExc_NameError) {
6324+
// Include the name in the NameError exceptions to offer suggestions later.
6325+
_Py_IDENTIFIER(name);
6326+
PyObject *type, *value, *traceback;
6327+
PyErr_Fetch(&type, &value, &traceback);
6328+
PyErr_NormalizeException(&type, &value, &traceback);
6329+
if (PyErr_GivenExceptionMatches(value, PyExc_NameError)) {
6330+
// We do not care if this fails because we are going to restore the
6331+
// NameError anyway.
6332+
_PyObject_SetAttrId(value, &PyId_name, obj);
6333+
}
6334+
PyErr_Restore(type, value, traceback);
6335+
}
63226336
}
63236337

63246338
static void

Python/suggestions.c

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
#include "Python.h"
2+
#include "frameobject.h"
23

34
#include "pycore_pyerrors.h"
45

56
#define MAX_DISTANCE 3
67
#define MAX_CANDIDATE_ITEMS 100
7-
#define MAX_STRING_SIZE 20
8+
#define MAX_STRING_SIZE 25
89

910
/* Calculate the Levenshtein distance between string1 and string2 */
1011
static size_t
1112
levenshtein_distance(const char *a, const char *b) {
12-
if (a == NULL || b == NULL) {
13-
return 0;
14-
}
1513

1614
const size_t a_size = strlen(a);
1715
const size_t b_size = strlen(b);
@@ -89,14 +87,19 @@ calculate_suggestions(PyObject *dir,
8987

9088
Py_ssize_t suggestion_distance = PyUnicode_GetLength(name);
9189
PyObject *suggestion = NULL;
90+
const char *name_str = PyUnicode_AsUTF8(name);
91+
if (name_str == NULL) {
92+
PyErr_Clear();
93+
return NULL;
94+
}
9295
for (int i = 0; i < dir_size; ++i) {
9396
PyObject *item = PyList_GET_ITEM(dir, i);
94-
const char *name_str = PyUnicode_AsUTF8(name);
95-
if (name_str == NULL) {
97+
const char *item_str = PyUnicode_AsUTF8(item);
98+
if (item_str == NULL) {
9699
PyErr_Clear();
97-
continue;
100+
return NULL;
98101
}
99-
Py_ssize_t current_distance = levenshtein_distance(PyUnicode_AsUTF8(name), PyUnicode_AsUTF8(item));
102+
Py_ssize_t current_distance = levenshtein_distance(name_str, item_str);
100103
if (current_distance == 0 || current_distance > MAX_DISTANCE) {
101104
continue;
102105
}
@@ -132,13 +135,57 @@ offer_suggestions_for_attribute_error(PyAttributeErrorObject *exc) {
132135
return suggestions;
133136
}
134137

138+
139+
static PyObject *
140+
offer_suggestions_for_name_error(PyNameErrorObject *exc) {
141+
PyObject *name = exc->name; // borrowed reference
142+
PyTracebackObject *traceback = (PyTracebackObject *) exc->traceback; // borrowed reference
143+
// Abort if we don't have an attribute name or we have an invalid one
144+
if (name == NULL || traceback == NULL || !PyUnicode_CheckExact(name)) {
145+
return NULL;
146+
}
147+
148+
// Move to the traceback of the exception
149+
while (traceback->tb_next != NULL) {
150+
traceback = traceback->tb_next;
151+
}
152+
153+
PyFrameObject *frame = traceback->tb_frame;
154+
assert(frame != NULL);
155+
PyCodeObject *code = frame->f_code;
156+
assert(code != NULL && code->co_varnames != NULL);
157+
PyObject *dir = PySequence_List(code->co_varnames);
158+
if (dir == NULL) {
159+
PyErr_Clear();
160+
return NULL;
161+
}
162+
163+
PyObject *suggestions = calculate_suggestions(dir, name);
164+
Py_DECREF(dir);
165+
if (suggestions != NULL) {
166+
return suggestions;
167+
}
168+
169+
dir = PySequence_List(frame->f_globals);
170+
if (dir == NULL) {
171+
PyErr_Clear();
172+
return NULL;
173+
}
174+
suggestions = calculate_suggestions(dir, name);
175+
Py_DECREF(dir);
176+
177+
return suggestions;
178+
}
179+
135180
// Offer suggestions for a given exception. Returns a python string object containing the
136181
// suggestions. This function does not raise exceptions and returns NULL if no suggestion was found.
137182
PyObject *_Py_Offer_Suggestions(PyObject *exception) {
138183
PyObject *result = NULL;
139184
assert(!PyErr_Occurred()); // Check that we are not going to clean any existing exception
140185
if (PyErr_GivenExceptionMatches(exception, PyExc_AttributeError)) {
141186
result = offer_suggestions_for_attribute_error((PyAttributeErrorObject *) exception);
187+
} else if (PyErr_GivenExceptionMatches(exception, PyExc_NameError)) {
188+
result = offer_suggestions_for_name_error((PyNameErrorObject *) exception);
142189
}
143190
assert(!PyErr_Occurred());
144191
return result;

0 commit comments

Comments
 (0)