bpo-38530: Offer suggestions on AttributeError by pablogsal · Pull Request #16850 · python/cpython · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Include/internal/pycore_suggestions.h
26 changes: 25 additions & 1 deletion Lib/test/test_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
2 changes: 2 additions & 0 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ PYTHON_OBJS= \
Python/dtoa.o \
Python/formatter_unicode.o \
Python/fileutils.o \
Python/suggestions.o \
Python/$(DYNLOADFILE) \
$(LIBOBJS) \
$(MACHDEP_OBJS) \
Expand Down Expand Up @@ -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 \
Expand Down
29 changes: 22 additions & 7 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions PCbuild/pythoncore.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
<ClInclude Include="..\Include\internal\pycore_traceback.h" />
<ClInclude Include="..\Include\internal\pycore_tupleobject.h" />
<ClInclude Include="..\Include\internal\pycore_warnings.h" />
<ClInclude Include="..\Include\internal\pycore_suggestions.h" />
<ClInclude Include="..\Include\interpreteridobject.h" />
<ClInclude Include="..\Include\intrcheck.h" />
<ClInclude Include="..\Include\iterobject.h" />
Expand Down Expand Up @@ -462,6 +463,7 @@
<ClCompile Include="..\Python\dtoa.c" />
<ClCompile Include="..\Python\Python-ast.c" />
<ClCompile Include="..\Python\pythonrun.c" />
<ClCompile Include="..\Python\suggestions.c" />
<ClCompile Include="..\Python\structmember.c" />
<ClCompile Include="..\Python\symtable.c" />
<ClCompile Include="..\Python\sysmodule.c" />
Expand Down
176 changes: 176 additions & 0 deletions Python/suggestions.c