Skip to content

Commit c9c85b5

Browse files
committed
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
1 parent 11c3bd3 commit c9c85b5

File tree

11 files changed

+537
-10
lines changed

11 files changed

+537
-10
lines changed

Include/cpython/pyerrors.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ typedef struct {
6262
PyObject *value;
6363
} PyStopIterationObject;
6464

65+
typedef struct {
66+
PyException_HEAD
67+
PyObject *obj;
68+
PyObject *name;
69+
} PyAttributeErrorObject;
70+
6571
/* Compatibility typedefs */
6672
typedef PyOSErrorObject PyEnvironmentErrorObject;
6773
#ifdef MS_WINDOWS

Include/internal/pycore_suggestions.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#include "Python.h"
2+
3+
#ifndef Py_INTERNAL_SUGGESTIONS_H
4+
#define Py_INTERNAL_SUGGESTIONS_H
5+
6+
#ifndef Py_BUILD_CORE
7+
# error "this header requires Py_BUILD_CORE define"
8+
#endif
9+
10+
int _Py_offer_suggestions_for_attribute_error(PyAttributeErrorObject* exception_value);
11+
12+
13+
#endif /* !Py_INTERNAL_SUGGESTIONS_H */

Include/pyerrors.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,6 @@ PyAPI_FUNC(PyObject *) PyErr_NewExceptionWithDoc(
221221
const char *name, const char *doc, PyObject *base, PyObject *dict);
222222
PyAPI_FUNC(void) PyErr_WriteUnraisable(PyObject *);
223223

224-
225224
/* In signalmodule.c */
226225
PyAPI_FUNC(int) PyErr_CheckSignals(void);
227226
PyAPI_FUNC(void) PyErr_SetInterrupt(void);

Lib/test/test_exceptions.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,6 +1414,165 @@ class TestException(MemoryError):
14141414
gc_collect()
14151415

14161416

1417+
class AttributeErrorTests(unittest.TestCase):
1418+
def test_attributes(self):
1419+
# Setting 'attr' should not be a problem.
1420+
exc = AttributeError('Ouch!')
1421+
self.assertIsNone(exc.name)
1422+
self.assertIsNone(exc.obj)
1423+
1424+
sentinel = object()
1425+
exc = AttributeError('Ouch', name='carry', obj=sentinel)
1426+
self.assertEqual(exc.name, 'carry')
1427+
self.assertIs(exc.obj, sentinel)
1428+
1429+
def test_getattr_has_name_and_obj(self):
1430+
class A:
1431+
blech = None
1432+
1433+
obj = A()
1434+
try:
1435+
obj.bluch
1436+
except AttributeError as exc:
1437+
self.assertEqual("bluch", exc.name)
1438+
self.assertEqual(obj, exc.obj)
1439+
1440+
def test_getattr_has_name_and_obj_for_method(self):
1441+
class A:
1442+
def blech(self):
1443+
return
1444+
1445+
obj = A()
1446+
try:
1447+
obj.bluch()
1448+
except AttributeError as exc:
1449+
self.assertEqual("bluch", exc.name)
1450+
self.assertEqual(obj, exc.obj)
1451+
1452+
def test_getattr_suggestions(self):
1453+
class Substitution:
1454+
noise = more_noise = a = bc = None
1455+
blech = None
1456+
1457+
class Elimination:
1458+
noise = more_noise = a = bc = None
1459+
blch = None
1460+
1461+
class Addition:
1462+
noise = more_noise = a = bc = None
1463+
bluchin = None
1464+
1465+
class SubstitutionOverElimination:
1466+
blach = None
1467+
bluc = None
1468+
1469+
class SubstitutionOverAddition:
1470+
blach = None
1471+
bluchi = None
1472+
1473+
class EliminationOverAddition:
1474+
blucha = None
1475+
bluc = None
1476+
1477+
for cls, suggestion in [(Substitution, "blech?"),
1478+
(Elimination, "blch?"),
1479+
(Addition, "bluchin?"),
1480+
(EliminationOverAddition, "bluc?"),
1481+
(SubstitutionOverElimination, "blach?"),
1482+
(SubstitutionOverAddition, "blach?")]:
1483+
try:
1484+
cls().bluch
1485+
except AttributeError as exc:
1486+
with support.captured_stderr() as err:
1487+
sys.__excepthook__(*sys.exc_info())
1488+
1489+
self.assertIn(suggestion, err.getvalue())
1490+
1491+
def test_getattr_suggestions_do_not_trigger_for_long_attributes(self):
1492+
class A:
1493+
blech = None
1494+
1495+
try:
1496+
A().somethingverywrong
1497+
except AttributeError as exc:
1498+
with support.captured_stderr() as err:
1499+
sys.__excepthook__(*sys.exc_info())
1500+
1501+
self.assertNotIn("blech", err.getvalue())
1502+
1503+
def test_getattr_suggestions_do_not_trigger_for_big_dicts(self):
1504+
class A:
1505+
blech = None
1506+
# A class with a very big __dict__ will not be consider
1507+
# for suggestions.
1508+
for index in range(101):
1509+
setattr(A, f"index_{index}", None)
1510+
1511+
try:
1512+
A().bluch
1513+
except AttributeError as exc:
1514+
with support.captured_stderr() as err:
1515+
sys.__excepthook__(*sys.exc_info())
1516+
1517+
self.assertNotIn("blech", err.getvalue())
1518+
1519+
def test_getattr_suggestions_no_args(self):
1520+
class A:
1521+
blech = None
1522+
def __getattr__(self, attr):
1523+
raise AttributeError()
1524+
1525+
try:
1526+
A().bluch
1527+
except AttributeError as exc:
1528+
with support.captured_stderr() as err:
1529+
sys.__excepthook__(*sys.exc_info())
1530+
1531+
self.assertIn("blech", err.getvalue())
1532+
1533+
class A:
1534+
blech = None
1535+
def __getattr__(self, attr):
1536+
raise AttributeError
1537+
1538+
try:
1539+
A().bluch
1540+
except AttributeError as exc:
1541+
with support.captured_stderr() as err:
1542+
sys.__excepthook__(*sys.exc_info())
1543+
1544+
self.assertIn("blech", err.getvalue())
1545+
1546+
def test_getattr_suggestions_invalid_args(self):
1547+
class NonStringifyClass:
1548+
__str__ = None
1549+
__repr__ = None
1550+
1551+
class A:
1552+
blech = None
1553+
def __getattr__(self, attr):
1554+
raise AttributeError(NonStringifyClass())
1555+
1556+
class B:
1557+
blech = None
1558+
def __getattr__(self, attr):
1559+
raise AttributeError("Error", 23)
1560+
1561+
class C:
1562+
blech = None
1563+
def __getattr__(self, attr):
1564+
raise AttributeError(23)
1565+
1566+
for cls in [A, B, C]:
1567+
try:
1568+
cls().bluch
1569+
except AttributeError as exc:
1570+
with support.captured_stderr() as err:
1571+
sys.__excepthook__(*sys.exc_info())
1572+
1573+
self.assertNotIn("blech", err.getvalue())
1574+
1575+
14171576
class ImportErrorTests(unittest.TestCase):
14181577

