From de0962bb5693043e6cad75567668417366c31675 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Sun, 27 Dec 2020 18:16:15 +0200 Subject: [PATCH 1/8] Add `name` attribute to `property` class --- Doc/howto/descriptor.rst | 29 ++++++--- Lib/test/test_property.py | 40 +++++++++++++ Lib/test/test_sys.py | 2 +- .../2020-12-27-18-07-43.bpo-27794.sxgfGi.rst | 2 + Objects/clinic/descrobject.c.h | 25 +++++--- Objects/descrobject.c | 60 ++++++++++++++++--- 6 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2020-12-27-18-07-43.bpo-27794.sxgfGi.rst diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index ab5a573c6a06d1..9d128a6fe582ec 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -907,7 +907,7 @@ Properties Calling :func:`property` is a succinct way of building a data descriptor that triggers a function call upon access to an attribute. Its signature is:: - property(fget=None, fset=None, fdel=None, doc=None) -> property + property(fget=None, fset=None, fdel=None, doc=None, name=None) -> property The documentation shows a typical use to define a managed attribute ``x``: @@ -927,39 +927,52 @@ here is a pure Python equivalent: class Property: "Emulate PyProperty_Type() in Objects/descrobject.c" - def __init__(self, fget=None, fset=None, fdel=None, doc=None): + def __init__(self, fget=None, fset=None, fdel=None, doc=None, name=None): self.fget = fget self.fset = fset self.fdel = fdel if doc is None and fget is not None: doc = fget.__doc__ self.__doc__ = doc + self.name = 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( + "unreadable attribute" + + (" " + self.name if self.name is not None else "") + ) return self.fget(obj) def __set__(self, obj, value): if self.fset is None: - raise AttributeError("can't set attribute") + raise AttributeError( + "can't set attribute" + + (" " + self.name if self.name is not None else "") + ) self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: - raise AttributeError("can't delete attribute") + raise AttributeError( + "can't delete attribute" + + (" " + self.name if self.name is not None else "") + ) self.fdel(obj) def getter(self, fget): - return type(self)(fget, self.fset, self.fdel, self.__doc__) + return type(self)(fget, self.fset, self.fdel, self.__doc__, self.name) def setter(self, fset): - return type(self)(self.fget, fset, self.fdel, self.__doc__) + return type(self)(self.fget, fset, self.fdel, self.__doc__, self.name) def deleter(self, fdel): - return type(self)(self.fget, self.fset, fdel, self.__doc__) + return type(self)(self.fget, self.fset, fdel, self.__doc__, self.name) .. testcode:: :hide: diff --git a/Lib/test/test_property.py b/Lib/test/test_property.py index 172737ade143fa..c7a4bdc3165f75 100644 --- a/Lib/test/test_property.py +++ b/Lib/test/test_property.py @@ -299,6 +299,46 @@ def spam(self): self.assertEqual(Foo.spam.__doc__, "a new docstring") +class _PropertyUnreachableAttribute: + obj = None + prop = None + + def _format_exc_msg(self, msg): + if self.prop.name is None: + return msg + + return "{} {}".format(msg, self.prop.name) + + def setUpClass(self): + class TestCls: + foo = self.prop + + self.obj = TestCls() + + 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): + def setUpClass(self): + self.prop = property(name='foo') + super().setUpClass() + + +class PropertyUnreachableAttributeNoName(_PropertyUnreachableAttribute, unittest.TestCase): + def setUpClass(self): + self.prop = property() + super().setUpClass() + 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..4c125ba80a5669 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2020-12-27-18-07-43.bpo-27794.sxgfGi.rst @@ -0,0 +1,2 @@ +Add ``name`` attribute to ``property`` class to add more information when +``AttributeError`` exception raised. Patch provided by Yurii Karabas. diff --git a/Objects/clinic/descrobject.c.h b/Objects/clinic/descrobject.c.h index d248b91bf48da2..4eaa19c07107a0 100644 --- a/Objects/clinic/descrobject.c.h +++ b/Objects/clinic/descrobject.c.h @@ -28,7 +28,7 @@ mappingproxy_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) } PyDoc_STRVAR(property_init__doc__, -"property(fget=None, fset=None, fdel=None, doc=None)\n" +"property(fget=None, fset=None, fdel=None, doc=None, name=None)\n" "--\n" "\n" "Property attribute.\n" @@ -41,6 +41,8 @@ PyDoc_STRVAR(property_init__doc__, " function to be used for del\'ing an attribute\n" " doc\n" " docstring\n" +" name\n" +" name of a property\n" "\n" "Typical use is to define a managed attribute x:\n" "\n" @@ -66,15 +68,15 @@ PyDoc_STRVAR(property_init__doc__, static int property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset, - PyObject *fdel, PyObject *doc); + PyObject *fdel, PyObject *doc, PyObject *name); static int property_init(PyObject *self, PyObject *args, PyObject *kwargs) { int return_value = -1; - static const char * const _keywords[] = {"fget", "fset", "fdel", "doc", NULL}; + static const char * const _keywords[] = {"fget", "fset", "fdel", "doc", "name", NULL}; static _PyArg_Parser _parser = {NULL, _keywords, "property", 0}; - PyObject *argsbuf[4]; + PyObject *argsbuf[5]; PyObject * const *fastargs; Py_ssize_t nargs = PyTuple_GET_SIZE(args); Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; @@ -82,8 +84,9 @@ property_init(PyObject *self, PyObject *args, PyObject *kwargs) PyObject *fset = NULL; PyObject *fdel = NULL; PyObject *doc = NULL; + PyObject *name = NULL; - fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 0, 4, 0, argsbuf); + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 0, 5, 0, argsbuf); if (!fastargs) { goto exit; } @@ -108,11 +111,17 @@ property_init(PyObject *self, PyObject *args, PyObject *kwargs) goto skip_optional_pos; } } - doc = fastargs[3]; + if (fastargs[3]) { + doc = fastargs[3]; + if (!--noptargs) { + goto skip_optional_pos; + } + } + name = fastargs[4]; skip_optional_pos: - return_value = property_init_impl((propertyobject *)self, fget, fset, fdel, doc); + return_value = property_init_impl((propertyobject *)self, fget, fset, fdel, doc, name); exit: return return_value; } -/*[clinic end generated code: output=916624e717862abc input=a9049054013a1b77]*/ +/*[clinic end generated code: output=562ff56bb7a4dbf0 input=a9049054013a1b77]*/ diff --git a/Objects/descrobject.c b/Objects/descrobject.c index a8ce13c7aa4bab..2329bd4ad34e50 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1457,13 +1457,14 @@ PyWrapper_New(PyObject *d, PyObject *self) /* class property(object): - def __init__(self, fget=None, fset=None, fdel=None, doc=None): + def __init__(self, fget=None, fset=None, fdel=None, doc=None, name=None): if doc is None and fget is not None and hasattr(fget, "__doc__"): doc = fget.__doc__ self.__get = fget self.__set = fset self.__del = fdel self.__doc__ = doc + self.name = name def __get__(self, inst, type=None): if inst is None: @@ -1490,6 +1491,7 @@ typedef struct { PyObject *prop_set; PyObject *prop_del; PyObject *prop_doc; + PyObject *prop_name; int getter_doc; } propertyobject; @@ -1501,6 +1503,7 @@ static PyMemberDef property_members[] = { {"fset", T_OBJECT, offsetof(propertyobject, prop_set), READONLY}, {"fdel", T_OBJECT, offsetof(propertyobject, prop_del), READONLY}, {"__doc__", T_OBJECT, offsetof(propertyobject, prop_doc), 0}, + {"name", T_OBJECT, offsetof(propertyobject, prop_name), READONLY}, {0} }; @@ -1535,10 +1538,30 @@ 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) { + propertyobject *prop = (propertyobject *)self; + PyObject *name = PyTuple_GetItem(args, 1); + + if (name == NULL) { + return NULL; + } + + 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 +1576,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 +1590,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 != Py_None) { + PyErr_Format(PyExc_AttributeError, "unreadable attribute %S", gs->prop_name); + } else { + PyErr_SetString(PyExc_AttributeError, "unreadable attribute"); + } + return NULL; } @@ -1584,10 +1613,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 != Py_None) { + PyErr_Format(PyExc_AttributeError, value == NULL ? - "can't delete attribute" : - "can't set attribute"); + "can't delete attribute %S" : + "can't set attribute %S", + gs->prop_name); + } else { + PyErr_SetString(PyExc_AttributeError, + value == NULL ? + "can't delete attribute" : + "can't set attribute"); + } return -1; } if (value == NULL) @@ -1630,7 +1667,7 @@ property_copy(PyObject *old, PyObject *get, PyObject *set, PyObject *del) doc = pold->prop_doc ? pold->prop_doc : Py_None; } - new = PyObject_CallFunctionObjArgs(type, get, set, del, doc, NULL); + new = PyObject_CallFunctionObjArgs(type, get, set, del, doc, pold->prop_name, NULL); Py_DECREF(type); if (new == NULL) return NULL; @@ -1648,6 +1685,8 @@ property.__init__ as property_init function to be used for del'ing an attribute doc: object(c_default="NULL") = None docstring + name: object(c_default="NULL") = None + name of a property Property attribute. @@ -1676,8 +1715,8 @@ class C(object): static int property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset, - PyObject *fdel, PyObject *doc) -/*[clinic end generated code: output=01a960742b692b57 input=dfb5dbbffc6932d5]*/ + PyObject *fdel, PyObject *doc, PyObject *name) +/*[clinic end generated code: output=e9a13f227c218be9 input=fa7a7e0378f4e253]*/ { if (fget == Py_None) fget = NULL; @@ -1685,16 +1724,20 @@ property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset, fset = NULL; if (fdel == Py_None) fdel = NULL; + if (name == Py_None) + name = NULL; Py_XINCREF(fget); Py_XINCREF(fset); Py_XINCREF(fdel); Py_XINCREF(doc); + Py_XINCREF(name); Py_XSETREF(self->prop_get, fget); Py_XSETREF(self->prop_set, fset); Py_XSETREF(self->prop_del, fdel); Py_XSETREF(self->prop_doc, doc); + Py_XSETREF(self->prop_name, name); 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; } From a0414666d9b3c958729e0ab3e23621c6e9b95a1e Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Sun, 27 Dec 2020 18:58:32 +0200 Subject: [PATCH 2/8] Fix property test cases --- Lib/test/test_property.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_property.py b/Lib/test/test_property.py index c7a4bdc3165f75..385b05d7453947 100644 --- a/Lib/test/test_property.py +++ b/Lib/test/test_property.py @@ -304,16 +304,19 @@ class _PropertyUnreachableAttribute: prop = None def _format_exc_msg(self, msg): - if self.prop.name is None: + name = self.prop[0].name + + if name is None: return msg - return "{} {}".format(msg, self.prop.name) + return "{} {}".format(msg, name) - def setUpClass(self): + @classmethod + def setUpClass(cls): class TestCls: - foo = self.prop + foo, = cls.prop - self.obj = TestCls() + cls.obj = TestCls() def test_get_property(self): with self.assertRaisesRegex(AttributeError, self._format_exc_msg("unreadable attribute")): @@ -329,15 +332,11 @@ def test_del_property(self): class PropertyUnreachableAttributeWithName(_PropertyUnreachableAttribute, unittest.TestCase): - def setUpClass(self): - self.prop = property(name='foo') - super().setUpClass() + prop = property(name='foo'), class PropertyUnreachableAttributeNoName(_PropertyUnreachableAttribute, unittest.TestCase): - def setUpClass(self): - self.prop = property() - super().setUpClass() + prop = property(), if __name__ == '__main__': From 89b5b2193f4fbb1c451e67770fbebb40f1cefe4b Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Mon, 28 Dec 2020 00:00:09 +0200 Subject: [PATCH 3/8] Fix issue with incorrect exc message --- Lib/test/test_property.py | 4 ++-- Objects/descrobject.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_property.py b/Lib/test/test_property.py index 385b05d7453947..5c81e20b0f8bd4 100644 --- a/Lib/test/test_property.py +++ b/Lib/test/test_property.py @@ -307,9 +307,9 @@ def _format_exc_msg(self, msg): name = self.prop[0].name if name is None: - return msg + return '^{}$'.format(msg) - return "{} {}".format(msg, name) + return "^{} {}$".format(msg, name) @classmethod def setUpClass(cls): diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 2329bd4ad34e50..46e470482b8bb9 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1590,7 +1590,7 @@ property_descr_get(PyObject *self, PyObject *obj, PyObject *type) propertyobject *gs = (propertyobject *)self; if (gs->prop_get == NULL) { - if (gs->prop_name != Py_None) { + if (gs->prop_name != NULL) { PyErr_Format(PyExc_AttributeError, "unreadable attribute %S", gs->prop_name); } else { PyErr_SetString(PyExc_AttributeError, "unreadable attribute"); @@ -1613,7 +1613,7 @@ property_descr_set(PyObject *self, PyObject *obj, PyObject *value) else func = gs->prop_set; if (func == NULL) { - if (gs->prop_name != Py_None) { + if (gs->prop_name != NULL) { PyErr_Format(PyExc_AttributeError, value == NULL ? "can't delete attribute %S" : From f02f68ceb2bc27b979eff516c74f6cfc9117c555 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Mon, 28 Dec 2020 01:18:29 +0200 Subject: [PATCH 4/8] remove `name` from `property` init method --- Doc/howto/descriptor.rst | 18 ++++++++++++------ Lib/test/test_property.py | 17 +++++++++-------- Objects/clinic/descrobject.c.h | 25 ++++++++----------------- Objects/descrobject.c | 19 ++++++++----------- 4 files changed, 37 insertions(+), 42 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 9d128a6fe582ec..4a922568be0620 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -907,7 +907,7 @@ Properties Calling :func:`property` is a succinct way of building a data descriptor that triggers a function call upon access to an attribute. Its signature is:: - property(fget=None, fset=None, fdel=None, doc=None, name=None) -> property + property(fget=None, fset=None, fdel=None, doc=None) -> property The documentation shows a typical use to define a managed attribute ``x``: @@ -927,14 +927,14 @@ here is a pure Python equivalent: class Property: "Emulate PyProperty_Type() in Objects/descrobject.c" - def __init__(self, fget=None, fset=None, fdel=None, doc=None, name=None): + def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel if doc is None and fget is not None: doc = fget.__doc__ self.__doc__ = doc - self.name = name + self.name = None def __set_name__(self, owner, name): self.name = name @@ -966,13 +966,19 @@ here is a pure Python equivalent: self.fdel(obj) def getter(self, fget): - return type(self)(fget, self.fset, self.fdel, self.__doc__, self.name) + 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__, self.name) + 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__, self.name) + 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 5c81e20b0f8bd4..963e30ae96c782 100644 --- a/Lib/test/test_property.py +++ b/Lib/test/test_property.py @@ -301,10 +301,10 @@ def spam(self): class _PropertyUnreachableAttribute: obj = None - prop = None + cls = None def _format_exc_msg(self, msg): - name = self.prop[0].name + name = self.cls.foo.name if name is None: return '^{}$'.format(msg) @@ -313,10 +313,7 @@ def _format_exc_msg(self, msg): @classmethod def setUpClass(cls): - class TestCls: - foo, = cls.prop - - cls.obj = TestCls() + cls.obj = cls.cls() def test_get_property(self): with self.assertRaisesRegex(AttributeError, self._format_exc_msg("unreadable attribute")): @@ -332,11 +329,15 @@ def test_del_property(self): class PropertyUnreachableAttributeWithName(_PropertyUnreachableAttribute, unittest.TestCase): - prop = property(name='foo'), + class cls: + foo = property() class PropertyUnreachableAttributeNoName(_PropertyUnreachableAttribute, unittest.TestCase): - prop = property(), + class cls: + pass + + cls.foo = property() if __name__ == '__main__': diff --git a/Objects/clinic/descrobject.c.h b/Objects/clinic/descrobject.c.h index 4eaa19c07107a0..d248b91bf48da2 100644 --- a/Objects/clinic/descrobject.c.h +++ b/Objects/clinic/descrobject.c.h @@ -28,7 +28,7 @@ mappingproxy_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) } PyDoc_STRVAR(property_init__doc__, -"property(fget=None, fset=None, fdel=None, doc=None, name=None)\n" +"property(fget=None, fset=None, fdel=None, doc=None)\n" "--\n" "\n" "Property attribute.\n" @@ -41,8 +41,6 @@ PyDoc_STRVAR(property_init__doc__, " function to be used for del\'ing an attribute\n" " doc\n" " docstring\n" -" name\n" -" name of a property\n" "\n" "Typical use is to define a managed attribute x:\n" "\n" @@ -68,15 +66,15 @@ PyDoc_STRVAR(property_init__doc__, static int property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset, - PyObject *fdel, PyObject *doc, PyObject *name); + PyObject *fdel, PyObject *doc); static int property_init(PyObject *self, PyObject *args, PyObject *kwargs) { int return_value = -1; - static const char * const _keywords[] = {"fget", "fset", "fdel", "doc", "name", NULL}; + static const char * const _keywords[] = {"fget", "fset", "fdel", "doc", NULL}; static _PyArg_Parser _parser = {NULL, _keywords, "property", 0}; - PyObject *argsbuf[5]; + PyObject *argsbuf[4]; PyObject * const *fastargs; Py_ssize_t nargs = PyTuple_GET_SIZE(args); Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; @@ -84,9 +82,8 @@ property_init(PyObject *self, PyObject *args, PyObject *kwargs) PyObject *fset = NULL; PyObject *fdel = NULL; PyObject *doc = NULL; - PyObject *name = NULL; - fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 0, 5, 0, argsbuf); + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, 0, 4, 0, argsbuf); if (!fastargs) { goto exit; } @@ -111,17 +108,11 @@ property_init(PyObject *self, PyObject *args, PyObject *kwargs) goto skip_optional_pos; } } - if (fastargs[3]) { - doc = fastargs[3]; - if (!--noptargs) { - goto skip_optional_pos; - } - } - name = fastargs[4]; + doc = fastargs[3]; skip_optional_pos: - return_value = property_init_impl((propertyobject *)self, fget, fset, fdel, doc, name); + return_value = property_init_impl((propertyobject *)self, fget, fset, fdel, doc); exit: return return_value; } -/*[clinic end generated code: output=562ff56bb7a4dbf0 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=916624e717862abc input=a9049054013a1b77]*/ diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 46e470482b8bb9..4bcd0036f5a48f 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1457,14 +1457,13 @@ PyWrapper_New(PyObject *d, PyObject *self) /* class property(object): - def __init__(self, fget=None, fset=None, fdel=None, doc=None, name=None): + def __init__(self, fget=None, fset=None, fdel=None, doc=None): if doc is None and fget is not None and hasattr(fget, "__doc__"): doc = fget.__doc__ self.__get = fget self.__set = fset self.__del = fdel self.__doc__ = doc - self.name = name def __get__(self, inst, type=None): if inst is None: @@ -1667,10 +1666,12 @@ property_copy(PyObject *old, PyObject *get, PyObject *set, PyObject *del) doc = pold->prop_doc ? pold->prop_doc : Py_None; } - new = PyObject_CallFunctionObjArgs(type, get, set, del, doc, pold->prop_name, NULL); + new = PyObject_CallFunctionObjArgs(type, get, set, del, doc, NULL); Py_DECREF(type); if (new == NULL) return NULL; + + Py_XSETREF(((propertyobject *) new)->prop_name, pold->prop_name); return new; } @@ -1685,8 +1686,6 @@ property.__init__ as property_init function to be used for del'ing an attribute doc: object(c_default="NULL") = None docstring - name: object(c_default="NULL") = None - name of a property Property attribute. @@ -1715,8 +1714,8 @@ class C(object): static int property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset, - PyObject *fdel, PyObject *doc, PyObject *name) -/*[clinic end generated code: output=e9a13f227c218be9 input=fa7a7e0378f4e253]*/ + PyObject *fdel, PyObject *doc) +/*[clinic end generated code: output=01a960742b692b57 input=dfb5dbbffc6932d5]*/ { if (fget == Py_None) fget = NULL; @@ -1724,20 +1723,18 @@ property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset, fset = NULL; if (fdel == Py_None) fdel = NULL; - if (name == Py_None) - name = NULL; Py_XINCREF(fget); Py_XINCREF(fset); Py_XINCREF(fdel); Py_XINCREF(doc); - Py_XINCREF(name); Py_XSETREF(self->prop_get, fget); Py_XSETREF(self->prop_set, fset); Py_XSETREF(self->prop_del, fdel); Py_XSETREF(self->prop_doc, doc); - Py_XSETREF(self->prop_name, name); + Py_XSETREF(self->prop_name, NULL); + self->getter_doc = 0; /* if no docstring given and the getter has one, use that one */ From b5029073fa337f3bbc1b35e60f7fe4e214b8c668 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Mon, 28 Dec 2020 10:38:44 +0200 Subject: [PATCH 5/8] Make name attribute private --- Doc/howto/descriptor.rst | 25 ++++++------------- Lib/test/test_property.py | 12 ++++----- .../2020-12-27-18-07-43.bpo-27794.sxgfGi.rst | 5 ++-- Objects/descrobject.c | 2 +- 4 files changed, 18 insertions(+), 26 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 4a922568be0620..2821e80d3e5d2d 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -934,50 +934,41 @@ here is a pure Python equivalent: if doc is None and fget is not None: doc = fget.__doc__ self.__doc__ = doc - self.name = None + self._name = '' def __set_name__(self, owner, name): - self.name = 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" + - (" " + self.name if self.name is not None else "") - ) + raise AttributeError(f'unreadable attribute {self._name}'.strip()) return self.fget(obj) def __set__(self, obj, value): if self.fset is None: - raise AttributeError( - "can't set attribute" + - (" " + self.name if self.name is not None else "") - ) + raise AttributeError(f'can't set attribute {self._name}'.strip()) self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: - raise AttributeError( - "can't delete attribute" + - (" " + self.name if self.name is not None else "") - ) + raise AttributeError(f'can't delete attribute {self._name}'.strip()) self.fdel(obj) def getter(self, fget): prop = type(self)(fget, self.fset, self.fdel, self.__doc__) - prop.name = self.name + prop._name = self._name return prop def setter(self, fset): prop = type(self)(self.fget, fset, self.fdel, self.__doc__) - prop.name = self.name + prop._name = self._name return prop def deleter(self, fdel): prop = type(self)(self.fget, self.fset, fdel, self.__doc__) - prop.name = self.name + prop._name = self._name return prop .. testcode:: diff --git a/Lib/test/test_property.py b/Lib/test/test_property.py index 963e30ae96c782..7bab64b747847c 100644 --- a/Lib/test/test_property.py +++ b/Lib/test/test_property.py @@ -300,16 +300,12 @@ def spam(self): class _PropertyUnreachableAttribute: + msg_format = None obj = None cls = None def _format_exc_msg(self, msg): - name = self.cls.foo.name - - if name is None: - return '^{}$'.format(msg) - - return "^{} {}$".format(msg, name) + return self.msg_format.format(msg) @classmethod def setUpClass(cls): @@ -329,11 +325,15 @@ def test_del_property(self): class PropertyUnreachableAttributeWithName(_PropertyUnreachableAttribute, unittest.TestCase): + msg_format = "^{} foo$" + class cls: foo = property() class PropertyUnreachableAttributeNoName(_PropertyUnreachableAttribute, unittest.TestCase): + msg_format = "^{}$" + class cls: pass 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 index 4c125ba80a5669..0f66b4effc5df2 100644 --- 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 @@ -1,2 +1,3 @@ -Add ``name`` attribute to ``property`` class to add more information when -``AttributeError`` exception raised. Patch provided by Yurii Karabas. +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 4bcd0036f5a48f..ef64ce270988fb 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1502,7 +1502,6 @@ static PyMemberDef property_members[] = { {"fset", T_OBJECT, offsetof(propertyobject, prop_set), READONLY}, {"fdel", T_OBJECT, offsetof(propertyobject, prop_del), READONLY}, {"__doc__", T_OBJECT, offsetof(propertyobject, prop_doc), 0}, - {"name", T_OBJECT, offsetof(propertyobject, prop_name), READONLY}, {0} }; @@ -1671,6 +1670,7 @@ property_copy(PyObject *old, PyObject *get, PyObject *set, PyObject *del) if (new == NULL) return NULL; + Py_XINCREF(pold->prop_name); Py_XSETREF(((propertyobject *) new)->prop_name, pold->prop_name); return new; } From 6aa55bda52b4d0ef78bc0cc37e18b9388018cd02 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Mon, 28 Dec 2020 10:49:17 +0200 Subject: [PATCH 6/8] fix docs --- Doc/howto/descriptor.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 2821e80d3e5d2d..9caacb671243a0 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -948,12 +948,12 @@ here is a pure Python equivalent: def __set__(self, obj, value): if self.fset is None: - raise AttributeError(f'can't set attribute {self._name}'.strip()) + raise AttributeError(f'can\'t set attribute {self._name}'.strip()) self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: - raise AttributeError(f'can't delete attribute {self._name}'.strip()) + raise AttributeError(f'can\'t delete attribute {self._name}'.strip()) self.fdel(obj) def getter(self, fget): From 2c8376932dea5cd30166b48353a5800f612930ca Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Tue, 29 Dec 2020 10:34:45 +0200 Subject: [PATCH 7/8] Remove strip call from docs, remove extra blank line --- Doc/howto/descriptor.rst | 6 +++--- Objects/descrobject.c | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 9caacb671243a0..b47e016eb14c27 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -943,17 +943,17 @@ here is a pure Python equivalent: if obj is None: return self if self.fget is None: - raise AttributeError(f'unreadable attribute {self._name}'.strip()) + raise AttributeError(f'unreadable attribute {self._name}') return self.fget(obj) def __set__(self, obj, value): if self.fset is None: - raise AttributeError(f'can\'t set attribute {self._name}'.strip()) + raise AttributeError(f'can\'t set attribute {self._name}') self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: - raise AttributeError(f'can\'t delete attribute {self._name}'.strip()) + raise AttributeError(f'can\'t delete attribute {self._name}') self.fdel(obj) def getter(self, fget): diff --git a/Objects/descrobject.c b/Objects/descrobject.c index ef64ce270988fb..2b142ad09597bd 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1554,7 +1554,6 @@ property_set_name(PyObject *self, PyObject *args) { Py_RETURN_NONE; } - static PyMethodDef property_methods[] = { {"getter", property_getter, METH_O, getter_doc}, {"setter", property_setter, METH_O, setter_doc}, From d2b4e63aaa35920c7fce6436ff0ad6c3f07fa05c Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Tue, 29 Dec 2020 22:28:37 +0200 Subject: [PATCH 8/8] Update docs and property __set_name__ method --- Doc/howto/descriptor.rst | 4 ++-- Lib/test/test_property.py | 12 +++++++++++- Objects/descrobject.c | 18 +++++++++++------- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index b47e016eb14c27..d172c9b181c1ce 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -948,12 +948,12 @@ here is a pure Python equivalent: def __set__(self, obj, value): if self.fset is None: - raise AttributeError(f'can\'t set attribute {self._name}') + raise AttributeError(f"can't set attribute {self._name}") self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: - raise AttributeError(f'can\'t delete attribute {self._name}') + raise AttributeError(f"can't delete attribute {self._name}") self.fdel(obj) def getter(self, fget): diff --git a/Lib/test/test_property.py b/Lib/test/test_property.py index 7bab64b747847c..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): @@ -325,7 +335,7 @@ def test_del_property(self): class PropertyUnreachableAttributeWithName(_PropertyUnreachableAttribute, unittest.TestCase): - msg_format = "^{} foo$" + msg_format = "^{} 'foo'$" class cls: foo = property() diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 2b142ad09597bd..16c695a08f47d9 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1541,13 +1541,17 @@ PyDoc_STRVAR(set_name_doc, static PyObject * property_set_name(PyObject *self, PyObject *args) { - propertyobject *prop = (propertyobject *)self; - PyObject *name = PyTuple_GetItem(args, 1); - - if (name == NULL) { + 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); @@ -1588,7 +1592,7 @@ property_descr_get(PyObject *self, PyObject *obj, PyObject *type) propertyobject *gs = (propertyobject *)self; if (gs->prop_get == NULL) { if (gs->prop_name != NULL) { - PyErr_Format(PyExc_AttributeError, "unreadable attribute %S", gs->prop_name); + PyErr_Format(PyExc_AttributeError, "unreadable attribute %R", gs->prop_name); } else { PyErr_SetString(PyExc_AttributeError, "unreadable attribute"); } @@ -1613,8 +1617,8 @@ property_descr_set(PyObject *self, PyObject *obj, PyObject *value) if (gs->prop_name != NULL) { PyErr_Format(PyExc_AttributeError, value == NULL ? - "can't delete attribute %S" : - "can't set attribute %S", + "can't delete attribute %R" : + "can't set attribute %R", gs->prop_name); } else { PyErr_SetString(PyExc_AttributeError,