From 5da403906890e0232645334efcc0efed8dca534c Mon Sep 17 00:00:00 2001 From: Gobot1234 Date: Tue, 4 Apr 2023 01:35:26 +0100 Subject: [PATCH 01/11] Add a better error message for __dict__-less classes setattr --- Lib/test/test_class.py | 13 +++++++++++-- Objects/object.c | 8 ++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index 61df81b169775e..a8540c744e4c7b 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -640,7 +640,7 @@ class A: pass class B: y = 0 - __slots__ = ('z',) + __slots__ = ('z', "foo") error_msg = "'A' object has no attribute 'x'" with self.assertRaisesRegex(AttributeError, error_msg): @@ -648,7 +648,8 @@ class B: with self.assertRaisesRegex(AttributeError, error_msg): del A().x - error_msg = "'B' object has no attribute 'x'" + error_msg = ("'B' object has no attribute 'x' and no " + "__dict__ for setting new attributes") with self.assertRaisesRegex(AttributeError, error_msg): B().x with self.assertRaisesRegex(AttributeError, error_msg): @@ -656,6 +657,14 @@ class B: with self.assertRaisesRegex(AttributeError, error_msg): B().x = 0 + with self.assertRaisesRegex( + AttributeError, + "'B' object has no attribute 'x' and no " + "__dict__ for setting new attributes. Did you mean: 'foo'" + "" + ): + B().fod = 1 + error_msg = "'B' object attribute 'y' is read-only" with self.assertRaisesRegex(AttributeError, error_msg): del B().y diff --git a/Objects/object.c b/Objects/object.c index 9dd5eb998217f6..d3f2a650dfeb81 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1546,8 +1546,10 @@ _PyObject_GenericSetAttrWithDict(PyObject *obj, PyObject *name, if (dictptr == NULL) { if (descr == NULL) { PyErr_Format(PyExc_AttributeError, - "'%.100s' object has no attribute '%U'", + "'%.100s' object has no attribute '%U' and no " + "__dict__ for setting new attributes", tp->tp_name, name); + set_attribute_error_context(obj, name); } else { PyErr_Format(PyExc_AttributeError, @@ -1577,9 +1579,11 @@ _PyObject_GenericSetAttrWithDict(PyObject *obj, PyObject *name, } else { PyErr_Format(PyExc_AttributeError, - "'%.100s' object has no attribute '%U'", + "'%.100s' object has no attribute '%U' and no " + "__dict__ for setting new attributes", tp->tp_name, name); } + set_attribute_error_context(obj, name); } done: Py_XDECREF(descr); From dd9d18d1c797937d918f0a7f91577ed309561493 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 4 Apr 2023 00:40:07 +0000 Subject: [PATCH 02/11] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2023-04-04-00-40-04.gh-issue-96663.PdR9hK.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2023-04-04-00-40-04.gh-issue-96663.PdR9hK.rst diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-04-04-00-40-04.gh-issue-96663.PdR9hK.rst b/Misc/NEWS.d/next/Core and Builtins/2023-04-04-00-40-04.gh-issue-96663.PdR9hK.rst new file mode 100644 index 00000000000000..cb806b5ea7a9f3 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-04-04-00-40-04.gh-issue-96663.PdR9hK.rst @@ -0,0 +1 @@ +Add a better, more introspect-able error message when setting attributes on classes without a ``__dict__`` and no slot member for the attribute. From 8395973cc0c87c850cb097c0cdf099e97f12fc6a Mon Sep 17 00:00:00 2001 From: Gobot1234 Date: Tue, 4 Apr 2023 10:41:00 +0100 Subject: [PATCH 03/11] Fix tests --- Lib/test/test_class.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index a8540c744e4c7b..fc926680d0fbf3 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -648,13 +648,15 @@ class B: with self.assertRaisesRegex(AttributeError, error_msg): del A().x - error_msg = ("'B' object has no attribute 'x' and no " - "__dict__ for setting new attributes") + error_msg = ("'B' object has no attribute 'x'") with self.assertRaisesRegex(AttributeError, error_msg): B().x with self.assertRaisesRegex(AttributeError, error_msg): del B().x - with self.assertRaisesRegex(AttributeError, error_msg): + with self.assertRaisesRegex( + AttributeError, + "'B' object has no attribute 'x' and no __dict__ for setting new attributes" + ): B().x = 0 with self.assertRaisesRegex( From cb1810fcda216dad3db9cb54e4bd109ba7e0c3a7 Mon Sep 17 00:00:00 2001 From: Gobot1234 Date: Tue, 4 Apr 2023 11:49:07 +0100 Subject: [PATCH 04/11] Fix formatting nits --- Lib/test/test_class.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index fc926680d0fbf3..4ef048e0e20997 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -640,7 +640,7 @@ class A: pass class B: y = 0 - __slots__ = ('z', "foo") + __slots__ = ('z', 'foo') error_msg = "'A' object has no attribute 'x'" with self.assertRaisesRegex(AttributeError, error_msg): @@ -648,7 +648,7 @@ class B: with self.assertRaisesRegex(AttributeError, error_msg): del A().x - error_msg = ("'B' object has no attribute 'x'") + error_msg = "'B' object has no attribute 'x'" with self.assertRaisesRegex(AttributeError, error_msg): B().x with self.assertRaisesRegex(AttributeError, error_msg): From 88cbb1b49854d2b46ac1fb424a25ae31dda701d4 Mon Sep 17 00:00:00 2001 From: Gobot1234 Date: Tue, 4 Apr 2023 11:49:18 +0100 Subject: [PATCH 05/11] Fix another broken test --- Lib/test/test_descrtut.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_descrtut.py b/Lib/test/test_descrtut.py index 7796031ed0602f..b6a78a18f84564 100644 --- a/Lib/test/test_descrtut.py +++ b/Lib/test/test_descrtut.py @@ -8,9 +8,9 @@ # of much interest anymore), and a few were fiddled to make the output # deterministic. -from test.support import sortdict import doctest import unittest +from test.support import sortdict class defaultdict(dict): @@ -139,7 +139,7 @@ def merge(self, other): >>> a.x1 = 1 Traceback (most recent call last): File "", line 1, in ? - AttributeError: 'defaultdict2' object has no attribute 'x1' + AttributeError: 'defaultdict2' object has no attribute 'x1' and no __dict__ for setting new attributes >>> """ From 9b11ec9b723401514d348bd7efd5f7114caafca5 Mon Sep 17 00:00:00 2001 From: Gobot1234 Date: Tue, 4 Apr 2023 11:55:41 +0100 Subject: [PATCH 06/11] Fix more tests I think --- Lib/test/test_class.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index 4ef048e0e20997..7fb920ccc8c5df 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -661,8 +661,8 @@ class B: with self.assertRaisesRegex( AttributeError, - "'B' object has no attribute 'x' and no " - "__dict__ for setting new attributes. Did you mean: 'foo'" + "'B' object has no attribute 'fod' and no " + r"__dict__ for setting new attributes. Did you mean: 'foo'\?" "" ): B().fod = 1 From edc27972c67faeeaeed17bf8ed9f0bc950199256 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Tue, 4 Apr 2023 13:17:35 +0100 Subject: [PATCH 07/11] Remove test for suggestions --- Lib/test/test_class.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index 7fb920ccc8c5df..38266f9931e868 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -640,7 +640,7 @@ class A: pass class B: y = 0 - __slots__ = ('z', 'foo') + __slots__ = ('z',) error_msg = "'A' object has no attribute 'x'" with self.assertRaisesRegex(AttributeError, error_msg): @@ -659,14 +659,6 @@ class B: ): B().x = 0 - with self.assertRaisesRegex( - AttributeError, - "'B' object has no attribute 'fod' and no " - r"__dict__ for setting new attributes. Did you mean: 'foo'\?" - "" - ): - B().fod = 1 - error_msg = "'B' object attribute 'y' is read-only" with self.assertRaisesRegex(AttributeError, error_msg): del B().y From cac1d3358fb3037be1364e787bc5ea90468430a8 Mon Sep 17 00:00:00 2001 From: Gobot1234 Date: Tue, 4 Apr 2023 16:16:23 +0100 Subject: [PATCH 08/11] Remove the mention of __dict__ for del X().y when y doesn't exist --- Objects/object.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Objects/object.c b/Objects/object.c index d3f2a650dfeb81..e12662e8a7287a 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1579,8 +1579,7 @@ _PyObject_GenericSetAttrWithDict(PyObject *obj, PyObject *name, } else { PyErr_Format(PyExc_AttributeError, - "'%.100s' object has no attribute '%U' and no " - "__dict__ for setting new attributes", + "'%.100s' object has no attribute '%U'", tp->tp_name, name); } set_attribute_error_context(obj, name); From 0644b531fc7d02e07f3bb97749fb7db82f936c7b Mon Sep 17 00:00:00 2001 From: Gobot1234 Date: Tue, 4 Apr 2023 19:56:12 +0100 Subject: [PATCH 09/11] Try checking for a __setattr__ hook before mentioning __dict__ --- Lib/test/test_class.py | 14 +++++++++++++- Objects/object.c | 15 +++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index 38266f9931e868..31859fba2544b0 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -2,7 +2,6 @@ import unittest - testmeths = [ # Binary operations @@ -641,6 +640,14 @@ class A: class B: y = 0 __slots__ = ('z',) + class C: + __slots__ = ("y",) + + def __setattr__(self, name, value) -> None: + if name == "z": + super().__setattr__("y", 1) + else: + super().__setattr__(name, value) error_msg = "'A' object has no attribute 'x'" with self.assertRaisesRegex(AttributeError, error_msg): @@ -658,6 +665,11 @@ class B: "'B' object has no attribute 'x' and no __dict__ for setting new attributes" ): B().x = 0 + with self.assertRaisesRegex( + AttributeError, + "'C' object has no attribute 'x'" + ): + B().x = 0 error_msg = "'B' object attribute 'y' is read-only" with self.assertRaisesRegex(AttributeError, error_msg): diff --git a/Objects/object.c b/Objects/object.c index e12662e8a7287a..b875dab03376f7 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1545,10 +1545,17 @@ _PyObject_GenericSetAttrWithDict(PyObject *obj, PyObject *name, } if (dictptr == NULL) { if (descr == NULL) { - PyErr_Format(PyExc_AttributeError, - "'%.100s' object has no attribute '%U' and no " - "__dict__ for setting new attributes", - tp->tp_name, name); + if (tp->tp_setattro == PyObject_GenericSetAttr) { + PyErr_Format(PyExc_AttributeError, + "'%.100s' object has no attribute '%U' and no " + "__dict__ for setting new attributes", + tp->tp_name, name); + } + else { + PyErr_Format(PyExc_AttributeError, + "'%.100s' object has no attribute '%U'", + tp->tp_name, name); + } set_attribute_error_context(obj, name); } else { From 8642463d3060ae871bf5a36d92d97659a4c0231b Mon Sep 17 00:00:00 2001 From: Gobot1234 Date: Wed, 5 Apr 2023 11:17:25 +0100 Subject: [PATCH 10/11] Fix typo Co-authored-by: Crowthebird <78076854+thatbirdguythatuknownot@users.noreply.github.com> --- Lib/test/test_class.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index 31859fba2544b0..1f0762e8bfeaee 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -2,6 +2,7 @@ import unittest + testmeths = [ # Binary operations @@ -669,7 +670,7 @@ def __setattr__(self, name, value) -> None: AttributeError, "'C' object has no attribute 'x'" ): - B().x = 0 + C().x = 0 error_msg = "'B' object attribute 'y' is read-only" with self.assertRaisesRegex(AttributeError, error_msg): From 60e1f693b8ccc0772f29745deda8a1e90baafc33 Mon Sep 17 00:00:00 2001 From: James Hilton-Balfe Date: Tue, 18 Jul 2023 20:31:15 +0100 Subject: [PATCH 11/11] Fix import order --- Lib/test/test_descrtut.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_descrtut.py b/Lib/test/test_descrtut.py index b6a78a18f84564..13e3ea41bdb76c 100644 --- a/Lib/test/test_descrtut.py +++ b/Lib/test/test_descrtut.py @@ -8,9 +8,9 @@ # of much interest anymore), and a few were fiddled to make the output # deterministic. +from test.support import sortdict import doctest import unittest -from test.support import sortdict class defaultdict(dict):