From c9c85b5906c295ba4e3cf876e9ab3cf7f3a55627 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Sat, 19 Oct 2019 19:57:49 +0100 Subject: [PATCH 1/7] bpo-38530: Offer suggestions on AttributeError Add News entry Make 'name' and 'obj' public attributes of AttributeError Fix various issues with the implementation Add more tests Simplify implementation and rename public function More cosmetic changes Add more tests Add more tests --- Include/cpython/pyerrors.h | 6 + Include/internal/pycore_suggestions.h | 13 + Include/pyerrors.h | 1 - Lib/test/test_exceptions.py | 159 ++++++++++++ Makefile.pre.in | 2 + .../2019-10-27-20-20-07.bpo-38530.ZyoDNn.rst | 3 + Objects/exceptions.c | 77 +++++- Objects/object.c | 47 +++- PCbuild/pythoncore.vcxproj | 2 + Python/pythonrun.c | 7 + Python/suggestions.c | 230 ++++++++++++++++++ 11 files changed, 537 insertions(+), 10 deletions(-) create mode 100644 Include/internal/pycore_suggestions.h create mode 100644 Misc/NEWS.d/next/Core and Builtins/2019-10-27-20-20-07.bpo-38530.ZyoDNn.rst create mode 100644 Python/suggestions.c diff --git a/Include/cpython/pyerrors.h b/Include/cpython/pyerrors.h index 6711e8be68ffeb..a15082e693cb90 100644 --- a/Include/cpython/pyerrors.h +++ b/Include/cpython/pyerrors.h @@ -62,6 +62,12 @@ typedef struct { PyObject *value; } PyStopIterationObject; +typedef struct { + PyException_HEAD + PyObject *obj; + PyObject *name; +} PyAttributeErrorObject; + /* Compatibility typedefs */ typedef PyOSErrorObject PyEnvironmentErrorObject; #ifdef MS_WINDOWS diff --git a/Include/internal/pycore_suggestions.h b/Include/internal/pycore_suggestions.h new file mode 100644 index 00000000000000..ac67f9242fdced --- /dev/null +++ b/Include/internal/pycore_suggestions.h @@ -0,0 +1,13 @@ +#include "Python.h" + +#ifndef Py_INTERNAL_SUGGESTIONS_H +#define Py_INTERNAL_SUGGESTIONS_H + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +int _Py_offer_suggestions_for_attribute_error(PyAttributeErrorObject* exception_value); + + +#endif /* !Py_INTERNAL_SUGGESTIONS_H */ \ No newline at end of file diff --git a/Include/pyerrors.h b/Include/pyerrors.h index f5d1c711577186..196e7ea47f6325 100644 --- a/Include/pyerrors.h +++ b/Include/pyerrors.h @@ -221,7 +221,6 @@ PyAPI_FUNC(PyObject *) PyErr_NewExceptionWithDoc( const char *name, const char *doc, PyObject *base, PyObject *dict); PyAPI_FUNC(void) PyErr_WriteUnraisable(PyObject *); - /* In signalmodule.c */ PyAPI_FUNC(int) PyErr_CheckSignals(void); PyAPI_FUNC(void) PyErr_SetInterrupt(void); diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 1e6f525cbb5092..e5ec71da98ee1f 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -1414,6 +1414,165 @@ class TestException(MemoryError): gc_collect() +class AttributeErrorTests(unittest.TestCase): + def test_attributes(self): + # Setting 'attr' should not be a problem. + exc = AttributeError('Ouch!') + self.assertIsNone(exc.name) + self.assertIsNone(exc.obj) + + sentinel = object() + exc = AttributeError('Ouch', name='carry', obj=sentinel) + self.assertEqual(exc.name, 'carry') + self.assertIs(exc.obj, sentinel) + + def test_getattr_has_name_and_obj(self): + class A: + blech = None + + obj = A() + try: + obj.bluch + except AttributeError as exc: + self.assertEqual("bluch", exc.name) + self.assertEqual(obj, exc.obj) + + def test_getattr_has_name_and_obj_for_method(self): + class A: + def blech(self): + return + + obj = A() + try: + obj.bluch() + except AttributeError as exc: + self.assertEqual("bluch", exc.name) + self.assertEqual(obj, exc.obj) + + def test_getattr_suggestions(self): + class Substitution: + noise = more_noise = a = bc = None + blech = None + + class Elimination: + noise = more_noise = a = bc = None + blch = None + + class Addition: + noise = more_noise = a = bc = None + bluchin = None + + class SubstitutionOverElimination: + blach = None + bluc = None + + class SubstitutionOverAddition: + blach = None + bluchi = None + + class EliminationOverAddition: + blucha = None + bluc = None + + for cls, suggestion in [(Substitution, "blech?"), + (Elimination, "blch?"), + (Addition, "bluchin?"), + (EliminationOverAddition, "bluc?"), + (SubstitutionOverElimination, "blach?"), + (SubstitutionOverAddition, "blach?")]: + try: + cls().bluch + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertIn(suggestion, err.getvalue()) + + def test_getattr_suggestions_do_not_trigger_for_long_attributes(self): + class A: + blech = None + + try: + A().somethingverywrong + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertNotIn("blech", err.getvalue()) + + def test_getattr_suggestions_do_not_trigger_for_big_dicts(self): + class A: + blech = None + # A class with a very big __dict__ will not be consider + # for suggestions. + for index in range(101): + setattr(A, f"index_{index}", None) + + try: + A().bluch + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertNotIn("blech", err.getvalue()) + + def test_getattr_suggestions_no_args(self): + class A: + blech = None + def __getattr__(self, attr): + raise AttributeError() + + try: + A().bluch + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertIn("blech", err.getvalue()) + + class A: + blech = None + def __getattr__(self, attr): + raise AttributeError + + try: + A().bluch + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertIn("blech", err.getvalue()) + + def test_getattr_suggestions_invalid_args(self): + class NonStringifyClass: + __str__ = None + __repr__ = None + + class A: + blech = None + def __getattr__(self, attr): + raise AttributeError(NonStringifyClass()) + + class B: + blech = None + def __getattr__(self, attr): + raise AttributeError("Error", 23) + + class C: + blech = None + def __getattr__(self, attr): + raise AttributeError(23) + + for cls in [A, B, C]: + try: + cls().bluch + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertNotIn("blech", err.getvalue()) + + class ImportErrorTests(unittest.TestCase): def test_attributes(self): diff --git a/Makefile.pre.in b/Makefile.pre.in index 365449d644583f..6118d9c4f0f86b 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -387,6 +387,7 @@ PYTHON_OBJS= \ Python/dtoa.o \ Python/formatter_unicode.o \ Python/fileutils.o \ + Python/suggestions.o \ Python/$(DYNLOADFILE) \ $(LIBOBJS) \ $(MACHDEP_OBJS) \ @@ -1161,6 +1162,7 @@ PYTHON_HEADERS= \ $(srcdir)/Include/internal/pycore_list.h \ $(srcdir)/Include/internal/pycore_long.h \ $(srcdir)/Include/internal/pycore_object.h \ + $(srcdir)/Include/internal/pycore_suggestions.h \ $(srcdir)/Include/internal/pycore_pathconfig.h \ $(srcdir)/Include/internal/pycore_pyarena.h \ $(srcdir)/Include/internal/pycore_pyerrors.h \ diff --git a/Misc/NEWS.d/next/Core and Builtins/2019-10-27-20-20-07.bpo-38530.ZyoDNn.rst b/Misc/NEWS.d/next/Core and Builtins/2019-10-27-20-20-07.bpo-38530.ZyoDNn.rst new file mode 100644 index 00000000000000..ce9e024baac59c --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2019-10-27-20-20-07.bpo-38530.ZyoDNn.rst @@ -0,0 +1,3 @@ +When printing :exc:`AttributeError`, :c:func:`PyErr_Display` will offer +suggestions of simmilar attribute names in the object that the exception was +raised from. diff --git a/Objects/exceptions.c b/Objects/exceptions.c index dfa069e01d9607..84638969e207d0 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1338,9 +1338,82 @@ SimpleExtendsException(PyExc_NameError, UnboundLocalError, /* * AttributeError extends Exception */ -SimpleExtendsException(PyExc_Exception, AttributeError, - "Attribute not found."); +static int +AttributeError_init(PyAttributeErrorObject *self, PyObject *args, PyObject *kwds) +{ + static char *kwlist[] = {"name", "obj", NULL}; + PyObject *name = NULL; + PyObject *obj = NULL; + + if (BaseException_init((PyBaseExceptionObject *)self, args, NULL) == -1) { + return -1; + } + + PyObject *empty_tuple = PyTuple_New(0); + if (!empty_tuple) { + return -1; + } + if (!PyArg_ParseTupleAndKeywords(empty_tuple, kwds, "|$OO:AttributeError", kwlist, + &name, &obj)) { + Py_DECREF(empty_tuple); + return -1; + } + Py_DECREF(empty_tuple); + + Py_XINCREF(name); + Py_XSETREF(self->name, name); + + Py_XINCREF(obj); + Py_XSETREF(self->obj, obj); + + return 0; +} + +static int +AttributeError_clear(PyAttributeErrorObject *self) +{ + Py_CLEAR(self->obj); + Py_CLEAR(self->name); + return BaseException_clear((PyBaseExceptionObject *)self); +} + +static void +AttributeError_dealloc(PyAttributeErrorObject *self) +{ + _PyObject_GC_UNTRACK(self); + AttributeError_clear(self); + Py_TYPE(self)->tp_free((PyObject *)self); +} + +static int +AttributeError_traverse(PyAttributeErrorObject *self, visitproc visit, void *arg) +{ + Py_VISIT(self->obj); + Py_VISIT(self->name); + return BaseException_traverse((PyBaseExceptionObject *)self, visit, arg); +} + +static PyObject * +AttributeError_str(PyAttributeErrorObject *self) +{ + return BaseException_str((PyBaseExceptionObject *)self); +} + +static PyMemberDef AttributeError_members[] = { + {"name", T_OBJECT, offsetof(PyAttributeErrorObject, name), 0, PyDoc_STR("attribute name")}, + {"obj", T_OBJECT, offsetof(PyAttributeErrorObject, obj), 0, PyDoc_STR("object")}, + {NULL} /* Sentinel */ +}; + +static PyMethodDef AttributeError_methods[] = { + {NULL} /* Sentinel */ +}; + +ComplexExtendsException(PyExc_Exception, AttributeError, + AttributeError, 0, + AttributeError_methods, AttributeError_members, + 0, AttributeError_str, "Attribute not found."); /* * SyntaxError extends Exception diff --git a/Objects/object.c b/Objects/object.c index 1224160dd50c46..e835bcbaa6d3ea 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -12,6 +12,7 @@ #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_symtable.h" // PySTEntry_Type #include "pycore_unionobject.h" // _Py_UnionType +#include "pycore_suggestions.h" #include "frameobject.h" #include "interpreteridobject.h" @@ -884,10 +885,31 @@ _PyObject_SetAttrId(PyObject *v, _Py_Identifier *name, PyObject *w) return result; } +static inline int +add_context_to_attribute_error_exception(PyObject* v, PyObject* name) +{ + assert(PyErr_Occurred()); + // Intercept AttributeError exceptions and augment them to offer + // suggestions later. + if (PyErr_ExceptionMatches(PyExc_AttributeError)){ + PyObject *type, *value, *traceback; + PyErr_Fetch(&type, &value, &traceback); + PyErr_NormalizeException(&type, &value, &traceback); + if (PyErr_GivenExceptionMatches(value, PyExc_AttributeError) && + (PyObject_SetAttrString(value, "name", name) || + PyObject_SetAttrString(value, "obj", v))) { + return 1; + } + PyErr_Restore(type, value, traceback); + } + return 0; +} + PyObject * PyObject_GetAttr(PyObject *v, PyObject *name) { PyTypeObject *tp = Py_TYPE(v); + PyObject* result = NULL; if (!PyUnicode_Check(name)) { PyErr_Format(PyExc_TypeError, @@ -896,17 +918,23 @@ PyObject_GetAttr(PyObject *v, PyObject *name) return NULL; } if (tp->tp_getattro != NULL) - return (*tp->tp_getattro)(v, name); - if (tp->tp_getattr != NULL) { + result = (*tp->tp_getattro)(v, name); + else if (tp->tp_getattr != NULL) { const char *name_str = PyUnicode_AsUTF8(name); if (name_str == NULL) return NULL; - return (*tp->tp_getattr)(v, (char *)name_str); + result = (*tp->tp_getattr)(v, (char *)name_str); + } else { + PyErr_Format(PyExc_AttributeError, + "'%.50s' object has no attribute '%U'", + tp->tp_name, name); } - PyErr_Format(PyExc_AttributeError, - "'%.50s' object has no attribute '%U'", - tp->tp_name, name); - return NULL; + + if (!result && add_context_to_attribute_error_exception(v, name)) { + return NULL; + } + + return result; } int @@ -1165,6 +1193,11 @@ _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method) PyErr_Format(PyExc_AttributeError, "'%.50s' object has no attribute '%U'", tp->tp_name, name); + + if (add_context_to_attribute_error_exception(obj, name)) { + return 0; + } + return 0; } diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index 2c1cc0d4cc80f7..f3a619db9ae6b0 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -215,6 +215,7 @@ + @@ -485,6 +486,7 @@ + diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 99be6295b48a7c..2dbb7fefc973bc 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -24,6 +24,7 @@ #include "errcode.h" // E_EOF #include "code.h" // PyCodeObject #include "marshal.h" // PyMarshal_ReadLongFromFile() +#include "pycore_suggestions.h" // _Py_offer_suggestions #ifdef MS_WINDOWS # include "malloc.h" // alloca() @@ -1080,6 +1081,12 @@ PyErr_Display(PyObject *exception, PyObject *value, PyObject *tb) if (file == Py_None) { return; } + + if (PyErr_GivenExceptionMatches(exception, PyExc_AttributeError) && + _Py_offer_suggestions_for_attribute_error((PyAttributeErrorObject*) value) != 0) { + PyErr_Clear(); + } + Py_INCREF(file); _PyErr_Display(file, exception, value, tb); Py_DECREF(file); diff --git a/Python/suggestions.c b/Python/suggestions.c new file mode 100644 index 00000000000000..472910961c15ee --- /dev/null +++ b/Python/suggestions.c @@ -0,0 +1,230 @@ +#include "Python.h" + +#include "pycore_suggestions.h" + +#define MAX_GETATTR_PREDICT_DIST 3 +#define MAX_GETATTR_PREDICT_ITEMS 100 +#define MAX_GETATTR_STRING_SIZE 20 + +static size_t +distance(const char *string1, const char *string2) +{ + Py_ssize_t len1 = strlen(string1); + Py_ssize_t len2 = strlen(string2); + Py_ssize_t i; + Py_ssize_t half; + size_t *row; + size_t *end; + + /* Get rid of the common prefix */ + while (len1 > 0 && len2 > 0 && *string1 == *string2) { + len1--; + len2--; + string1++; + string2++; + } + + /* strip common suffix */ + while (len1 > 0 && len2 > 0 && string1[len1-1] == string2[len2-1]) { + len1--; + len2--; + } + + /* catch trivial cases */ + if (len1 == 0) { + return len2; + } + if (len2 == 0) { + return len1; + } + + /* make the inner cycle (i.e. string2) the longer one */ + if (len1 > len2) { + size_t nx = len1; + const char *sx = string1; + len1 = len2; + len2 = nx; + string1 = string2; + string2 = sx; + } + /* check len1 == 1 separately */ + if (len1 == 1) { + return len2 - (memchr(string2, *string1, len2) != NULL); + } + len1++; + len2++; + half = len1 >> 1; + + /* initalize first row */ + row = (size_t*)PyMem_Malloc(len2*sizeof(size_t)); + if (!row) { + return (size_t)(-1); + } + end = row + len2 - 1; + for (i = 0; i < len2 - half; i++) { + row[i] = i; + } + + /* We don't have to scan two corner triangles (of size len1/2) + * in the matrix because no best path can go throught them. This is + * not true when len1 == len2 == 2 so the memchr() special case above is + * necessary */ + row[0] = len1 - half - 1; + for (i = 1; i < len1; i++) { + size_t *p; + const char char1 = string1[i - 1]; + const char *char2p; + size_t D, x; + /* skip the upper triangle */ + if (i >= len1 - half) { + size_t offset = i - (len1 - half); + size_t c3; + + char2p = string2 + offset; + p = row + offset; + c3 = *(p++) + (char1 != *(char2p++)); + x = *p; + x++; + D = x; + if (x > c3) + x = c3; + *(p++) = x; + } + else { + p = row + 1; + char2p = string2; + D = x = i; + } + /* skip the lower triangle */ + if (i <= half + 1) { + end = row + len2 + i - half - 2; + } + /* main */ + while (p <= end) { + size_t c3 = --D + (char1 != *(char2p++)); + x++; + if (x > c3) + x = c3; + D = *p; + D++; + if (x > D) + x = D; + *(p++) = x; + } + /* lower triangle sentinel */ + if (i <= half) { + size_t c3 = --D + (char1 != *char2p); + x++; + if (x > c3) + x = c3; + *p = x; + } + } + i = *end; + PyMem_Free(row); + return i; +} + +static inline PyObject* +calculate_suggestions(PyObject* dir, + PyObject* name, + PyObject* oldexceptionvalue) +{ + assert(PyList_CheckExact(dir)); + + Py_ssize_t dir_size = PyList_GET_SIZE(dir); + if (dir_size >= MAX_GETATTR_PREDICT_ITEMS) { + return NULL; + } + + int suggestion_distance = PyUnicode_GetLength(name); + PyObject* suggestion = 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) { + PyErr_Clear(); + continue; + } + int current_distance = distance(PyUnicode_AsUTF8(name), + PyUnicode_AsUTF8(item)); + if (current_distance > MAX_GETATTR_PREDICT_DIST){ + continue; + } + if (!suggestion || current_distance < suggestion_distance) { + suggestion = item; + suggestion_distance = current_distance; + } + } + if (!suggestion) { + return NULL; + } + return PyUnicode_FromFormat("%S\n\nDid you mean: %U?", + oldexceptionvalue, suggestion); +} + +int +_Py_offer_suggestions_for_attribute_error(PyAttributeErrorObject* exc) { + int return_val = 0; + + PyObject* name = exc->name; + PyObject* v = exc->obj; + + // Aboirt if we don't have an attribute name or we have an invalid one + if ((name == NULL) || (v == NULL) || !PyUnicode_CheckExact(name)) { + return -1; + } + + PyObject* oldexceptionvalue = NULL; + Py_ssize_t nargs = PyTuple_GET_SIZE(exc->args); + switch (nargs) { + case 0: + oldexceptionvalue = PyUnicode_New(0, 0); + break; + case 1: + oldexceptionvalue = PyTuple_GET_ITEM(exc->args, 0); + Py_INCREF(oldexceptionvalue); + // Check that the the message is an uncode objects that we can use. + if (!PyUnicode_CheckExact(oldexceptionvalue)) { + return_val = -1; + goto exit; + } + break; + default: + // Exceptions with more than 1 value in args are not + // formatted using args, so no need to make a new suggestion. + return 0; + } + + PyObject* dir = PyObject_Dir(v); + if (!dir) { + goto exit; + } + + PyObject* newexceptionvalue = calculate_suggestions(dir, name, oldexceptionvalue); + Py_DECREF(dir); + + if (newexceptionvalue == NULL) { + // We did't find a suggestion :( + goto exit; + } + + PyObject* old_args = exc->args; + PyObject* new_args = PyTuple_Pack(1, newexceptionvalue); + Py_DECREF(newexceptionvalue); + if (new_args == NULL) { + return_val = -1; + goto exit; + } + exc->args = new_args; + Py_DECREF(old_args); + +exit: + Py_DECREF(oldexceptionvalue); + + // Clear any error that may have happened. + PyErr_Clear(); + return return_val; +} + + From 61d510b520574d9d7a2cf6dbd324b188fea0a200 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Sat, 10 Apr 2021 22:31:36 +0100 Subject: [PATCH 2/7] Address Victor's feedback --- Doc/library/exceptions.rst | 5 +++++ Include/internal/pycore_suggestions.h | 5 ++--- Include/pyerrors.h | 1 + Objects/exceptions.c | 8 +------- Objects/object.c | 9 +++++---- Python/pythonrun.c | 5 ++--- Python/suggestions.c | 27 ++++++++++++++++++--------- 7 files changed, 34 insertions(+), 26 deletions(-) diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 4dea6701a6bfd0..9eeb61ede6b201 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -149,6 +149,11 @@ The following exceptions are the exceptions that are usually raised. assignment fails. (When an object does not support attribute references or attribute assignments at all, :exc:`TypeError` is raised.) + The :attr:`name` and :attr:`obj` attributes can be set using keyword-only + arguments to the constructor. When set they represent the name of the attribute + that was attempted to be accessed and the object that was accessed for said + attribute, respectively. + .. exception:: EOFError diff --git a/Include/internal/pycore_suggestions.h b/Include/internal/pycore_suggestions.h index ac67f9242fdced..cc2b0f5859af7d 100644 --- a/Include/internal/pycore_suggestions.h +++ b/Include/internal/pycore_suggestions.h @@ -7,7 +7,6 @@ # error "this header requires Py_BUILD_CORE define" #endif -int _Py_offer_suggestions_for_attribute_error(PyAttributeErrorObject* exception_value); +int _Py_offer_suggestions(PyObject* exception, PyObject* value); - -#endif /* !Py_INTERNAL_SUGGESTIONS_H */ \ No newline at end of file +#endif /* !Py_INTERNAL_SUGGESTIONS_H */ diff --git a/Include/pyerrors.h b/Include/pyerrors.h index 196e7ea47f6325..f5d1c711577186 100644 --- a/Include/pyerrors.h +++ b/Include/pyerrors.h @@ -221,6 +221,7 @@ PyAPI_FUNC(PyObject *) PyErr_NewExceptionWithDoc( const char *name, const char *doc, PyObject *base, PyObject *dict); PyAPI_FUNC(void) PyErr_WriteUnraisable(PyObject *); + /* In signalmodule.c */ PyAPI_FUNC(int) PyErr_CheckSignals(void); PyAPI_FUNC(void) PyErr_SetInterrupt(void); diff --git a/Objects/exceptions.c b/Objects/exceptions.c index 84638969e207d0..4bb415331161fa 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -1394,12 +1394,6 @@ AttributeError_traverse(PyAttributeErrorObject *self, visitproc visit, void *arg return BaseException_traverse((PyBaseExceptionObject *)self, visit, arg); } -static PyObject * -AttributeError_str(PyAttributeErrorObject *self) -{ - return BaseException_str((PyBaseExceptionObject *)self); -} - static PyMemberDef AttributeError_members[] = { {"name", T_OBJECT, offsetof(PyAttributeErrorObject, name), 0, PyDoc_STR("attribute name")}, {"obj", T_OBJECT, offsetof(PyAttributeErrorObject, obj), 0, PyDoc_STR("object")}, @@ -1413,7 +1407,7 @@ static PyMethodDef AttributeError_methods[] = { ComplexExtendsException(PyExc_Exception, AttributeError, AttributeError, 0, AttributeError_methods, AttributeError_members, - 0, AttributeError_str, "Attribute not found."); + 0, BaseException_str, "Attribute not found."); /* * SyntaxError extends Exception diff --git a/Objects/object.c b/Objects/object.c index e835bcbaa6d3ea..82d8295c672519 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -886,7 +886,7 @@ _PyObject_SetAttrId(PyObject *v, _Py_Identifier *name, PyObject *w) } static inline int -add_context_to_attribute_error_exception(PyObject* v, PyObject* name) +set_attribute_error_context(PyObject* v, PyObject* name) { assert(PyErr_Occurred()); // Intercept AttributeError exceptions and augment them to offer @@ -917,8 +917,9 @@ PyObject_GetAttr(PyObject *v, PyObject *name) Py_TYPE(name)->tp_name); return NULL; } - if (tp->tp_getattro != NULL) + if (tp->tp_getattro != NULL) { result = (*tp->tp_getattro)(v, name); + } else if (tp->tp_getattr != NULL) { const char *name_str = PyUnicode_AsUTF8(name); if (name_str == NULL) @@ -930,7 +931,7 @@ PyObject_GetAttr(PyObject *v, PyObject *name) tp->tp_name, name); } - if (!result && add_context_to_attribute_error_exception(v, name)) { + if (!result && set_attribute_error_context(v, name)) { return NULL; } @@ -1194,7 +1195,7 @@ _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method) "'%.50s' object has no attribute '%U'", tp->tp_name, name); - if (add_context_to_attribute_error_exception(obj, name)) { + if (set_attribute_error_context(obj, name)) { return 0; } diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 2dbb7fefc973bc..2e537c027922b7 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -1082,9 +1082,8 @@ PyErr_Display(PyObject *exception, PyObject *value, PyObject *tb) return; } - if (PyErr_GivenExceptionMatches(exception, PyExc_AttributeError) && - _Py_offer_suggestions_for_attribute_error((PyAttributeErrorObject*) value) != 0) { - PyErr_Clear(); + if (_Py_offer_suggestions(exception, value) != 0) { + return; } Py_INCREF(file); diff --git a/Python/suggestions.c b/Python/suggestions.c index 472910961c15ee..3e902d2708dda0 100644 --- a/Python/suggestions.c +++ b/Python/suggestions.c @@ -6,7 +6,8 @@ #define MAX_GETATTR_PREDICT_ITEMS 100 #define MAX_GETATTR_STRING_SIZE 20 -static size_t +/* Calculate the Levenshtein distance between string1 and string2 */ +static Py_ssize_t distance(const char *string1, const char *string2) { Py_ssize_t len1 = strlen(string1); @@ -137,7 +138,7 @@ calculate_suggestions(PyObject* dir, return NULL; } - int suggestion_distance = PyUnicode_GetLength(name); + Py_ssize_t suggestion_distance = PyUnicode_GetLength(name); PyObject* suggestion = NULL; for (int i = 0; i < dir_size; ++i) { PyObject *item = PyList_GET_ITEM(dir, i); @@ -146,25 +147,25 @@ calculate_suggestions(PyObject* dir, PyErr_Clear(); continue; } - int current_distance = distance(PyUnicode_AsUTF8(name), - PyUnicode_AsUTF8(item)); + Py_ssize_t current_distance = distance(PyUnicode_AsUTF8(name), + PyUnicode_AsUTF8(item)); if (current_distance > MAX_GETATTR_PREDICT_DIST){ continue; } if (!suggestion || current_distance < suggestion_distance) { suggestion = item; suggestion_distance = current_distance; - } + } } if (!suggestion) { return NULL; } - return PyUnicode_FromFormat("%S\n\nDid you mean: %U?", + return PyUnicode_FromFormat("%S. Did you mean: %U?", oldexceptionvalue, suggestion); } -int -_Py_offer_suggestions_for_attribute_error(PyAttributeErrorObject* exc) { +static int +offer_suggestions_for_attribute_error(PyAttributeErrorObject* exc) { int return_val = 0; PyObject* name = exc->name; @@ -174,7 +175,7 @@ _Py_offer_suggestions_for_attribute_error(PyAttributeErrorObject* exc) { if ((name == NULL) || (v == NULL) || !PyUnicode_CheckExact(name)) { return -1; } - + PyObject* oldexceptionvalue = NULL; Py_ssize_t nargs = PyTuple_GET_SIZE(exc->args); switch (nargs) { @@ -228,3 +229,11 @@ _Py_offer_suggestions_for_attribute_error(PyAttributeErrorObject* exc) { } +int _Py_offer_suggestions(PyObject* exception, PyObject* value) { + if (PyErr_GivenExceptionMatches(exception, PyExc_AttributeError) && + offer_suggestions_for_attribute_error((PyAttributeErrorObject*) value) != 0) { + PyErr_Clear(); + } + return 0; +} + From dbd88f086a61041178189c0fcf7911cfcc8f4390 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Sat, 10 Apr 2021 22:44:49 +0100 Subject: [PATCH 3/7] More --- Include/internal/pycore_suggestions.h | 2 +- Objects/object.c | 22 +++++----- Python/pythonrun.c | 4 +- Python/suggestions.c | 59 +++++++++++++++------------ 4 files changed, 46 insertions(+), 41 deletions(-) diff --git a/Include/internal/pycore_suggestions.h b/Include/internal/pycore_suggestions.h index cc2b0f5859af7d..88392f8e0da0b9 100644 --- a/Include/internal/pycore_suggestions.h +++ b/Include/internal/pycore_suggestions.h @@ -7,6 +7,6 @@ # error "this header requires Py_BUILD_CORE define" #endif -int _Py_offer_suggestions(PyObject* exception, PyObject* value); +int _Py_Offer_Suggestions(PyObject* exception, PyObject* value); #endif /* !Py_INTERNAL_SUGGESTIONS_H */ diff --git a/Objects/object.c b/Objects/object.c index 82d8295c672519..ba21a94bcfad23 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -889,6 +889,8 @@ static inline int set_attribute_error_context(PyObject* v, PyObject* name) { assert(PyErr_Occurred()); + _Py_IDENTIFIER(name); + _Py_IDENTIFIER(obj); // Intercept AttributeError exceptions and augment them to offer // suggestions later. if (PyErr_ExceptionMatches(PyExc_AttributeError)){ @@ -896,8 +898,8 @@ set_attribute_error_context(PyObject* v, PyObject* name) PyErr_Fetch(&type, &value, &traceback); PyErr_NormalizeException(&type, &value, &traceback); if (PyErr_GivenExceptionMatches(value, PyExc_AttributeError) && - (PyObject_SetAttrString(value, "name", name) || - PyObject_SetAttrString(value, "obj", v))) { + (_PyObject_SetAttrId(value, &PyId_name, name) || + _PyObject_SetAttrId(value, &PyId_obj, v))) { return 1; } PyErr_Restore(type, value, traceback); @@ -922,19 +924,20 @@ PyObject_GetAttr(PyObject *v, PyObject *name) } else if (tp->tp_getattr != NULL) { const char *name_str = PyUnicode_AsUTF8(name); - if (name_str == NULL) + if (name_str == NULL) { return NULL; + } result = (*tp->tp_getattr)(v, (char *)name_str); - } else { + } + else { PyErr_Format(PyExc_AttributeError, "'%.50s' object has no attribute '%U'", tp->tp_name, name); } - if (!result && set_attribute_error_context(v, name)) { - return NULL; + if (result == NULL) { + set_attribute_error_context(v, name); } - return result; } @@ -1195,10 +1198,7 @@ _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method) "'%.50s' object has no attribute '%U'", tp->tp_name, name); - if (set_attribute_error_context(obj, name)) { - return 0; - } - + set_attribute_error_context(obj, name); return 0; } diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 2e537c027922b7..ac59dc1fe790ab 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -24,7 +24,7 @@ #include "errcode.h" // E_EOF #include "code.h" // PyCodeObject #include "marshal.h" // PyMarshal_ReadLongFromFile() -#include "pycore_suggestions.h" // _Py_offer_suggestions +#include "pycore_suggestions.h" // _Py_Offer_Suggestions #ifdef MS_WINDOWS # include "malloc.h" // alloca() @@ -1082,7 +1082,7 @@ PyErr_Display(PyObject *exception, PyObject *value, PyObject *tb) return; } - if (_Py_offer_suggestions(exception, value) != 0) { + if (_Py_Offer_Suggestions(exception, value) != 0) { return; } diff --git a/Python/suggestions.c b/Python/suggestions.c index 3e902d2708dda0..efaa9f3ca21700 100644 --- a/Python/suggestions.c +++ b/Python/suggestions.c @@ -59,7 +59,7 @@ distance(const char *string1, const char *string2) /* initalize first row */ row = (size_t*)PyMem_Malloc(len2*sizeof(size_t)); if (!row) { - return (size_t)(-1); + return (Py_ssize_t)(-1); } end = row + len2 - 1; for (i = 0; i < len2 - half; i++) { @@ -72,7 +72,7 @@ distance(const char *string1, const char *string2) * necessary */ row[0] = len1 - half - 1; for (i = 1; i < len1; i++) { - size_t *p; + size_t *scan_ptr; const char char1 = string1[i - 1]; const char *char2p; size_t D, x; @@ -82,17 +82,18 @@ distance(const char *string1, const char *string2) size_t c3; char2p = string2 + offset; - p = row + offset; - c3 = *(p++) + (char1 != *(char2p++)); - x = *p; + scan_ptr = row + offset; + c3 = *(scan_ptr++) + (char1 != *(char2p++)); + x = *scan_ptr; x++; D = x; - if (x > c3) + if (x > c3) { x = c3; - *(p++) = x; + } + *(scan_ptr++) = x; } else { - p = row + 1; + scan_ptr = row + 1; char2p = string2; D = x = i; } @@ -101,24 +102,26 @@ distance(const char *string1, const char *string2) end = row + len2 + i - half - 2; } /* main */ - while (p <= end) { + while (scan_ptr <= end) { size_t c3 = --D + (char1 != *(char2p++)); x++; - if (x > c3) + if (x > c3) { x = c3; - D = *p; + } + D = *scan_ptr; D++; if (x > D) x = D; - *(p++) = x; + *(scan_ptr++) = x; } /* lower triangle sentinel */ if (i <= half) { size_t c3 = --D + (char1 != *char2p); x++; - if (x > c3) + if (x > c3) { x = c3; - *p = x; + } + *scan_ptr = x; } } i = *end; @@ -131,6 +134,7 @@ calculate_suggestions(PyObject* dir, PyObject* name, PyObject* oldexceptionvalue) { + assert(!PyErr_Occurred()); assert(PyList_CheckExact(dir)); Py_ssize_t dir_size = PyList_GET_SIZE(dir); @@ -166,13 +170,13 @@ calculate_suggestions(PyObject* dir, static int offer_suggestions_for_attribute_error(PyAttributeErrorObject* exc) { - int return_val = 0; + int return_val = -1; - PyObject* name = exc->name; - PyObject* v = exc->obj; + PyObject* name = exc->name; // borrowed reference + PyObject* obj = exc->obj; // borrowed reference - // Aboirt if we don't have an attribute name or we have an invalid one - if ((name == NULL) || (v == NULL) || !PyUnicode_CheckExact(name)) { + // Abort if we don't have an attribute name or we have an invalid one + if (name == NULL || obj == NULL || !PyUnicode_CheckExact(name)) { return -1; } @@ -181,13 +185,15 @@ offer_suggestions_for_attribute_error(PyAttributeErrorObject* exc) { switch (nargs) { case 0: oldexceptionvalue = PyUnicode_New(0, 0); + if (oldexceptionvalue == NULL) { + return -1; + } break; case 1: oldexceptionvalue = PyTuple_GET_ITEM(exc->args, 0); Py_INCREF(oldexceptionvalue); - // Check that the the message is an uncode objects that we can use. + // Check that the the message is an unicode objects that we can use. if (!PyUnicode_CheckExact(oldexceptionvalue)) { - return_val = -1; goto exit; } break; @@ -197,8 +203,8 @@ offer_suggestions_for_attribute_error(PyAttributeErrorObject* exc) { return 0; } - PyObject* dir = PyObject_Dir(v); - if (!dir) { + PyObject* dir = PyObject_Dir(obj); + if (dir == NULL) { goto exit; } @@ -210,15 +216,14 @@ offer_suggestions_for_attribute_error(PyAttributeErrorObject* exc) { goto exit; } - PyObject* old_args = exc->args; PyObject* new_args = PyTuple_Pack(1, newexceptionvalue); Py_DECREF(newexceptionvalue); if (new_args == NULL) { - return_val = -1; goto exit; } + Py_SETREF(exc->args, new_args); exc->args = new_args; - Py_DECREF(old_args); + return_val = 0; exit: Py_DECREF(oldexceptionvalue); @@ -229,7 +234,7 @@ offer_suggestions_for_attribute_error(PyAttributeErrorObject* exc) { } -int _Py_offer_suggestions(PyObject* exception, PyObject* value) { +int _Py_Offer_Suggestions(PyObject* exception, PyObject* value) { if (PyErr_GivenExceptionMatches(exception, PyExc_AttributeError) && offer_suggestions_for_attribute_error((PyAttributeErrorObject*) value) != 0) { PyErr_Clear(); From a3dd16a8f6ca4dad3d8969e22626b1a1cb8696b2 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Mon, 12 Apr 2021 17:18:42 +0100 Subject: [PATCH 4/7] Return the suggestion as a object and handle it in print_exception --- Include/internal/pycore_suggestions.h | 2 +- Lib/test/test_exceptions.py | 2 +- Python/pythonrun.c | 12 +++-- Python/suggestions.c | 73 +++++---------------------- 4 files changed, 24 insertions(+), 65 deletions(-) diff --git a/Include/internal/pycore_suggestions.h b/Include/internal/pycore_suggestions.h index 88392f8e0da0b9..1febb5e99eb599 100644 --- a/Include/internal/pycore_suggestions.h +++ b/Include/internal/pycore_suggestions.h @@ -7,6 +7,6 @@ # error "this header requires Py_BUILD_CORE define" #endif -int _Py_Offer_Suggestions(PyObject* exception, PyObject* value); +PyObject* _Py_Offer_Suggestions(PyObject* exception); #endif /* !Py_INTERNAL_SUGGESTIONS_H */ diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index e5ec71da98ee1f..a4812233fc5ef9 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -1570,7 +1570,7 @@ def __getattr__(self, attr): with support.captured_stderr() as err: sys.__excepthook__(*sys.exc_info()) - self.assertNotIn("blech", err.getvalue()) + self.assertIn("blech", err.getvalue()) class ImportErrorTests(unittest.TestCase): diff --git a/Python/pythonrun.c b/Python/pythonrun.c index ac59dc1fe790ab..479cabf65c0da6 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -954,6 +954,14 @@ print_exception(PyObject *f, PyObject *value) if (err < 0) { PyErr_Clear(); } + PyObject* suggestions = _Py_Offer_Suggestions(value); + if (suggestions) { + err = PyFile_WriteString(". ", f); + if (err == 0) { + err = PyFile_WriteObject(suggestions, f, Py_PRINT_RAW); + } + Py_DECREF(suggestions); + } err += PyFile_WriteString("\n", f); Py_XDECREF(tb); Py_DECREF(value); @@ -1082,10 +1090,6 @@ PyErr_Display(PyObject *exception, PyObject *value, PyObject *tb) return; } - if (_Py_Offer_Suggestions(exception, value) != 0) { - return; - } - Py_INCREF(file); _PyErr_Display(file, exception, value, tb); Py_DECREF(file); diff --git a/Python/suggestions.c b/Python/suggestions.c index efaa9f3ca21700..d1974048a7971f 100644 --- a/Python/suggestions.c +++ b/Python/suggestions.c @@ -131,8 +131,7 @@ distance(const char *string1, const char *string2) static inline PyObject* calculate_suggestions(PyObject* dir, - PyObject* name, - PyObject* oldexceptionvalue) + PyObject* name) { assert(!PyErr_Occurred()); assert(PyList_CheckExact(dir)); @@ -164,81 +163,37 @@ calculate_suggestions(PyObject* dir, if (!suggestion) { return NULL; } - return PyUnicode_FromFormat("%S. Did you mean: %U?", - oldexceptionvalue, suggestion); + return PyUnicode_FromFormat("Did you mean: %U?", suggestion); } -static int +static PyObject* offer_suggestions_for_attribute_error(PyAttributeErrorObject* exc) { - int return_val = -1; - PyObject* name = exc->name; // borrowed reference PyObject* obj = exc->obj; // borrowed reference // Abort if we don't have an attribute name or we have an invalid one if (name == NULL || obj == NULL || !PyUnicode_CheckExact(name)) { - return -1; - } - - PyObject* oldexceptionvalue = NULL; - Py_ssize_t nargs = PyTuple_GET_SIZE(exc->args); - switch (nargs) { - case 0: - oldexceptionvalue = PyUnicode_New(0, 0); - if (oldexceptionvalue == NULL) { - return -1; - } - break; - case 1: - oldexceptionvalue = PyTuple_GET_ITEM(exc->args, 0); - Py_INCREF(oldexceptionvalue); - // Check that the the message is an unicode objects that we can use. - if (!PyUnicode_CheckExact(oldexceptionvalue)) { - goto exit; - } - break; - default: - // Exceptions with more than 1 value in args are not - // formatted using args, so no need to make a new suggestion. - return 0; + return NULL; } PyObject* dir = PyObject_Dir(obj); if (dir == NULL) { - goto exit; + return NULL; } - PyObject* newexceptionvalue = calculate_suggestions(dir, name, oldexceptionvalue); + PyObject* suggestions = calculate_suggestions(dir, name); Py_DECREF(dir); - - if (newexceptionvalue == NULL) { - // We did't find a suggestion :( - goto exit; - } - - PyObject* new_args = PyTuple_Pack(1, newexceptionvalue); - Py_DECREF(newexceptionvalue); - if (new_args == NULL) { - goto exit; - } - Py_SETREF(exc->args, new_args); - exc->args = new_args; - return_val = 0; - -exit: - Py_DECREF(oldexceptionvalue); - - // Clear any error that may have happened. - PyErr_Clear(); - return return_val; + return suggestions; } -int _Py_Offer_Suggestions(PyObject* exception, PyObject* value) { - if (PyErr_GivenExceptionMatches(exception, PyExc_AttributeError) && - offer_suggestions_for_attribute_error((PyAttributeErrorObject*) value) != 0) { - PyErr_Clear(); +// Offer suggestions for a given exception. This function does not raise exceptions +// and returns NULL if no exception was found. +PyObject* _Py_Offer_Suggestions(PyObject* exception) { + assert(!PyErr_Occurred()); + if (PyErr_GivenExceptionMatches(exception, PyExc_AttributeError)) { + return offer_suggestions_for_attribute_error((PyAttributeErrorObject*) exception); } - return 0; + return NULL; } From c760303cb60b2caec2d3c2539b8bc200d8429b78 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Wed, 14 Apr 2021 01:08:25 +0100 Subject: [PATCH 5/7] fixup! Return the suggestion as a object and handle it in print_exception --- Doc/library/exceptions.rst | 2 ++ Include/internal/pycore_pyerrors.h | 2 ++ Include/internal/pycore_suggestions.h | 12 ------------ Makefile.pre.in | 1 - Objects/object.c | 5 ++--- PCbuild/pythoncore.vcxproj | 1 - Python/pythonrun.c | 8 ++++---- Python/suggestions.c | 23 +++++++++++++---------- 8 files changed, 23 insertions(+), 31 deletions(-) delete mode 100644 Include/internal/pycore_suggestions.h diff --git a/Doc/library/exceptions.rst b/Doc/library/exceptions.rst index 9eeb61ede6b201..8fdd6ebecfa69e 100644 --- a/Doc/library/exceptions.rst +++ b/Doc/library/exceptions.rst @@ -154,6 +154,8 @@ The following exceptions are the exceptions that are usually raised. that was attempted to be accessed and the object that was accessed for said attribute, respectively. + .. versionchanged:: 3.10 + Added the :attr:`name` and :attr:`obj` attributes. .. exception:: EOFError diff --git a/Include/internal/pycore_pyerrors.h b/Include/internal/pycore_pyerrors.h index 9dd66aec9c3d78..d1af8e91b3b908 100644 --- a/Include/internal/pycore_pyerrors.h +++ b/Include/internal/pycore_pyerrors.h @@ -86,6 +86,8 @@ PyAPI_FUNC(int) _PyErr_CheckSignalsTstate(PyThreadState *tstate); PyAPI_FUNC(void) _Py_DumpExtensionModules(int fd, PyInterpreterState *interp); +extern PyObject* _Py_Offer_Suggestions(PyObject* exception); + #ifdef __cplusplus } #endif diff --git a/Include/internal/pycore_suggestions.h b/Include/internal/pycore_suggestions.h deleted file mode 100644 index 1febb5e99eb599..00000000000000 --- a/Include/internal/pycore_suggestions.h +++ /dev/null @@ -1,12 +0,0 @@ -#include "Python.h" - -#ifndef Py_INTERNAL_SUGGESTIONS_H -#define Py_INTERNAL_SUGGESTIONS_H - -#ifndef Py_BUILD_CORE -# error "this header requires Py_BUILD_CORE define" -#endif - -PyObject* _Py_Offer_Suggestions(PyObject* exception); - -#endif /* !Py_INTERNAL_SUGGESTIONS_H */ diff --git a/Makefile.pre.in b/Makefile.pre.in index 6118d9c4f0f86b..eccc72697704b8 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -1162,7 +1162,6 @@ PYTHON_HEADERS= \ $(srcdir)/Include/internal/pycore_list.h \ $(srcdir)/Include/internal/pycore_long.h \ $(srcdir)/Include/internal/pycore_object.h \ - $(srcdir)/Include/internal/pycore_suggestions.h \ $(srcdir)/Include/internal/pycore_pathconfig.h \ $(srcdir)/Include/internal/pycore_pyarena.h \ $(srcdir)/Include/internal/pycore_pyerrors.h \ diff --git a/Objects/object.c b/Objects/object.c index ba21a94bcfad23..74b28dc12b6892 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -12,7 +12,6 @@ #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_symtable.h" // PySTEntry_Type #include "pycore_unionobject.h" // _Py_UnionType -#include "pycore_suggestions.h" #include "frameobject.h" #include "interpreteridobject.h" @@ -911,14 +910,14 @@ PyObject * PyObject_GetAttr(PyObject *v, PyObject *name) { PyTypeObject *tp = Py_TYPE(v); - PyObject* result = NULL; - if (!PyUnicode_Check(name)) { PyErr_Format(PyExc_TypeError, "attribute name must be string, not '%.200s'", Py_TYPE(name)->tp_name); return NULL; } + + PyObject* result = NULL; if (tp->tp_getattro != NULL) { result = (*tp->tp_getattro)(v, name); } diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index f3a619db9ae6b0..3c4785c077e6a1 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -215,7 +215,6 @@ - diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 479cabf65c0da6..321b04eb724ed4 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -15,7 +15,7 @@ #include "pycore_interp.h" // PyInterpreterState.importlib #include "pycore_object.h" // _PyDebug_PrintTotalRefs() #include "pycore_parser.h" // _PyParser_ASTFromString() -#include "pycore_pyerrors.h" // _PyErr_Fetch +#include "pycore_pyerrors.h" // _PyErr_Fetch, _Py_Offer_Suggestions #include "pycore_pylifecycle.h" // _Py_UnhandledKeyboardInterrupt #include "pycore_pystate.h" // _PyInterpreterState_GET() #include "pycore_sysmodule.h" // _PySys_Audit() @@ -24,7 +24,6 @@ #include "errcode.h" // E_EOF #include "code.h" // PyCodeObject #include "marshal.h" // PyMarshal_ReadLongFromFile() -#include "pycore_suggestions.h" // _Py_Offer_Suggestions #ifdef MS_WINDOWS # include "malloc.h" // alloca() @@ -956,9 +955,11 @@ print_exception(PyObject *f, PyObject *value) } PyObject* suggestions = _Py_Offer_Suggestions(value); if (suggestions) { - err = PyFile_WriteString(". ", f); + // Add a trailer ". Did you mean: (...)?" + err = PyFile_WriteString(". Did you mean: ", f); if (err == 0) { err = PyFile_WriteObject(suggestions, f, Py_PRINT_RAW); + err += PyFile_WriteString("?", f); } Py_DECREF(suggestions); } @@ -1089,7 +1090,6 @@ PyErr_Display(PyObject *exception, PyObject *value, PyObject *tb) if (file == Py_None) { return; } - Py_INCREF(file); _PyErr_Display(file, exception, value, tb); Py_DECREF(file); diff --git a/Python/suggestions.c b/Python/suggestions.c index d1974048a7971f..afb53aca864bc7 100644 --- a/Python/suggestions.c +++ b/Python/suggestions.c @@ -1,6 +1,6 @@ #include "Python.h" -#include "pycore_suggestions.h" +#include "pycore_pyerrors.h" #define MAX_GETATTR_PREDICT_DIST 3 #define MAX_GETATTR_PREDICT_ITEMS 100 @@ -8,7 +8,7 @@ /* Calculate the Levenshtein distance between string1 and string2 */ static Py_ssize_t -distance(const char *string1, const char *string2) +levenshtein_distance(const char *string1, const char *string2) { Py_ssize_t len1 = strlen(string1); Py_ssize_t len2 = strlen(string2); @@ -150,8 +150,8 @@ calculate_suggestions(PyObject* dir, PyErr_Clear(); continue; } - Py_ssize_t current_distance = distance(PyUnicode_AsUTF8(name), - PyUnicode_AsUTF8(item)); + Py_ssize_t current_distance = levenshtein_distance(PyUnicode_AsUTF8(name), + PyUnicode_AsUTF8(item)); if (current_distance > MAX_GETATTR_PREDICT_DIST){ continue; } @@ -163,7 +163,8 @@ calculate_suggestions(PyObject* dir, if (!suggestion) { return NULL; } - return PyUnicode_FromFormat("Did you mean: %U?", suggestion); + Py_INCREF(suggestion); + return suggestion; } static PyObject* @@ -187,13 +188,15 @@ offer_suggestions_for_attribute_error(PyAttributeErrorObject* exc) { } -// Offer suggestions for a given exception. This function does not raise exceptions -// and returns NULL if no exception was found. +// 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) { - assert(!PyErr_Occurred()); + PyObject* result = NULL; + assert(!PyErr_Occurred()); // Check that we are not going to clean any existing exception if (PyErr_GivenExceptionMatches(exception, PyExc_AttributeError)) { - return offer_suggestions_for_attribute_error((PyAttributeErrorObject*) exception); + result = offer_suggestions_for_attribute_error((PyAttributeErrorObject*) exception); } - return NULL; + assert(!PyErr_Occurred()); + return result; } From 71e8766385d8fc48ac3cccba921a1060b00687d1 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Wed, 14 Apr 2021 01:44:55 +0100 Subject: [PATCH 6/7] Simplify the algorithm implementation --- Python/suggestions.c | 200 ++++++++++++++++--------------------------- 1 file changed, 72 insertions(+), 128 deletions(-) diff --git a/Python/suggestions.c b/Python/suggestions.c index afb53aca864bc7..2c0858d558d00c 100644 --- a/Python/suggestions.c +++ b/Python/suggestions.c @@ -2,147 +2,93 @@ #include "pycore_pyerrors.h" -#define MAX_GETATTR_PREDICT_DIST 3 -#define MAX_GETATTR_PREDICT_ITEMS 100 -#define MAX_GETATTR_STRING_SIZE 20 +#define MAX_DISTANCE 3 +#define MAX_CANDIDATE_ITEMS 100 +#define MAX_STRING_SIZE 20 /* Calculate the Levenshtein distance between string1 and string2 */ -static Py_ssize_t -levenshtein_distance(const char *string1, const char *string2) -{ - Py_ssize_t len1 = strlen(string1); - Py_ssize_t len2 = strlen(string2); - Py_ssize_t i; - Py_ssize_t half; - size_t *row; - size_t *end; - - /* Get rid of the common prefix */ - while (len1 > 0 && len2 > 0 && *string1 == *string2) { - len1--; - len2--; - string1++; - string2++; +static size_t +levenshtein_distance(const char *a, const char *b) { + if (a == NULL || b == NULL) { + return 0; } - /* strip common suffix */ - while (len1 > 0 && len2 > 0 && string1[len1-1] == string2[len2-1]) { - len1--; - len2--; - } + const size_t a_size = strlen(a); + const size_t b_size = strlen(b); - /* catch trivial cases */ - if (len1 == 0) { - return len2; + if (a_size > MAX_STRING_SIZE || b_size > MAX_STRING_SIZE) { + return 0; } - if (len2 == 0) { - return len1; + + // Both strings are the same (by identity) + if (a == b) { + return 0; } - /* make the inner cycle (i.e. string2) the longer one */ - if (len1 > len2) { - size_t nx = len1; - const char *sx = string1; - len1 = len2; - len2 = nx; - string1 = string2; - string2 = sx; + // The first string is empty + if (a_size == 0) { + return b_size; } - /* check len1 == 1 separately */ - if (len1 == 1) { - return len2 - (memchr(string2, *string1, len2) != NULL); + + // The second string is empty + if (b_size == 0) { + return a_size; } - len1++; - len2++; - half = len1 >> 1; - - /* initalize first row */ - row = (size_t*)PyMem_Malloc(len2*sizeof(size_t)); - if (!row) { - return (Py_ssize_t)(-1); + + size_t *buffer = PyMem_Calloc(a_size, sizeof(size_t)); + if (buffer == NULL) { + return 0; } - end = row + len2 - 1; - for (i = 0; i < len2 - half; i++) { - row[i] = i; + + // Initialize the buffer row + size_t index = 0; + while (index < a_size) { + buffer[index] = index + 1; + index++; } - /* We don't have to scan two corner triangles (of size len1/2) - * in the matrix because no best path can go throught them. This is - * not true when len1 == len2 == 2 so the memchr() special case above is - * necessary */ - row[0] = len1 - half - 1; - for (i = 1; i < len1; i++) { - size_t *scan_ptr; - const char char1 = string1[i - 1]; - const char *char2p; - size_t D, x; - /* skip the upper triangle */ - if (i >= len1 - half) { - size_t offset = i - (len1 - half); - size_t c3; - - char2p = string2 + offset; - scan_ptr = row + offset; - c3 = *(scan_ptr++) + (char1 != *(char2p++)); - x = *scan_ptr; - x++; - D = x; - if (x > c3) { - x = c3; + size_t b_index = 0; + size_t result = 0; + while (b_index < b_size) { + char code = b[b_index]; + size_t distance = result = b_index++; + index = SIZE_MAX; + while (++index < a_size) { + size_t b_distance = code == a[index] ? distance : distance + 1; + distance = buffer[index]; + if (distance > result) { + if (b_distance > result) { + result = result + 1; + } else { + result = b_distance; + } + } else { + if (b_distance > distance) { + result = distance + 1; + } else { + result = b_distance; + } } - *(scan_ptr++) = x; - } - else { - scan_ptr = row + 1; - char2p = string2; - D = x = i; - } - /* skip the lower triangle */ - if (i <= half + 1) { - end = row + len2 + i - half - 2; - } - /* main */ - while (scan_ptr <= end) { - size_t c3 = --D + (char1 != *(char2p++)); - x++; - if (x > c3) { - x = c3; - } - D = *scan_ptr; - D++; - if (x > D) - x = D; - *(scan_ptr++) = x; - } - /* lower triangle sentinel */ - if (i <= half) { - size_t c3 = --D + (char1 != *char2p); - x++; - if (x > c3) { - x = c3; - } - *scan_ptr = x; + buffer[index] = result; } } - i = *end; - PyMem_Free(row); - return i; + PyMem_Free(buffer); + return result; } -static inline PyObject* -calculate_suggestions(PyObject* dir, - PyObject* name) -{ +static inline PyObject * +calculate_suggestions(PyObject *dir, + PyObject *name) { assert(!PyErr_Occurred()); assert(PyList_CheckExact(dir)); Py_ssize_t dir_size = PyList_GET_SIZE(dir); - if (dir_size >= MAX_GETATTR_PREDICT_ITEMS) { + if (dir_size >= MAX_CANDIDATE_ITEMS) { return NULL; } Py_ssize_t suggestion_distance = PyUnicode_GetLength(name); - PyObject* suggestion = NULL; + PyObject *suggestion = NULL; for (int i = 0; i < dir_size; ++i) { PyObject *item = PyList_GET_ITEM(dir, i); const char *name_str = PyUnicode_AsUTF8(name); @@ -150,9 +96,8 @@ calculate_suggestions(PyObject* dir, PyErr_Clear(); continue; } - Py_ssize_t current_distance = levenshtein_distance(PyUnicode_AsUTF8(name), - PyUnicode_AsUTF8(item)); - if (current_distance > MAX_GETATTR_PREDICT_DIST){ + Py_ssize_t current_distance = levenshtein_distance(PyUnicode_AsUTF8(name), PyUnicode_AsUTF8(item)); + if (current_distance == 0 || current_distance > MAX_DISTANCE) { continue; } if (!suggestion || current_distance < suggestion_distance) { @@ -167,34 +112,33 @@ calculate_suggestions(PyObject* dir, return suggestion; } -static PyObject* -offer_suggestions_for_attribute_error(PyAttributeErrorObject* exc) { - PyObject* name = exc->name; // borrowed reference - PyObject* obj = exc->obj; // borrowed reference +static PyObject * +offer_suggestions_for_attribute_error(PyAttributeErrorObject *exc) { + PyObject *name = exc->name; // borrowed reference + PyObject *obj = exc->obj; // borrowed reference // Abort if we don't have an attribute name or we have an invalid one if (name == NULL || obj == NULL || !PyUnicode_CheckExact(name)) { return NULL; } - PyObject* dir = PyObject_Dir(obj); + PyObject *dir = PyObject_Dir(obj); if (dir == NULL) { return NULL; } - PyObject* suggestions = calculate_suggestions(dir, name); + PyObject *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) { - PyObject* result = NULL; +PyObject *_Py_Offer_Suggestions(PyObject *exception) { + PyObject *result = NULL; 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); + result = offer_suggestions_for_attribute_error((PyAttributeErrorObject *) exception); } assert(!PyErr_Occurred()); return result; From 45061e48d815a96d1f01c11b93ed28effdb52a50 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Wed, 14 Apr 2021 02:13:39 +0100 Subject: [PATCH 7/7] Update docs --- Doc/whatsnew/3.10.rst | 24 +++++++++++++++++-- .../2019-10-27-20-20-07.bpo-38530.ZyoDNn.rst | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 7cf55767657480..c9de5d59ff2285 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -125,8 +125,11 @@ Check :pep:`617` for more details. in :issue:`12782` and :issue:`40334`.) -Better error messages in the parser ------------------------------------ +Better error messages +--------------------- + +SyntaxErrors +~~~~~~~~~~~~ When parsing code that contains unclosed parentheses or brackets the interpreter now includes the location of the unclosed bracket of parentheses instead of displaying @@ -167,6 +170,23 @@ These improvements are inspired by previous work in the PyPy interpreter. (Contributed by Pablo Galindo in :issue:`42864` and Batuhan Taskaya in :issue:`40176`.) + +AttributeErrors +~~~~~~~~~~~~~~~ + +When printing :exc:`AttributeError`, :c:func:`PyErr_Display` will offer +suggestions of simmilar attribute names in the object that the exception was +raised from: + +.. code-block:: python + + >>> collections.namedtoplo + Traceback (most recent call last): + File "", line 1, in + AttributeError: module 'collections' has no attribute 'namedtoplo'. Did you mean: namedtuple? + +(Contributed by Pablo Galindo in :issue:`38530`.) + PEP 626: Precise line numbers for debugging and other tools ----------------------------------------------------------- diff --git a/Misc/NEWS.d/next/Core and Builtins/2019-10-27-20-20-07.bpo-38530.ZyoDNn.rst b/Misc/NEWS.d/next/Core and Builtins/2019-10-27-20-20-07.bpo-38530.ZyoDNn.rst index ce9e024baac59c..09c73eae77def5 100644 --- a/Misc/NEWS.d/next/Core and Builtins/2019-10-27-20-20-07.bpo-38530.ZyoDNn.rst +++ b/Misc/NEWS.d/next/Core and Builtins/2019-10-27-20-20-07.bpo-38530.ZyoDNn.rst @@ -1,3 +1,3 @@ When printing :exc:`AttributeError`, :c:func:`PyErr_Display` will offer suggestions of simmilar attribute names in the object that the exception was -raised from. +raised from. Patch by Pablo Galindo