-
-
Notifications
You must be signed in to change notification settings - Fork 34.9k
gh-153785: Generate AttributeError messages from context
#153786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ac555bc
4f22a16
b30d676
28e04d3
e5c4922
3854563
1e88331
2fd216a
356f56e
8db0394
ebcc795
b6fda10
d624835
c50a356
7ad5204
b65a7fd
e99d875
8c29de3
6faf358
086a0c9
e5bb17e
f2507a5
9c81069
e905ab2
7d720ac
337cef4
6aa47db
4ff6a48
e00db5f
b5ce3c9
da8c723
c941657
9ea6c8a
f825f27
7b73fb8
7378e30
faf6e53
a86fde5
9b00321
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We dropped the |
||
| Added the :attr:`name` and :attr:`obj` attributes. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| 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, | ||
|
|
@@ -2047,6 +2048,66 @@ def blech(self): | |
| self.assertEqual("bluch", exc.name) | ||
| self.assertEqual(obj, exc.obj) | ||
|
|
||
| def test_getattr_error_message(self): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please check also obj and name attributes of AttributeError: Details 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.assertIs(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.assertIs(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.assertIs(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.assertIs(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.assertIs(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.assertIs(cm.exception.name, "missing4")
nameless_mod = ModuleType.__new__(ModuleType)
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.assertIs(cm.exception.name, "missing4") |
||
| def fqn(type): | ||
| return f'{type.__module__}.{type.__qualname__}' | ||
|
|
||
| class RaiseWithName: | ||
| def __getattr__(self, name): | ||
| raise AttributeError(name) | ||
| with self.assertRaises(AttributeError) as cm: | ||
| getattr(RaiseWithName(), "missing1") | ||
| self.assertEqual(str(cm.exception), | ||
| f"'{fqn(RaiseWithName)}' object has no attribute 'missing1'") | ||
|
|
||
| class BareRaise: | ||
| def __getattr__(self, name): | ||
| raise AttributeError | ||
| with self.assertRaises(AttributeError) as cm: | ||
| getattr(BareRaise(), "missing2") | ||
| self.assertEqual(str(cm.exception), | ||
| f"'{fqn(BareRaise)}' object has no attribute 'missing2'") | ||
|
|
||
| class RaiseCustom: | ||
| def __getattr__(self, name): | ||
| raise AttributeError("custom") | ||
| with self.assertRaises(AttributeError) as cm: | ||
| getattr(RaiseCustom(), "missing3") | ||
| self.assertEqual(str(cm.exception), "custom") | ||
|
|
||
| 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'") | ||
|
|
||
| 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'") | ||
|
|
||
| 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") | ||
|
|
||
| nameless_mod = ModuleType.__new__(ModuleType) | ||
| 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'") | ||
|
|
||
| # Note: name suggestion tests live in `test_traceback`. | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,4 @@ | ||||||||||
| The default error message is now generated from ``name`` and ``obj`` attributes | ||||||||||
| of :exc:`AttributeError` when both are set and the exception was constructed with | ||||||||||
|
Comment on lines
+1
to
+2
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
| no positional arguments, or with a single positional argument equal to ``name``. | ||||||||||
| Patch by Bartosz Sławecki. | ||||||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |||||||
| #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 +2703,49 @@ AttributeError_dealloc(PyObject *self) | |||||||
| Py_TYPE(self)->tp_free(self); | ||||||||
| } | ||||||||
|
|
||||||||
| static PyObject * | ||||||||
| AttributeError_str(PyObject *op) | ||||||||
| { | ||||||||
| PyAttributeErrorObject *self = PyAttributeErrorObject_CAST(op); | ||||||||
| PyObject *arg, *obj = NULL, *name; | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I dislike the
Suggested change
|
||||||||
|
|
||||||||
| 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) | ||||||||
|
Comment on lines
+2715
to
+2718
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please add a comment explaining why you use |
||||||||
| ) { | ||||||||
| obj = Py_NewRef(self->obj); | ||||||||
| name = Py_NewRef(self->name); | ||||||||
|
serhiy-storchaka marked this conversation as resolved.
|
||||||||
| } | ||||||||
| Py_END_CRITICAL_SECTION(); | ||||||||
|
|
||||||||
| if (!obj) { | ||||||||
| return BaseException_str(op); /* re-acquires lock */ | ||||||||
| } | ||||||||
|
|
||||||||
| PyObject *result; | ||||||||
| if (PyModule_Check(obj)) { | ||||||||
| PyModuleObject *mod = _PyModule_CAST(obj); | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: we use |
||||||||
| /* In a typical case, module's __name__ is examined instead. */ | ||||||||
| if (mod->md_name) { | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This uses cached |
||||||||
| result = PyUnicode_FromFormat("module '%U' has no attribute '%U'", | ||||||||
| mod->md_name, | ||||||||
| name); | ||||||||
|
Comment on lines
+2735
to
+2736
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| } else { | ||||||||
| result = PyUnicode_FromFormat("module has no attribute '%U'", name); | ||||||||
| } | ||||||||
| } else { | ||||||||
| result = PyUnicode_FromFormat("'%T' object has no attribute '%U'", | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This formats class objects as instances of their metaclass. Can we preserve the standard |
||||||||
| obj, name); | ||||||||
| } | ||||||||
| Py_DECREF(obj); | ||||||||
| Py_DECREF(name); | ||||||||
| return result; | ||||||||
| } | ||||||||
|
|
||||||||
| static int | ||||||||
| AttributeError_traverse(PyObject *op, visitproc visit, void *arg) | ||||||||
| { | ||||||||
|
|
@@ -2770,7 +2814,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 | ||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change surprised me but it's correct.
This addition surprised me since the PR doesn't change that. The change documents that
_PyObject_SetAttributeErrorContext()is called by many functions such asPyObject_GetAttr()to setnameandobjattributes of anAttributeError.