diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index ab5a573c6a06d1..d172c9b181c1ce 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -934,32 +934,42 @@ here is a pure Python equivalent: if doc is None and fget is not None: doc = fget.__doc__ self.__doc__ = doc + self._name = '' + + def __set_name__(self, owner, name): + self._name = name def __get__(self, obj, objtype=None): if obj is None: return self if self.fget is None: - raise AttributeError("unreadable attribute") + raise AttributeError(f'unreadable attribute {self._name}') return self.fget(obj) def __set__(self, obj, value): if self.fset is None: - raise AttributeError("can't set attribute") + raise AttributeError(f"can't set attribute {self._name}") self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: - raise AttributeError("can't delete attribute") + raise AttributeError(f"can't delete attribute {self._name}") self.fdel(obj) def getter(self, fget): - return type(self)(fget, self.fset, self.fdel, self.__doc__) + prop = type(self)(fget, self.fset, self.fdel, self.__doc__) + prop._name = self._name + return prop def setter(self, fset): - return type(self)(self.fget, fset, self.fdel, self.__doc__) + prop = type(self)(self.fget, fset, self.fdel, self.__doc__) + prop._name = self._name + return prop def deleter(self, fdel): - return type(self)(self.fget, self.fset, fdel, self.__doc__) + prop = type(self)(self.fget, self.fset, fdel, self.__doc__) + prop._name = self._name + return prop .. testcode:: :hide: diff --git a/Lib/test/test_property.py b/Lib/test/test_property.py index 172737ade143fa..7f3813fc8cd15e 100644 --- a/Lib/test/test_property.py +++ b/Lib/test/test_property.py @@ -204,6 +204,16 @@ def __doc__(cls): return 'Second' self.assertEqual(A.__doc__, 'Second') + def test_property_set_name_incorrect_args(self): + p = property() + + for i in (0, 1, 3): + with self.assertRaisesRegex( + TypeError, + fr'^__set_name__\(\) takes 2 positional arguments but {i} were given$' + ): + p.__set_name__(*([0] * i)) + # Issue 5890: subclasses of property do not preserve method __doc__ strings class PropertySub(property): @@ -299,6 +309,46 @@ def spam(self): self.assertEqual(Foo.spam.__doc__, "a new docstring") +class _PropertyUnreachableAttribute: + msg_format = None + obj = None + cls = None + + def _format_exc_msg(self, msg): + return self.msg_format.format(msg) + + @classmethod + def setUpClass(cls): + cls.obj = cls.cls() + + def test_get_property(self): + with self.assertRaisesRegex(AttributeError, self._format_exc_msg("unreadable attribute")): + self.obj.foo + + def test_set_property(self): + with self.assertRaisesRegex(AttributeError, self._format_exc_msg("can't set attribute")): + self.obj.foo = None + + def test_del_property(self): + with self.assertRaisesRegex(AttributeError, self._format_exc_msg("can't delete attribute")): + del self.obj.foo + + +class PropertyUnreachableAttributeWithName(_PropertyUnreachableAttribute, unittest.TestCase): + msg_format = "^{} 'foo'$" + + class cls: + foo = property() + + +class PropertyUnreachableAttributeNoName(_PropertyUnreachableAttribute, unittest.TestCase): + msg_format = "^{}$" + + class cls: + pass + + cls.foo = property() + if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 3860656c181c2b..3af5b117affde6 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1329,7 +1329,7 @@ def getx(self): return self.__x def setx(self, value): self.__x = value def delx(self): del self.__x x = property(getx, setx, delx, "") - check(x, size('4Pi')) + check(x, size('5Pi')) # PyCapsule # XXX # rangeiterator diff --git a/Misc/NEWS.d/next/Core and Builtins/2020-12-27-18-07-43.bpo-27794.sxgfGi.rst b/Misc/NEWS.d/next/Core and Builtins/2020-12-27-18-07-43.bpo-27794.sxgfGi.rst new file mode 100644 index 00000000000000..0f66b4effc5df2 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2020-12-27-18-07-43.bpo-27794.sxgfGi.rst @@ -0,0 +1,3 @@ +Improve the error message for failed writes/deletes to property objects. +When possible, the attribute name is now shown. Patch provided by +Yurii Karabas. diff --git a/Objects/descrobject.c b/Objects/descrobject.c index a8ce13c7aa4bab..16c695a08f47d9 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1490,6 +1490,7 @@ typedef struct { PyObject *prop_set; PyObject *prop_del; PyObject *prop_doc; + PyObject *prop_name; int getter_doc; } propertyobject; @@ -1535,10 +1536,33 @@ property_deleter(PyObject *self, PyObject *deleter) } +PyDoc_STRVAR(set_name_doc, + "Method to set name of a property."); + +static PyObject * +property_set_name(PyObject *self, PyObject *args) { + if (PyTuple_GET_SIZE(args) != 2) { + PyErr_Format( + PyExc_TypeError, + "__set_name__() takes 2 positional arguments but %d were given", + PyTuple_GET_SIZE(args)); + return NULL; + } + + propertyobject *prop = (propertyobject *)self; + PyObject *name = PyTuple_GET_ITEM(args, 1); + + Py_XINCREF(name); + Py_XSETREF(prop->prop_name, name); + + Py_RETURN_NONE; +} + static PyMethodDef property_methods[] = { {"getter", property_getter, METH_O, getter_doc}, {"setter", property_setter, METH_O, setter_doc}, {"deleter", property_deleter, METH_O, deleter_doc}, + {"__set_name__", property_set_name, METH_VARARGS, set_name_doc}, {0} }; @@ -1553,6 +1577,7 @@ property_dealloc(PyObject *self) Py_XDECREF(gs->prop_set); Py_XDECREF(gs->prop_del); Py_XDECREF(gs->prop_doc); + Py_XDECREF(gs->prop_name); Py_TYPE(self)->tp_free(self); } @@ -1566,7 +1591,12 @@ property_descr_get(PyObject *self, PyObject *obj, PyObject *type) propertyobject *gs = (propertyobject *)self; if (gs->prop_get == NULL) { - PyErr_SetString(PyExc_AttributeError, "unreadable attribute"); + if (gs->prop_name != NULL) { + PyErr_Format(PyExc_AttributeError, "unreadable attribute %R", gs->prop_name); + } else { + PyErr_SetString(PyExc_AttributeError, "unreadable attribute"); + } + return NULL; } @@ -1584,10 +1614,18 @@ property_descr_set(PyObject *self, PyObject *obj, PyObject *value) else func = gs->prop_set; if (func == NULL) { - PyErr_SetString(PyExc_AttributeError, + if (gs->prop_name != NULL) { + PyErr_Format(PyExc_AttributeError, value == NULL ? - "can't delete attribute" : - "can't set attribute"); + "can't delete attribute %R" : + "can't set attribute %R", + gs->prop_name); + } else { + PyErr_SetString(PyExc_AttributeError, + value == NULL ? + "can't delete attribute" : + "can't set attribute"); + } return -1; } if (value == NULL) @@ -1634,6 +1672,9 @@ property_copy(PyObject *old, PyObject *get, PyObject *set, PyObject *del) Py_DECREF(type); if (new == NULL) return NULL; + + Py_XINCREF(pold->prop_name); + Py_XSETREF(((propertyobject *) new)->prop_name, pold->prop_name); return new; } @@ -1695,6 +1736,8 @@ property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset, Py_XSETREF(self->prop_set, fset); Py_XSETREF(self->prop_del, fdel); Py_XSETREF(self->prop_doc, doc); + Py_XSETREF(self->prop_name, NULL); + self->getter_doc = 0; /* if no docstring given and the getter has one, use that one */ @@ -1769,6 +1812,7 @@ property_traverse(PyObject *self, visitproc visit, void *arg) Py_VISIT(pp->prop_set); Py_VISIT(pp->prop_del); Py_VISIT(pp->prop_doc); + Py_VISIT(pp->prop_name); return 0; }