[Python-checkins] bpo-27794: Add `name` attribute to `property` class (GH-23967)

rhettinger webhook-mailer at python.org
Wed Dec 30 04:51:58 EST 2020


https://github.com/python/cpython/commit/c56387f80c5aabf8100ceaffe365cc004ce0d7e0
commit: c56387f80c5aabf8100ceaffe365cc004ce0d7e0
branch: master
author: Yurii Karabas <1998uriyyo at gmail.com>
committer: rhettinger <rhettinger at users.noreply.github.com>
date: 2020-12-30T01:51:24-08:00
summary:

bpo-27794: Add `name` attribute to `property` class (GH-23967)

files:
A Misc/NEWS.d/next/Core and Builtins/2020-12-27-18-07-43.bpo-27794.sxgfGi.rst
M Doc/howto/descriptor.rst
M Lib/test/test_property.py
M Lib/test/test_sys.py
M Objects/descrobject.c

diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst
index ab5a573c6a06d..d172c9b181c1c 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 172737ade143f..7f3813fc8cd15 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 3860656c181c2..3af5b117affde 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 0000000000000..0f66b4effc5df
--- /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 a8ce13c7aa4ba..16c695a08f47d 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;
 }
 



More information about the Python-checkins mailing list