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 @@
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: