]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-153785: Generate `AttributeError` messages from context (#153786)
authorBartosz Sławecki <bartosz@ilikepython.com>
Fri, 24 Jul 2026 12:14:47 +0000 (14:14 +0200)
committerGitHub <noreply@github.com>
Fri, 24 Jul 2026 12:14:47 +0000 (14:14 +0200)
Co-authored-by: Victor Stinner <vstinner@python.org>
Doc/library/exceptions.rst
Lib/test/test_exceptions.py
Misc/NEWS.d/next/Core_and_Builtins/2026-07-16-02-50-01.gh-issue-153785.fJqPKC.rst [new file with mode: 0644]
Objects/exceptions.c

index 3775d5ac81a27369b23172179637e85d6580e115..ecf62fb6391b1b10d32e794907fdfa064ed168f0 100644 (file)
@@ -215,6 +215,8 @@ The following exceptions are the exceptions that are usually raised.
 
       The object that was accessed for the named attribute.
 
+   When possible, :attr:`name` and :attr:`obj` are set automatically.
+
    .. versionchanged:: 3.10
       Added the :attr:`name` and :attr:`obj` attributes.
 
index ee4888c18598f3dd173327a6fcae3bb0a2a6efca..0cee756958f3ad19807f7742c6bf0432ec8781ca 100644 (file)
@@ -10,6 +10,7 @@ import errno
 from codecs import BOM_UTF8
 from itertools import product
 from textwrap import dedent
+from types import ModuleType
 
 from test.support import (captured_stderr, check_impl_detail,
                           cpython_only, gc_collect,
@@ -2054,6 +2055,129 @@ class AttributeErrorTests(unittest.TestCase):
             self.assertEqual("bluch", exc.name)
             self.assertEqual(obj, exc.obj)
 
+    def test_getattr_error_message(self):
+        def fqn(type):
+            return f'{type.__module__}.{type.__qualname__}'
+
+        class RaiseWithName:
+            def __getattr__(self, name):
+                raise AttributeError(name)
+        obj = RaiseWithName()
+        with self.assertRaises(AttributeError) as cm:
+            getattr(obj, "missing1")
+        self.assertEqual(str(cm.exception),
+                         f"'{fqn(RaiseWithName)}' object has no attribute 'missing1'")
+        self.assertIs(cm.exception.obj, obj)
+        self.assertEqual(cm.exception.name, "missing1")
+
+        class BareRaise:
+            def __getattr__(self, name):
+                raise AttributeError
+        obj = BareRaise()
+        with self.assertRaises(AttributeError) as cm:
+            getattr(obj, "missing2")
+        self.assertEqual(str(cm.exception),
+                         f"'{fqn(BareRaise)}' object has no attribute 'missing2'")
+        self.assertIs(cm.exception.obj, obj)
+        self.assertEqual(cm.exception.name, "missing2")
+
+        class RaiseCustom:
+            def __getattr__(self, name):
+                raise AttributeError("custom")
+        obj = RaiseCustom()
+        with self.assertRaises(AttributeError) as cm:
+            getattr(obj, "missing3")
+        self.assertEqual(str(cm.exception), "custom")
+        self.assertIs(cm.exception.obj, obj)
+        self.assertEqual(cm.exception.name, "missing3")
+
+    def test_class_getattr_error_message(self):
+        def fqn(type):
+            return f'{type.__module__}.{type.__qualname__}'
+
+        class MetaclassRaiseWithName(type):
+            def __getattr__(self, name):
+                raise AttributeError(name)
+        cls = MetaclassRaiseWithName("spam", (), {})
+        with self.assertRaises(AttributeError) as cm:
+            getattr(cls, "missing1")
+        self.assertEqual(str(cm.exception),
+                         f"type object '{fqn(cls)}' has no attribute 'missing1'")
+        self.assertIs(cm.exception.obj, cls)
+        self.assertEqual(cm.exception.name, "missing1")
+
+        class MetaclassBareRaise(type):
+            def __getattr__(self, name):
+                raise AttributeError
+        cls = MetaclassBareRaise("eggs", (), {})
+        with self.assertRaises(AttributeError) as cm:
+            getattr(cls, "missing2")
+        self.assertEqual(str(cm.exception),
+                         f"type object '{fqn(cls)}' has no attribute 'missing2'")
+        self.assertIs(cm.exception.obj, cls)
+        self.assertEqual(cm.exception.name, "missing2")
+
+        class MetaclassRaiseCustom(type):
+            def __getattr__(self, name):
+                raise AttributeError("custom")
+        cls = MetaclassRaiseCustom("ham", (), {})
+        with self.assertRaises(AttributeError) as cm:
+            getattr(cls, "missing3")
+        self.assertEqual(str(cm.exception), "custom")
+        self.assertIs(cm.exception.obj, cls)
+        self.assertEqual(cm.exception.name, "missing3")
+
+    def test_module_getattr_error_message(self):
+        raisewithname_mod = ModuleType("raisewithname")
+        def raise_with_name(name):
+            raise AttributeError(name)
+        raisewithname_mod.__getattr__ = raise_with_name
+        with self.assertRaises(AttributeError) as cm:
+            getattr(raisewithname_mod, "missing1")
+        self.assertEqual(str(cm.exception),
+                         "module 'raisewithname' has no attribute 'missing1'")
+        self.assertIs(cm.exception.obj, raisewithname_mod)
+        self.assertEqual(cm.exception.name, "missing1")
+
+        bareraise_mod = ModuleType("bareraise")
+        def bare_raise(name):
+            raise AttributeError
+        bareraise_mod.__getattr__ = bare_raise
+        with self.assertRaises(AttributeError) as cm:
+            getattr(bareraise_mod, "missing2")
+        self.assertEqual(str(cm.exception),
+                         "module 'bareraise' has no attribute 'missing2'")
+        self.assertIs(cm.exception.obj, bareraise_mod)
+        self.assertEqual(cm.exception.name, "missing2")
+
+        custom_mod = ModuleType("custom")
+        def raise_custom(name):
+            raise AttributeError("custom")
+        custom_mod.__getattr__ = raise_custom
+        with self.assertRaises(AttributeError) as cm:
+            getattr(custom_mod, "missing3")
+        self.assertEqual(str(cm.exception), "custom")
+        self.assertIs(cm.exception.obj, custom_mod)
+        self.assertEqual(cm.exception.name, "missing3")
+
+        nameless_mod = ModuleType("forgettable")
+        del nameless_mod.__dict__["__name__"]
+        nameless_mod.__getattr__ = raise_with_name
+        with self.assertRaises(AttributeError) as cm:
+            getattr(nameless_mod, "missing4")
+        self.assertEqual(str(cm.exception), "module has no attribute 'missing4'")
+        self.assertIs(cm.exception.obj, nameless_mod)
+        self.assertEqual(cm.exception.name, "missing4")
+
+        nameless_mod = ModuleType("broken")
+        nameless_mod.__dict__["__name__"] = 10j
+        nameless_mod.__getattr__ = raise_with_name
+        with self.assertRaises(AttributeError) as cm:
+            getattr(nameless_mod, "missing4")
+        self.assertEqual(str(cm.exception), "module has no attribute 'missing4'")
+        self.assertIs(cm.exception.obj, nameless_mod)
+        self.assertEqual(cm.exception.name, "missing4")
+
     # Note: name suggestion tests live in `test_traceback`.
 
 
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-16-02-50-01.gh-issue-153785.fJqPKC.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-16-02-50-01.gh-issue-153785.fJqPKC.rst
new file mode 100644 (file)
index 0000000..40981ac
--- /dev/null
@@ -0,0 +1,4 @@
+:exc:`AttributeError`: The default error message is now generated from ``name``
+and ``obj`` attributes when both are set and the exception was constructed with
+no positional arguments, or with a single positional argument equal to ``name``.
+Patch by Bartosz Sławecki.
index 149595e64cec1445e495918b550801666f992381..fb546ad2673576d5d76b79da4739b8ac7293e608 100644 (file)
 #include "pycore_exceptions.h"    // struct _Py_exc_state
 #include "pycore_initconfig.h"
 #include "pycore_modsupport.h"    // _PyArg_NoKeywords()
+#include "pycore_moduleobject.h"  // _PyModule_CAST()
 #include "pycore_object.h"
 #include "pycore_pyerrors.h"      // struct _PyErr_SetRaisedException
 #include "pycore_tuple.h"         // _PyTuple_FromPair
+#include "pycore_unicodeobject.h" // _PyUnicode_Equal()
 
 #include "osdefs.h"               // SEP
 #include "clinic/exceptions.c.h"
@@ -2702,6 +2704,65 @@ AttributeError_dealloc(PyObject *self)
     Py_TYPE(self)->tp_free(self);
 }
 
+static PyObject *
+AttributeError_str(PyObject *op)
+{
+    PyAttributeErrorObject *self = PyAttributeErrorObject_CAST(op);
+    PyObject *arg;  // borrowed ref
+    PyObject *obj = NULL, *name = NULL;
+
+     /* .name and .obj are set automatically when attribute lookup fails, so
+        synthesize a more informative message from them when the caller
+        didn't supply a meaningful one of their own -- that is, when args is
+        empty, or contains only the attribute name.  Otherwise, use the
+        message the caller gave. */
+
+    Py_BEGIN_CRITICAL_SECTION(self);
+    if (
+        self->obj && self->name && PyUnicode_Check(self->name)
+        && ((PyTuple_GET_SIZE(self->args) == 1
+             && PyUnicode_Check(arg = PyTuple_GET_ITEM(self->args, 0))
+             && _PyUnicode_Equal(arg, self->name))
+            || PyTuple_GET_SIZE(self->args) == 0)
+    ) {
+        obj = Py_NewRef(self->obj);
+        name = Py_NewRef(self->name);
+    }
+    Py_END_CRITICAL_SECTION();
+
+    if (!obj) {
+        assert(!name);
+        return BaseException_str(op);  /* re-acquires lock */
+    }
+
+    PyObject *result = NULL;
+    if (PyModule_Check(obj)) {
+        PyModuleObject *mod = _PyModule_CAST(obj);
+        PyObject *modname;
+        if (PyDict_GetItemRef(mod->md_dict, &_Py_ID(__name__), &modname) < 0) {
+            goto done;
+        }
+        if (modname && PyUnicode_Check(modname)) {
+            result = PyUnicode_FromFormat("module %R has no attribute %R",
+                                          modname, name);
+            Py_DECREF(modname);
+        } else {
+            Py_XDECREF(modname);
+            result = PyUnicode_FromFormat("module has no attribute %R", name);
+        }
+    } else if (PyType_Check(obj)) {
+        result = PyUnicode_FromFormat("type object '%N' has no attribute %R",
+                                      obj, name);
+    } else {
+        result = PyUnicode_FromFormat("'%T' object has no attribute %R",
+                                      obj, name);
+    }
+done:
+    Py_DECREF(obj);
+    Py_DECREF(name);
+    return result;
+}
+
 static int
 AttributeError_traverse(PyObject *op, visitproc visit, void *arg)
 {
@@ -2770,7 +2831,7 @@ static PyMethodDef AttributeError_methods[] = {
 ComplexExtendsException(PyExc_Exception, AttributeError,
                         AttributeError, 0,
                         AttributeError_methods, AttributeError_members,
-                        0, BaseException_str, 0, "Attribute not found.");
+                        0, AttributeError_str, 0, "Attribute not found.");
 
 /*
  *    SyntaxError extends Exception