diff --git a/Include/internal/pycore_suggestions.h b/Include/internal/pycore_suggestions.h new file mode 100644 index 00000000000000..a28d85c9ecb5fc --- /dev/null +++ b/Include/internal/pycore_suggestions.h @@ -0,0 +1,12 @@ +#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(PyObject* v, PyObject* name); + +#endif /* !Py_INTERNAL_SUGGESTIONS_H */ \ No newline at end of file diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index 456f1be30be046..c6672ff7f9859a 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -516,7 +516,7 @@ class A: try: A().a # Raised AttributeError: A instance has no attribute 'a' except AttributeError as x: - if str(x) != "booh": + if not str(x).startswith("booh"): self.fail("attribute error for A().a got masked: %s" % x) class E: @@ -534,6 +534,30 @@ class I: else: self.fail("attribute error for I.__init__ got masked") + def test_getattr_suggestions(self): + class A: + blech = None + + try: + A().bluch + except AttributeError as x: + self.assertIn("blech", str(x)) + + try: + A().somethingverywrong + except AttributeError as x: + self.assertNotIn("blech", str(x)) + + # 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 x: + self.assertNotIn("blech", str(x)) + def assertNotOrderable(self, a, b): with self.assertRaises(TypeError): a < b diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 796e60a7704795..73bd87f62c81d1 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -4582,11 +4582,11 @@ class C(object): __getattr__ = descr self.assertRaises(AttributeError, getattr, A(), "attr") - self.assertEqual(descr.counter, 1) + self.assertEqual(descr.counter, 3) self.assertRaises(AttributeError, getattr, B(), "attr") - self.assertEqual(descr.counter, 2) - self.assertRaises(AttributeError, getattr, C(), "attr") self.assertEqual(descr.counter, 4) + self.assertRaises(AttributeError, getattr, C(), "attr") + self.assertEqual(descr.counter, 10) class EvilGetattribute(object): # This used to segfault diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index a48132c5b1cb5b..c5808d3e96df92 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -714,6 +714,7 @@ def __dir__(self): return object.__dir__(self) extras = self._mock_methods or [] + extras = list(extras) from_type = dir(type(self)) from_dict = list(self.__dict__) from_child_mocks = [ diff --git a/Makefile.pre.in b/Makefile.pre.in index 1c0958ec974b25..036062e615c76a 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -368,6 +368,7 @@ PYTHON_OBJS= \ Python/dtoa.o \ Python/formatter_unicode.o \ Python/fileutils.o \ + Python/suggestions.o \ Python/$(DYNLOADFILE) \ $(LIBOBJS) \ $(MACHDEP_OBJS) \ @@ -1086,6 +1087,7 @@ PYTHON_HEADERS= \ $(srcdir)/Include/internal/pycore_import.h \ $(srcdir)/Include/internal/pycore_initconfig.h \ $(srcdir)/Include/internal/pycore_object.h \ + $(srcdir)/Include/internal/pycore_suggestions.h \ $(srcdir)/Include/internal/pycore_pathconfig.h \ $(srcdir)/Include/internal/pycore_pyerrors.h \ $(srcdir)/Include/internal/pycore_pyhash.h \ diff --git a/Objects/object.c b/Objects/object.c index 2c8e823f05ee94..3f9fd3f622f712 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -6,6 +6,7 @@ #include "pycore_object.h" #include "pycore_pystate.h" #include "pycore_context.h" +#include "pycore_suggestions.h" #include "frameobject.h" #include "interpreteridobject.h" @@ -930,6 +931,7 @@ PyObject * PyObject_GetAttr(PyObject *v, PyObject *name) { PyTypeObject *tp = Py_TYPE(v); + PyObject* result = NULL; if (!PyUnicode_Check(name)) { PyErr_Format(PyExc_TypeError, @@ -938,17 +940,30 @@ 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; + + // xxx use thread local storage for this thing + static int should_offer_suggestions = 1; + if (!result && should_offer_suggestions && PyErr_ExceptionMatches(PyExc_AttributeError)) { + should_offer_suggestions = 0; + int ret = _Py_offer_suggestions(v, name); + should_offer_suggestions = 1; + if (ret == -1) { + return NULL; + } + } + + return result; } int diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index 1c055b6a334304..696b7c8d3a7ca4 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -179,6 +179,7 @@ + @@ -462,6 +463,7 @@ + diff --git a/Python/suggestions.c b/Python/suggestions.c new file mode 100644 index 00000000000000..897125cc077744 --- /dev/null +++ b/Python/suggestions.c @@ -0,0 +1,176 @@ +#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; +} + +int _Py_offer_suggestions(PyObject* v, PyObject* name){ + PyObject *oldexceptiontype, *oldexceptionvalue, *oldtraceback; + PyErr_Fetch(&oldexceptiontype, &oldexceptionvalue, &oldtraceback); + if (Py_EnterRecursiveCall(" while getting the __dir__ of an object")) { + return -1; + } + PyObject* dir = PyObject_Dir(v); + Py_LeaveRecursiveCall(); + PyObject* newexceptionvalue = oldexceptionvalue; + if (dir) { + if (!PyList_CheckExact(dir)) { + return -1; + } + PyObject* suggestion = NULL; + Py_ssize_t dir_size = PyList_GET_SIZE(dir); + if (dir_size <= MAX_GETATTR_PREDICT_ITEMS) { + int suggestion_distance = PyUnicode_GetLength(name); + 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) { + newexceptionvalue = PyUnicode_FromFormat("%S\n\nDid you mean: %U?", + oldexceptionvalue, + suggestion); + Py_DECREF(oldexceptionvalue); + } + Py_DECREF(dir); + } + PyErr_Clear(); + PyErr_Restore(oldexceptiontype, newexceptionvalue, oldtraceback); + return 0; +} + + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy