bpo-38530: Offer suggestions on NameError (GH-25397)

When printing NameError raised by the interpreter, PyErr_Display
will offer suggestions of simmilar variable names in the function that the exception
was raised from:

    >>> schwarzschild_black_hole = None
    >>> schwarschild_black_hole
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    NameError: name 'schwarschild_black_hole' is not defined. Did you mean: schwarzschild_black_hole?
This commit is contained in:
Pablo Galindo 2021-04-14 15:10:33 +01:00 committed by GitHub
parent c4073a24f9
commit 5bf8bf2267
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 287 additions and 10 deletions

View file

@ -1,17 +1,15 @@
#include "Python.h"
#include "frameobject.h"
#include "pycore_pyerrors.h"
#define MAX_DISTANCE 3
#define MAX_CANDIDATE_ITEMS 100
#define MAX_STRING_SIZE 20
#define MAX_STRING_SIZE 25
/* Calculate the Levenshtein distance between string1 and string2 */
static size_t
levenshtein_distance(const char *a, const char *b) {
if (a == NULL || b == NULL) {
return 0;
}
const size_t a_size = strlen(a);
const size_t b_size = strlen(b);
@ -89,14 +87,19 @@ calculate_suggestions(PyObject *dir,
Py_ssize_t suggestion_distance = PyUnicode_GetLength(name);
PyObject *suggestion = NULL;
const char *name_str = PyUnicode_AsUTF8(name);
if (name_str == NULL) {
PyErr_Clear();
return NULL;
}
for (int i = 0; i < dir_size; ++i) {
PyObject *item = PyList_GET_ITEM(dir, i);
const char *name_str = PyUnicode_AsUTF8(name);
if (name_str == NULL) {
const char *item_str = PyUnicode_AsUTF8(item);
if (item_str == NULL) {
PyErr_Clear();
continue;
return NULL;
}
Py_ssize_t current_distance = levenshtein_distance(PyUnicode_AsUTF8(name), PyUnicode_AsUTF8(item));
Py_ssize_t current_distance = levenshtein_distance(name_str, item_str);
if (current_distance == 0 || current_distance > MAX_DISTANCE) {
continue;
}
@ -132,6 +135,48 @@ offer_suggestions_for_attribute_error(PyAttributeErrorObject *exc) {
return suggestions;
}
static PyObject *
offer_suggestions_for_name_error(PyNameErrorObject *exc) {
PyObject *name = exc->name; // borrowed reference
PyTracebackObject *traceback = (PyTracebackObject *) exc->traceback; // borrowed reference
// Abort if we don't have an attribute name or we have an invalid one
if (name == NULL || traceback == NULL || !PyUnicode_CheckExact(name)) {
return NULL;
}
// Move to the traceback of the exception
while (traceback->tb_next != NULL) {
traceback = traceback->tb_next;
}
PyFrameObject *frame = traceback->tb_frame;
assert(frame != NULL);
PyCodeObject *code = frame->f_code;
assert(code != NULL && code->co_varnames != NULL);
PyObject *dir = PySequence_List(code->co_varnames);
if (dir == NULL) {
PyErr_Clear();
return NULL;
}
PyObject *suggestions = calculate_suggestions(dir, name);
Py_DECREF(dir);
if (suggestions != NULL) {
return suggestions;
}
dir = PySequence_List(frame->f_globals);
if (dir == NULL) {
PyErr_Clear();
return NULL;
}
suggestions = calculate_suggestions(dir, name);
Py_DECREF(dir);
return suggestions;
}
// Offer suggestions for a given exception. Returns a python string object containing the
// suggestions. This function does not raise exceptions and returns NULL if no suggestion was found.
PyObject *_Py_Offer_Suggestions(PyObject *exception) {
@ -139,6 +184,8 @@ PyObject *_Py_Offer_Suggestions(PyObject *exception) {
assert(!PyErr_Occurred()); // Check that we are not going to clean any existing exception
if (PyErr_GivenExceptionMatches(exception, PyExc_AttributeError)) {
result = offer_suggestions_for_attribute_error((PyAttributeErrorObject *) exception);
} else if (PyErr_GivenExceptionMatches(exception, PyExc_NameError)) {
result = offer_suggestions_for_name_error((PyNameErrorObject *) exception);
}
assert(!PyErr_Occurred());
return result;