14191578
def test_attributes(self):

Makefile.pre.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ PYTHON_OBJS= \
387387
Python/dtoa.o \
388388
Python/formatter_unicode.o \
389389
Python/fileutils.o \
390+
Python/suggestions.o \
390391
Python/$(DYNLOADFILE) \
391392
$(LIBOBJS) \
392393
$(MACHDEP_OBJS) \
@@ -1161,6 +1162,7 @@ PYTHON_HEADERS= \
11611162
$(srcdir)/Include/internal/pycore_list.h \
11621163
$(srcdir)/Include/internal/pycore_long.h \
11631164
$(srcdir)/Include/internal/pycore_object.h \
1165+
$(srcdir)/Include/internal/pycore_suggestions.h \
11641166
$(srcdir)/Include/internal/pycore_pathconfig.h \
11651167
$(srcdir)/Include/internal/pycore_pyarena.h \
11661168
$(srcdir)/Include/internal/pycore_pyerrors.h \
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
When printing :exc:`AttributeError`, :c:func:`PyErr_Display` will offer
2+
suggestions of simmilar attribute names in the object that the exception was
3+
raised from.

Objects/exceptions.c

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,9 +1338,82 @@ SimpleExtendsException(PyExc_NameError, UnboundLocalError,
13381338
/*
13391339
* AttributeError extends Exception
13401340
*/
1341-
SimpleExtendsException(PyExc_Exception, AttributeError,
1342-
"Attribute not found.");
13431341

1342+
static int
1343+
AttributeError_init(PyAttributeErrorObject *self, PyObject *args, PyObject *kwds)
1344+
{
1345+
static char *kwlist[] = {"name", "obj", NULL};
1346+
PyObject *name = NULL;
1347+
PyObject *obj = NULL;
1348+
1349+
if (BaseException_init((PyBaseExceptionObject *)self, args, NULL) == -1) {
1350+
return -1;
1351+
}
1352+
1353+
PyObject *empty_tuple = PyTuple_New(0);
1354+
if (!empty_tuple) {
1355+
return -1;
1356+
}
1357+
if (!PyArg_ParseTupleAndKeywords(empty_tuple, kwds, "|$OO:AttributeError", kwlist,
1358+
&name, &obj)) {
1359+
Py_DECREF(empty_tuple);
1360+
return -1;
1361+
}
1362+
Py_DECREF(empty_tuple);
1363+
1364+
Py_XINCREF(name);
1365+
Py_XSETREF(self->name, name);
1366+
1367+
Py_XINCREF(obj);
1368+
Py_XSETREF(self->obj, obj);
1369+
1370+
return 0;
1371+
}
1372+
1373+
static int
1374+
AttributeError_clear(PyAttributeErrorObject *self)
1375+
{
1376+
Py_CLEAR(self->obj);
1377+
Py_CLEAR(self->name);
1378+
return BaseException_clear((PyBaseExceptionObject *)self);
1379+
}
1380+
1381+
static void
1382+
AttributeError_dealloc(PyAttributeErrorObject *self)
1383+
{
1384+
_PyObject_GC_UNTRACK(self);
1385+
AttributeError_clear(self);
1386+
Py_TYPE(self)->tp_free((PyObject *)self);
1387+
}
1388+
1389+
static int
1390+
AttributeError_traverse(PyAttributeErrorObject *self, visitproc visit, void *arg)
1391+
{
1392+
Py_VISIT(self->obj);
1393+
Py_VISIT(self->name);
1394+
return BaseException_traverse((PyBaseExceptionObject *)self, visit, arg);
1395+
}
1396+
1397+
static PyObject *
1398+
AttributeError_str(PyAttributeErrorObject *self)
1399+
{
1400+
return BaseException_str((PyBaseExceptionObject *)self);
1401+
}
1402+
1403+
static PyMemberDef AttributeError_members[] = {
1404+
{"name", T_OBJECT, offsetof(PyAttributeErrorObject, name), 0, PyDoc_STR("attribute name")},
1405+
{"obj", T_OBJECT, offsetof(PyAttributeErrorObject, obj), 0, PyDoc_STR("object")},
1406+
{NULL} /* Sentinel */
1407+
};
1408+
1409+
static PyMethodDef AttributeError_methods[] = {
1410+
{NULL} /* Sentinel */
1411+
};
1412+
1413+
ComplexExtendsException(PyExc_Exception, AttributeError,
1414+
AttributeError, 0,
1415+
AttributeError_methods, AttributeError_members,
1416+
0, AttributeError_str, "Attribute not found.");
13441417

13451418
/*
13461419
* SyntaxError extends Exception

Objects/object.c

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "pycore_pystate.h" // _PyThreadState_GET()
1313
#include "pycore_symtable.h" // PySTEntry_Type
1414
#include "pycore_unionobject.h" // _Py_UnionType
15+
#include "pycore_suggestions.h"
1516
#include "frameobject.h"
1617
#include "interpreteridobject.h"
1718

@@ -884,10 +885,31 @@ _PyObject_SetAttrId(PyObject *v, _Py_Identifier *name, PyObject *w)
884885
return result;
885886
}
886887

888+
static inline int
889+
add_context_to_attribute_error_exception(PyObject* v, PyObject* name)
890+
{
891+
assert(PyErr_Occurred());
892+
// Intercept AttributeError exceptions and augment them to offer
893+
// suggestions later.
894+
if (PyErr_ExceptionMatches(PyExc_AttributeError)){
895+
PyObject *type, *value, *traceback;
896+
PyErr_Fetch(&type, &value, &traceback);
897+
PyErr_NormalizeException(&type, &value, &traceback);
898+
if (PyErr_GivenExceptionMatches(value, PyExc_AttributeError) &&
899+
(PyObject_SetAttrString(value, "name", name) ||
900+
PyObject_SetAttrString(value, "obj", v))) {
901+
return 1;
902+
}
903+
PyErr_Restore(type, value, traceback);
904+
}
905+
return 0;
906+
}
907+
887908
PyObject *
888909
PyObject_GetAttr(PyObject *v, PyObject *name)
889910
{
890911
PyTypeObject *tp = Py_TYPE(v);
912+
PyObject* result = NULL;
891913

892914
if (!PyUnicode_Check(name)) {
893915
PyErr_Format(PyExc_TypeError,
@@ -896,17 +918,23 @@ PyObject_GetAttr(PyObject *v, PyObject *name)
896918
return NULL;
897919
}
898920
if (tp->tp_getattro != NULL)
899-
return (*tp->tp_getattro)(v, name);
900-
if (tp->tp_getattr != NULL) {
921+
result = (*tp->tp_getattro)(v, name);
922+
else if (tp->tp_getattr != NULL) {
901923
const char *name_str = PyUnicode_AsUTF8(name);
902924
if (name_str == NULL)
903925
return NULL;
904-
return (*tp->tp_getattr)(v, (char *)name_str);
926+
result = (*tp->tp_getattr)(v, (char *)name_str);
927+
} else {
928+
PyErr_Format(PyExc_AttributeError,
929+
"'%.50s' object has no attribute '%U'",
930+
tp->tp_name, name);
905931
}
906-
PyErr_Format(PyExc_AttributeError,
907-
"'%.50s' object has no attribute '%U'",
908-
tp->tp_name, name);
909-
return NULL;
932+
933+
if (!result && add_context_to_attribute_error_exception(v, name)) {
934+
return NULL;
935+
}
936+
937+
return result;
910938
}
911939

912940
int
@@ -1165,6 +1193,11 @@ _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method)
11651193
PyErr_Format(PyExc_AttributeError,
11661194
"'%.50s' object has no attribute '%U'",
11671195
tp->tp_name, name);
1196+
1197+
if (add_context_to_attribute_error_exception(obj, name)) {
1198+
return 0;
1199+
}
1200+
11681201
return 0;
11691202
}
11701203

0 commit comments

Comments
 (0)