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

This commit is contained in:
Yurii Karabas 2020-12-30 11:51:24 +02:00 committed by GitHub
parent ba0e49a464
commit c56387f80c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 118 additions and 11 deletions

View File

@ -934,32 +934,42 @@ here is a pure Python equivalent:
if doc is None and fget is not None: if doc is None and fget is not None:
doc = fget.__doc__ doc = fget.__doc__
self.__doc__ = doc self.__doc__ = doc
self._name = ''
def __set_name__(self, owner, name):
self._name = name
def __get__(self, obj, objtype=None): def __get__(self, obj, objtype=None):
if obj is None: if obj is None:
return self return self
if self.fget is None: if self.fget is None:
raise AttributeError("unreadable attribute") raise AttributeError(f'unreadable attribute {self._name}')
return self.fget(obj) return self.fget(obj)
def __set__(self, obj, value): def __set__(self, obj, value):
if self.fset is None: if self.fset is None:
raise AttributeError("can't set attribute") raise AttributeError(f"can't set attribute {self._name}")
self.fset(obj, value) self.fset(obj, value)
def __delete__(self, obj): def __delete__(self, obj):
if self.fdel is None: if self.fdel is None:
raise AttributeError("can't delete attribute") raise AttributeError(f"can't delete attribute {self._name}")
self.fdel(obj) self.fdel(obj)
def getter(self, fget): 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): 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): 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:: .. testcode::
:hide: :hide:

View File

@ -204,6 +204,16 @@ class PropertyTests(unittest.TestCase):
return 'Second' return 'Second'
self.assertEqual(A.__doc__, '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 # Issue 5890: subclasses of property do not preserve method __doc__ strings
class PropertySub(property): class PropertySub(property):
@ -299,6 +309,46 @@ class PropertySubclassTests(unittest.TestCase):
self.assertEqual(Foo.spam.__doc__, "a new docstring") 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__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -1329,7 +1329,7 @@ class SizeofTest(unittest.TestCase):
def setx(self, value): self.__x = value def setx(self, value): self.__x = value
def delx(self): del self.__x def delx(self): del self.__x
x = property(getx, setx, delx, "") x = property(getx, setx, delx, "")
check(x, size('4Pi')) check(x, size('5Pi'))
# PyCapsule # PyCapsule
# XXX # XXX
# rangeiterator # rangeiterator

View File

@ -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.

View File

@ -1490,6 +1490,7 @@ typedef struct {
PyObject *prop_set; PyObject *prop_set;
PyObject *prop_del; PyObject *prop_del;
PyObject *prop_doc; PyObject *prop_doc;
PyObject *prop_name;
int getter_doc; int getter_doc;
} propertyobject; } 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[] = { static PyMethodDef property_methods[] = {
{"getter", property_getter, METH_O, getter_doc}, {"getter", property_getter, METH_O, getter_doc},
{"setter", property_setter, METH_O, setter_doc}, {"setter", property_setter, METH_O, setter_doc},
{"deleter", property_deleter, METH_O, deleter_doc}, {"deleter", property_deleter, METH_O, deleter_doc},
{"__set_name__", property_set_name, METH_VARARGS, set_name_doc},
{0} {0}
}; };
@ -1553,6 +1577,7 @@ property_dealloc(PyObject *self)
Py_XDECREF(gs->prop_set); Py_XDECREF(gs->prop_set);
Py_XDECREF(gs->prop_del); Py_XDECREF(gs->prop_del);
Py_XDECREF(gs->prop_doc); Py_XDECREF(gs->prop_doc);
Py_XDECREF(gs->prop_name);
Py_TYPE(self)->tp_free(self); Py_TYPE(self)->tp_free(self);
} }
@ -1566,7 +1591,12 @@ property_descr_get(PyObject *self, PyObject *obj, PyObject *type)
propertyobject *gs = (propertyobject *)self; propertyobject *gs = (propertyobject *)self;
if (gs->prop_get == NULL) { if (gs->prop_get == NULL) {
if (gs->prop_name != NULL) {
PyErr_Format(PyExc_AttributeError, "unreadable attribute %R", gs->prop_name);
} else {
PyErr_SetString(PyExc_AttributeError, "unreadable attribute"); PyErr_SetString(PyExc_AttributeError, "unreadable attribute");
}
return NULL; return NULL;
} }
@ -1584,10 +1614,18 @@ property_descr_set(PyObject *self, PyObject *obj, PyObject *value)
else else
func = gs->prop_set; func = gs->prop_set;
if (func == NULL) { if (func == NULL) {
if (gs->prop_name != NULL) {
PyErr_Format(PyExc_AttributeError,
value == NULL ?
"can't delete attribute %R" :
"can't set attribute %R",
gs->prop_name);
} else {
PyErr_SetString(PyExc_AttributeError, PyErr_SetString(PyExc_AttributeError,
value == NULL ? value == NULL ?
"can't delete attribute" : "can't delete attribute" :
"can't set attribute"); "can't set attribute");
}
return -1; return -1;
} }
if (value == NULL) if (value == NULL)
@ -1634,6 +1672,9 @@ property_copy(PyObject *old, PyObject *get, PyObject *set, PyObject *del)
Py_DECREF(type); Py_DECREF(type);
if (new == NULL) if (new == NULL)
return NULL; return NULL;
Py_XINCREF(pold->prop_name);
Py_XSETREF(((propertyobject *) new)->prop_name, pold->prop_name);
return new; 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_set, fset);
Py_XSETREF(self->prop_del, fdel); Py_XSETREF(self->prop_del, fdel);
Py_XSETREF(self->prop_doc, doc); Py_XSETREF(self->prop_doc, doc);
Py_XSETREF(self->prop_name, NULL);
self->getter_doc = 0; self->getter_doc = 0;
/* if no docstring given and the getter has one, use that one */ /* 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_set);
Py_VISIT(pp->prop_del); Py_VISIT(pp->prop_del);
Py_VISIT(pp->prop_doc); Py_VISIT(pp->prop_doc);
Py_VISIT(pp->prop_name);
return 0; return 0;
} }