diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index 8c0afd14d616e85..454e030a313eab4 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -3953,7 +3953,7 @@ def __setattr__(self, name, value): return object.__setattr__(self, name, value) else: raise AttributeError( - "'decimal.Context' object has no attribute '%s'" % name) + f"'decimal.Context' object has no attribute {name!r}") def __delattr__(self, name): raise AttributeError("%s cannot be deleted" % name) diff --git a/Lib/calendar.py b/Lib/calendar.py index 92fe6b7723fe268..1d17f2d7f9810e9 100644 --- a/Lib/calendar.py +++ b/Lib/calendar.py @@ -54,7 +54,7 @@ def __getattr__(name): else: return 2 - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + raise AttributeError(f"module '{__name__}' has no attribute {name!r}") # Constants for months diff --git a/Lib/enum.py b/Lib/enum.py index 7aff36c94ce1dcf..e581c3d34a15f57 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -190,9 +190,8 @@ def __get__(self, instance, ownerclass=None): if self.member is not None: return self.member else: - raise AttributeError( - '%r has no attribute %r' % (ownerclass, self.name) - ) + errmsg = f'{ownerclass!r} has no attribute {self.name!r}' + raise AttributeError(errmsg) if self.fget is not None: # use previous enum.property return self.fget(instance) @@ -206,9 +205,8 @@ def __get__(self, instance, ownerclass=None): try: return ownerclass._member_map_[self.name] except KeyError: - raise AttributeError( - '%r has no attribute %r' % (ownerclass, self.name) - ) from None + errmsg = f'{ownerclass!r} has no attribute {self.name!r}' + raise AttributeError(errmsg) from None def __set__(self, instance, value): if self.fset is not None: diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 1d2c105ac047e18..2d254c75a624933 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -1226,6 +1226,10 @@ def test_getattr(self): self.assertRaises(AttributeError, getattr, sys, chr(sys.maxunicode)) # unicode surrogates are not encodable to the default encoding (utf8) self.assertRaises(AttributeError, getattr, 1, "\uDAD1\uD51E") + with self.assertRaises(AttributeError) as cm: + getattr(object(), "a'b") + self.assertEqual(str(cm.exception), + ''''object' object has no attribute "a'b"''') def test_hasattr(self): self.assertTrue(hasattr(sys, 'stdout')) @@ -2155,6 +2159,15 @@ def test_setattr(self): msg = r"^attribute name must be string, not 'int'$" self.assertRaisesRegex(TypeError, msg, setattr, sys, 1, 'spam') + class A: + __slots__ = () + + with self.assertRaises(AttributeError) as cm: + setattr(object(), "a'b", 1) + self.assertEqual(str(cm.exception), + """'object' object has no attribute "a'b" """ + "and no __dict__ for setting new attributes") + # test_str(): see test_str.py and test_bytes.py for str() tests. def test_sum(self): diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 1c723b25784da11..5dc5e51b07968f0 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -3695,6 +3695,19 @@ def test_to_integral_value(self): self.assertRaises(TypeError, c.to_integral_value, '10') self.assertRaises(TypeError, c.to_integral_value, 10, 'x') + def test_setattr(self): + Context = self.decimal.Context + + ctx = Context() + ctx.prec = 123 + with self.assertRaises(AttributeError): + ctx.xyz = 1 + with self.assertRaises(AttributeError) as cm: + setattr(ctx, "a'b", 1) + self.assertEqual(str(cm.exception), + ''''decimal.Context' object has no attribute "a'b"''') + + @requires_cdecimal class CContextAPItests(ContextAPItests, unittest.TestCase): decimal = C diff --git a/Lib/test/test_module/__init__.py b/Lib/test/test_module/__init__.py index 22132b01c8a0563..a837b42ef57de25 100644 --- a/Lib/test/test_module/__init__.py +++ b/Lib/test/test_module/__init__.py @@ -47,6 +47,9 @@ def test_missing_getattr(self): self.assertRaisesRegex( AttributeError, "module 'foo' has no attribute 'not_here'", getattr, foo, "not_here") + self.assertRaisesRegex( + AttributeError, '''module 'foo' has no attribute "a'b"''', + getattr, foo, "a'b") def test_no_docstring(self): # Regularly initialized module, no docstring diff --git a/Lib/test/test_unittest/test_case.py b/Lib/test/test_unittest/test_case.py index 83b42918e1eff6d..4552c0093319385 100644 --- a/Lib/test/test_unittest/test_case.py +++ b/Lib/test/test_unittest/test_case.py @@ -796,6 +796,10 @@ def testAssertHasAttr(self): self.assertHasAttr(a, 'y') self.assertEqual(str(cm.exception), "'List' object has no attribute 'y'") + with self.assertRaises(self.failureException) as cm: + self.assertHasAttr(a, "a'b") + self.assertEqual(str(cm.exception), + ''''List' object has no attribute "a'b"''') with self.assertRaises(self.failureException) as cm: self.assertHasAttr(List, 'spam') self.assertEqual(str(cm.exception), diff --git a/Lib/test/test_unittest/testmock/testmock.py b/Lib/test/test_unittest/testmock/testmock.py index 764585ec5d54688..408efb813ec336c 100644 --- a/Lib/test/test_unittest/testmock/testmock.py +++ b/Lib/test/test_unittest/testmock/testmock.py @@ -685,6 +685,11 @@ def test_attributes(mock): "Mock object has no attribute 'z'", getattr, mock, 'z' ) + self.assertRaisesRegex( + AttributeError, + '''Mock object has no attribute "a'b"''', + getattr, mock, "a'b" + ) self.assertRaisesRegex( AttributeError, "Mock object has no attribute '__foobar__'", diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 49011faaa51eaed..2bbcc6bb97f8010 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -808,13 +808,13 @@ def __setattr__(self, name, value): elif (self._spec_set and self._mock_methods is not None and name not in self._mock_methods and name not in self.__dict__): - raise AttributeError("Mock object has no attribute '%s'" % name) + raise AttributeError(f"Mock object has no attribute {name!r}") elif name in _unsupported_magics: - msg = 'Attempting to set unsupported magic method %r.' % name + msg = f'Attempting to set unsupported magic method {name!r}.' raise AttributeError(msg) elif name in _all_magics: if self._mock_methods is not None and name not in self._mock_methods: - raise AttributeError("Mock object has no attribute '%s'" % name) + raise AttributeError(f"Mock object has no attribute {name!r}") if not _is_instance_mock(value): setattr(type(self), name, _get_method(name, value)) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-24-15-50-43.gh-issue-154610.t5PtGH.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-24-15-50-43.gh-issue-154610.t5PtGH.rst new file mode 100644 index 000000000000000..4030e603e121a27 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-24-15-50-43.gh-issue-154610.t5PtGH.rst @@ -0,0 +1,4 @@ +Format the attribute name with :func:`repr`, instead of :func:`str`, when +formatting :exc:`AttributeError` error message. For example, +``getattr(object(), "a'b")`` now formats the attribute error correctly. +Patch by Victor Stinner. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index c650aa456d2cc9d..277bb7ef9e3302a 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -7497,7 +7497,7 @@ store_instance_attr_lock_held(PyObject *obj, PyDictValues *values, PyObject *old_value = values->values[ix]; if (old_value == NULL && value == NULL) { PyErr_Format(PyExc_AttributeError, - "'%.100s' object has no attribute '%U'", + "'%s' object has no attribute %R", Py_TYPE(obj)->tp_name, name); (void)_PyObject_SetAttributeErrorContext(obj, name); return -1; diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 0fffd36ad462dab..247d03775df3d69 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -1361,7 +1361,7 @@ descriptor_set_wrapped_attribute(PyObject *oobj, PyObject *name, PyObject *value if (PyErr_ExceptionMatches(PyExc_KeyError)) { PyErr_Clear(); PyErr_Format(PyExc_AttributeError, - "'%.200s' object has no attribute '%U'", + "'%s' object has no attribute %R", type_name, name); Py_DECREF(dict); return -1; diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c index b8cd6025c20ba56..84d2c4c70a47f15 100644 --- a/Objects/moduleobject.c +++ b/Objects/moduleobject.c @@ -1422,7 +1422,7 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) if (!mod_name || !PyUnicode_Check(mod_name)) { Py_XDECREF(mod_name); PyErr_Format(PyExc_AttributeError, - "module has no attribute '%U'", name); + "module has no attribute %R", name); return NULL; } PyObject *spec; @@ -1432,7 +1432,7 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) } if (spec == NULL) { PyErr_Format(PyExc_AttributeError, - "module '%U' has no attribute '%U'", + "module '%U' has no attribute %R", mod_name, name); Py_DECREF(mod_name); return NULL; @@ -1466,9 +1466,9 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) if (is_possibly_shadowing_stdlib) { assert(origin); PyErr_Format(PyExc_AttributeError, - "module '%U' has no attribute '%U' " - "(consider renaming '%U' since it has the same " - "name as the standard library module named '%U' " + "module %R has no attribute %R " + "(consider renaming %R since it has the same " + "name as the standard library module named %R " "and prevents importing that standard library module)", mod_name, name, origin, mod_name); } @@ -1483,22 +1483,22 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) // For non-stdlib modules, only mention the possibility of // shadowing if the module is being initialized. PyErr_Format(PyExc_AttributeError, - "module '%U' has no attribute '%U' " - "(consider renaming '%U' if it has the same name " + "module %R has no attribute %R " + "(consider renaming %R if it has the same name " "as a library you intended to import)", mod_name, name, origin); } else if (origin) { PyErr_Format(PyExc_AttributeError, "partially initialized " - "module '%U' from '%U' has no attribute '%U' " + "module %R from %R has no attribute %R " "(most likely due to a circular import)", mod_name, origin, name); } else { PyErr_Format(PyExc_AttributeError, "partially initialized " - "module '%U' has no attribute '%U' " + "module %R has no attribute %R " "(most likely due to a circular import)", mod_name, name); } @@ -1514,7 +1514,7 @@ _Py_module_getattro_impl(PyModuleObject *m, PyObject *name, int suppress) } else if (rc == 0) { PyErr_Format(PyExc_AttributeError, - "module '%U' has no attribute '%U'", + "module %R has no attribute %R", mod_name, name); } } diff --git a/Objects/object.c b/Objects/object.c index fadd9273a36607c..bc7232a5f89310f 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1330,7 +1330,7 @@ PyObject_GetAttr(PyObject *v, PyObject *name) } else { PyErr_Format(PyExc_AttributeError, - "'%.100s' object has no attribute '%U'", + "'%s' object has no attribute %R", tp->tp_name, name); } @@ -1377,7 +1377,7 @@ _PyObject_GetAttrStackRef(PyObject *v, PyObject *name) } else { PyErr_Format(PyExc_AttributeError, - "'%.100s' object has no attribute '%U'", + "'%s' object has no attribute %R", tp->tp_name, name); } @@ -1548,14 +1548,14 @@ PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value) _PyObject_ASSERT(name, Py_REFCNT(name) >= 1); if (tp->tp_getattr == NULL && tp->tp_getattro == NULL) PyErr_Format(PyExc_TypeError, - "'%.100s' object has no attributes " + "'%s' object has no attributes " "(%s .%U)", tp->tp_name, value==NULL ? "del" : "assign to", name); else PyErr_Format(PyExc_TypeError, - "'%.100s' object has only read-only attributes " + "'%s' object has only read-only attributes " "(%s .%U)", tp->tp_name, value==NULL ? "del" : "assign to", @@ -1737,7 +1737,7 @@ _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method) } PyErr_Format(PyExc_AttributeError, - "'%.100s' object has no attribute '%U'", + "'%s' object has no attribute %R", tp->tp_name, name); _PyObject_SetAttributeErrorContext(obj, name); @@ -1871,7 +1871,7 @@ _PyObject_GetMethodStackRef(PyThreadState *ts, _PyStackRef *self, } PyErr_Format(PyExc_AttributeError, - "'%.100s' object has no attribute '%U'", + "'%s' object has no attribute %R", tp->tp_name, name); _PyObject_SetAttributeErrorContext(obj, name); @@ -1995,7 +1995,7 @@ _PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, if (!suppress) { PyErr_Format(PyExc_AttributeError, - "'%.100s' object has no attribute '%U'", + "'%s' object has no attribute %R", tp->tp_name, name); _PyObject_SetAttributeErrorContext(obj, name); @@ -2070,13 +2070,13 @@ _PyObject_GenericSetAttrWithDict(PyObject *obj, PyObject *name, if (descr == NULL) { if (tp->tp_setattro == PyObject_GenericSetAttr) { PyErr_Format(PyExc_AttributeError, - "'%.100s' object has no attribute '%U' and no " + "'%s' object has no attribute %R and no " "__dict__ for setting new attributes", tp->tp_name, name); } else { PyErr_Format(PyExc_AttributeError, - "'%.100s' object has no attribute '%U'", + "'%s' object has no attribute %R", tp->tp_name, name); } _PyObject_SetAttributeErrorContext(obj, name); @@ -2103,7 +2103,7 @@ _PyObject_GenericSetAttrWithDict(PyObject *obj, PyObject *name, error_check: if (res < 0 && PyErr_ExceptionMatches(PyExc_KeyError)) { PyErr_Format(PyExc_AttributeError, - "'%.100s' object has no attribute '%U'", + "'%s' object has no attribute %R", tp->tp_name, name); _PyObject_SetAttributeErrorContext(obj, name); } diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 60e26eab069b094..49b18e7259b7bb1 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6513,7 +6513,7 @@ _Py_type_getattro_stackref(PyTypeObject *type, PyObject *name, /* Give up */ if (suppress_missing_attribute == NULL) { PyErr_Format(PyExc_AttributeError, - "type object '%.100s' has no attribute '%U'", + "type object '%s' has no attribute %R", type->tp_name, name); } else { @@ -6552,7 +6552,7 @@ type_update_dict(PyTypeObject *type, PyDictObject *dict, PyObject *name, if (_PyDict_SetItem_LockHeld(dict, name, value) < 0) { PyErr_Format(PyExc_AttributeError, - "type object '%.50s' has no attribute '%U'", + "type object '%s' has no attribute %R", ((PyTypeObject*)type)->tp_name, name); _PyObject_SetAttributeErrorContext((PyObject *)type, name); return -1;