Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-27794: Add name attribute to property class #23967

Merged
merged 8 commits into from
Dec 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions Doc/howto/descriptor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
50 changes: 50 additions & 0 deletions Lib/test/test_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()
2 changes: 1 addition & 1 deletion Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
52 changes: 48 additions & 4 deletions Objects/descrobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,7 @@ typedef struct {
PyObject *prop_set;
PyObject *prop_del;
PyObject *prop_doc;
PyObject *prop_name;
int getter_doc;
} propertyobject;

Expand Down Expand Up @@ -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},
rhettinger marked this conversation as resolved.
Show resolved Hide resolved
{0}
};

Expand All @@ -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);
}

Expand All @@ -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;
}

Expand All @@ -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)
Expand Down Expand Up @@ -1634,6 +1672,9 @@ property_copy(PyObject *old, PyObject *get, PyObject *set, PyObject *del)
Py_DECREF(type);
if (new == NULL)
return NULL;

uriyyo marked this conversation as resolved.
Show resolved Hide resolved
Py_XINCREF(pold->prop_name);
Py_XSETREF(((propertyobject *) new)->prop_name, pold->prop_name);
return new;
}

Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
}

Expand Down