Skip to content

Commit ae72d0c

Browse files
johnslavikvstinner
andauthored
gh-153785: Generate AttributeError messages from context (#153786)
Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent a096314 commit ae72d0c

4 files changed

Lines changed: 192 additions & 1 deletion

File tree

Doc/library/exceptions.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ The following exceptions are the exceptions that are usually raised.
215215

216216
The object that was accessed for the named attribute.
217217

218+
When possible, :attr:`name` and :attr:`obj` are set automatically.
219+
218220
.. versionchanged:: 3.10
219221
Added the :attr:`name` and :attr:`obj` attributes.
220222

Lib/test/test_exceptions.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from codecs import BOM_UTF8
1111
from itertools import product
1212
from textwrap import dedent
13+
from types import ModuleType
1314

1415
from test.support import (captured_stderr, check_impl_detail,
1516
cpython_only, gc_collect,
@@ -2054,6 +2055,129 @@ def blech(self):
20542055
self.assertEqual("bluch", exc.name)
20552056
self.assertEqual(obj, exc.obj)
20562057

2058+
def test_getattr_error_message(self):
2059+
def fqn(type):
2060+
return f'{type.__module__}.{type.__qualname__}'
2061+
2062+
class RaiseWithName:
2063+
def __getattr__(self, name):
2064+
raise AttributeError(name)
2065+
obj = RaiseWithName()
2066+
with self.assertRaises(AttributeError) as cm:
2067+
getattr(obj, "missing1")
2068+
self.assertEqual(str(cm.exception),
2069+
f"'{fqn(RaiseWithName)}' object has no attribute 'missing1'")
2070+
self.assertIs(cm.exception.obj, obj)
2071+
self.assertEqual(cm.exception.name, "missing1")
2072+
2073+
class BareRaise:
2074+
def __getattr__(self, name):
2075+
raise AttributeError
2076+
obj = BareRaise()
2077+
with self.assertRaises(AttributeError) as cm:
2078+
getattr(obj, "missing2")
2079+
self.assertEqual(str(cm.exception),
2080+
f"'{fqn(BareRaise)}' object has no attribute 'missing2'")
2081+
self.assertIs(cm.exception.obj, obj)
2082+
self.assertEqual(cm.exception.name, "missing2")
2083+
2084+
class RaiseCustom:
2085+
def __getattr__(self, name):
2086+
raise AttributeError("custom")
2087+
obj = RaiseCustom()
2088+
with self.assertRaises(AttributeError) as cm:
2089+
getattr(obj, "missing3")
2090+
self.assertEqual(str(cm.exception), "custom")
2091+
self.assertIs(cm.exception.obj, obj)
2092+
self.assertEqual(cm.exception.name, "missing3")
2093+
2094+
def test_class_getattr_error_message(self):
2095+
def fqn(type):
2096+
return f'{type.__module__}.{type.__qualname__}'
2097+
2098+
class MetaclassRaiseWithName(type):
2099+
def __getattr__(self, name):
2100+
raise AttributeError(name)
2101+
cls = MetaclassRaiseWithName("spam", (), {})
2102+
with self.assertRaises(AttributeError) as cm:
2103+
getattr(cls, "missing1")
2104+
self.assertEqual(str(cm.exception),
2105+
f"type object '{fqn(cls)}' has no attribute 'missing1'")
2106+
self.assertIs(cm.exception.obj, cls)
2107+
self.assertEqual(cm.exception.name, "missing1")
2108+
2109+
class MetaclassBareRaise(type):
2110+
def __getattr__(self, name):
2111+
raise AttributeError
2112+
cls = MetaclassBareRaise("eggs", (), {})
2113+
with self.assertRaises(AttributeError) as cm:
2114+
getattr(cls, "missing2")
2115+
self.assertEqual(str(cm.exception),
2116+
f"type object '{fqn(cls)}' has no attribute 'missing2'")
2117+
self.assertIs(cm.exception.obj, cls)
2118+
self.assertEqual(cm.exception.name, "missing2")
2119+
2120+
class MetaclassRaiseCustom(type):
2121+
def __getattr__(self, name):
2122+
raise AttributeError("custom")
2123+
cls = MetaclassRaiseCustom("ham", (), {})
2124+
with self.assertRaises(AttributeError) as cm:
2125+
getattr(cls, "missing3")
2126+
self.assertEqual(str(cm.exception), "custom")
2127+
self.assertIs(cm.exception.obj, cls)
2128+
self.assertEqual(cm.exception.name, "missing3")
2129+
2130+
def test_module_getattr_error_message(self):
2131+
raisewithname_mod = ModuleType("raisewithname")
2132+
def raise_with_name(name):
2133+
raise AttributeError(name)
2134+
raisewithname_mod.__getattr__ = raise_with_name
2135+
with self.assertRaises(AttributeError) as cm:
2136+
getattr(raisewithname_mod, "missing1")
2137+
self.assertEqual(str(cm.exception),
2138+
"module 'raisewithname' has no attribute 'missing1'")
2139+
self.assertIs(cm.exception.obj, raisewithname_mod)
2140+
self.assertEqual(cm.exception.name, "missing1")
2141+
2142+
bareraise_mod = ModuleType("bareraise")
2143+
def bare_raise(name):
2144+
raise AttributeError
2145+
bareraise_mod.__getattr__ = bare_raise
2146+
with self.assertRaises(AttributeError) as cm:
2147+
getattr(bareraise_mod, "missing2")
2148+
self.assertEqual(str(cm.exception),
2149+
"module 'bareraise' has no attribute 'missing2'")
2150+
self.assertIs(cm.exception.obj, bareraise_mod)
2151+
self.assertEqual(cm.exception.name, "missing2")
2152+
2153+
custom_mod = ModuleType("custom")
2154+
def raise_custom(name):
2155+
raise AttributeError("custom")
2156+
custom_mod.__getattr__ = raise_custom
2157+
with self.assertRaises(AttributeError) as cm:
2158+
getattr(custom_mod, "missing3")
2159+
self.assertEqual(str(cm.exception), "custom")
2160+
self.assertIs(cm.exception.obj, custom_mod)
2161+
self.assertEqual(cm.exception.name, "missing3")
2162+
2163+
nameless_mod = ModuleType("forgettable")
2164+
del nameless_mod.__dict__["__name__"]
2165+
nameless_mod.__getattr__ = raise_with_name
2166+
with self.assertRaises(AttributeError) as cm:
2167+
getattr(nameless_mod, "missing4")
2168+
self.assertEqual(str(cm.exception), "module has no attribute 'missing4'")
2169+
self.assertIs(cm.exception.obj, nameless_mod)
2170+
self.assertEqual(cm.exception.name, "missing4")
2171+
2172+
nameless_mod = ModuleType("broken")
2173+
nameless_mod.__dict__["__name__"] = 10j
2174+
nameless_mod.__getattr__ = raise_with_name
2175+
with self.assertRaises(AttributeError) as cm:
2176+
getattr(nameless_mod, "missing4")
2177+
self.assertEqual(str(cm.exception), "module has no attribute 'missing4'")
2178+
self.assertIs(cm.exception.obj, nameless_mod)
2179+
self.assertEqual(cm.exception.name, "missing4")
2180+
20572181
# Note: name suggestion tests live in `test_traceback`.
20582182

20592183

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:exc:`AttributeError`: The default error message is now generated from ``name``
2+
and ``obj`` attributes when both are set and the exception was constructed with
3+
no positional arguments, or with a single positional argument equal to ``name``.
4+
Patch by Bartosz Sławecki.

Objects/exceptions.c

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
#include "pycore_exceptions.h" // struct _Py_exc_state
1212
#include "pycore_initconfig.h"
1313
#include "pycore_modsupport.h" // _PyArg_NoKeywords()
14+
#include "pycore_moduleobject.h" // _PyModule_CAST()
1415
#include "pycore_object.h"
1516
#include "pycore_pyerrors.h" // struct _PyErr_SetRaisedException
1617
#include "pycore_tuple.h" // _PyTuple_FromPair
18+
#include "pycore_unicodeobject.h" // _PyUnicode_Equal()
1719

1820
#include "osdefs.h" // SEP
1921
#include "clinic/exceptions.c.h"
@@ -2702,6 +2704,65 @@ AttributeError_dealloc(PyObject *self)
27022704
Py_TYPE(self)->tp_free(self);
27032705
}
27042706

2707+
static PyObject *
2708+
AttributeError_str(PyObject *op)
2709+
{
2710+
PyAttributeErrorObject *self = PyAttributeErrorObject_CAST(op);
2711+
PyObject *arg; // borrowed ref
2712+
PyObject *obj = NULL, *name = NULL;
2713+
2714+
/* .name and .obj are set automatically when attribute lookup fails, so
2715+
synthesize a more informative message from them when the caller
2716+
didn't supply a meaningful one of their own -- that is, when args is
2717+
empty, or contains only the attribute name. Otherwise, use the
2718+
message the caller gave. */
2719+
2720+
Py_BEGIN_CRITICAL_SECTION(self);
2721+
if (
2722+
self->obj && self->name && PyUnicode_Check(self->name)
2723+
&& ((PyTuple_GET_SIZE(self->args) == 1
2724+
&& PyUnicode_Check(arg = PyTuple_GET_ITEM(self->args, 0))
2725+
&& _PyUnicode_Equal(arg, self->name))
2726+
|| PyTuple_GET_SIZE(self->args) == 0)
2727+
) {
2728+
obj = Py_NewRef(self->obj);
2729+
name = Py_NewRef(self->name);
2730+
}
2731+
Py_END_CRITICAL_SECTION();
2732+
2733+
if (!obj) {
2734+
assert(!name);
2735+
return BaseException_str(op); /* re-acquires lock */
2736+
}
2737+
2738+
PyObject *result = NULL;
2739+
if (PyModule_Check(obj)) {
2740+
PyModuleObject *mod = _PyModule_CAST(obj);
2741+
PyObject *modname;
2742+
if (PyDict_GetItemRef(mod->md_dict, &_Py_ID(__name__), &modname) < 0) {
2743+
goto done;
2744+
}
2745+
if (modname && PyUnicode_Check(modname)) {
2746+
result = PyUnicode_FromFormat("module %R has no attribute %R",
2747+
modname, name);
2748+
Py_DECREF(modname);
2749+
} else {
2750+
Py_XDECREF(modname);
2751+
result = PyUnicode_FromFormat("module has no attribute %R", name);
2752+
}
2753+
} else if (PyType_Check(obj)) {
2754+
result = PyUnicode_FromFormat("type object '%N' has no attribute %R",
2755+
obj, name);
2756+
} else {
2757+
result = PyUnicode_FromFormat("'%T' object has no attribute %R",
2758+
obj, name);
2759+
}
2760+
done:
2761+
Py_DECREF(obj);
2762+
Py_DECREF(name);
2763+
return result;
2764+
}
2765+
27052766
static int
27062767
AttributeError_traverse(PyObject *op, visitproc visit, void *arg)
27072768
{
@@ -2770,7 +2831,7 @@ static PyMethodDef AttributeError_methods[] = {
27702831
ComplexExtendsException(PyExc_Exception, AttributeError,
27712832
AttributeError, 0,
27722833
AttributeError_methods, AttributeError_members,
2773-
0, BaseException_str, 0, "Attribute not found.");
2834+
0, AttributeError_str, 0, "Attribute not found.");
27742835

27752836
/*
27762837
* SyntaxError extends Exception

0 commit comments

Comments
 (0)