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;
+}
+
+