gh-101860: Expose __name__ on property (GH-101876)

Useful for introspection and consistent with functions and other
descriptors.
This commit is contained in:
Eugene Toder 2024-02-20 10:14:34 -05:00 committed by GitHub
parent 9af80ec83d
commit c0b0c2f201
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 158 additions and 25 deletions

View File

@ -1004,31 +1004,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 = '' self._name = None
def __set_name__(self, owner, name): def __set_name__(self, owner, name):
self._name = name self._name = name
@property
def __name__(self):
return self._name if self._name is not None else self.fget.__name__
@__name__.setter
def __name__(self, value):
self._name = value
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( raise AttributeError(
f'property {self._name!r} of {type(obj).__name__!r} object has no getter' f'property {self.__name__!r} of {type(obj).__name__!r} '
'object has no getter'
) )
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( raise AttributeError(
f'property {self._name!r} of {type(obj).__name__!r} object has no setter' f'property {self.__name__!r} of {type(obj).__name__!r} '
'object has no setter'
) )
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( raise AttributeError(
f'property {self._name!r} of {type(obj).__name__!r} object has no deleter' f'property {self.__name__!r} of {type(obj).__name__!r} '
'object has no deleter'
) )
self.fdel(obj) self.fdel(obj)

View File

@ -834,9 +834,8 @@ def _finddoc(obj):
cls = self.__class__ cls = self.__class__
# Should be tested before isdatadescriptor(). # Should be tested before isdatadescriptor().
elif isinstance(obj, property): elif isinstance(obj, property):
func = obj.fget name = obj.__name__
name = func.__name__ cls = _findclass(obj.fget)
cls = _findclass(func)
if cls is None or getattr(cls, name) is not obj: if cls is None or getattr(cls, name) is not obj:
return None return None
elif ismethoddescriptor(obj) or isdatadescriptor(obj): elif ismethoddescriptor(obj) or isdatadescriptor(obj):

View File

@ -127,9 +127,8 @@ def _finddoc(obj):
cls = self.__class__ cls = self.__class__
# Should be tested before isdatadescriptor(). # Should be tested before isdatadescriptor().
elif isinstance(obj, property): elif isinstance(obj, property):
func = obj.fget name = obj.__name__
name = func.__name__ cls = _findclass(obj.fget)
cls = _findclass(func)
if cls is None or getattr(cls, name) is not obj: if cls is None or getattr(cls, name) is not obj:
return None return None
elif inspect.ismethoddescriptor(obj) or inspect.isdatadescriptor(obj): elif inspect.ismethoddescriptor(obj) or inspect.isdatadescriptor(obj):

View File

@ -68,9 +68,9 @@ class FesteringGob(MalodorousPervert, ParrotDroppings):
def abuse(self, a, b, c): def abuse(self, a, b, c):
pass pass
@property def _getter(self):
def contradiction(self):
pass pass
contradiction = property(_getter)
async def lobbest(grenade): async def lobbest(grenade):
pass pass

View File

@ -201,6 +201,59 @@ class PropertyTests(unittest.TestCase):
self.assertIsNone(prop.fdel) self.assertIsNone(prop.fdel)
self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10) self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10)
def test_property_name(self):
def getter(self):
return 42
def setter(self, value):
pass
class A:
@property
def foo(self):
return 1
@foo.setter
def oof(self, value):
pass
bar = property(getter)
baz = property(None, setter)
self.assertEqual(A.foo.__name__, 'foo')
self.assertEqual(A.oof.__name__, 'oof')
self.assertEqual(A.bar.__name__, 'bar')
self.assertEqual(A.baz.__name__, 'baz')
A.quux = property(getter)
self.assertEqual(A.quux.__name__, 'getter')
A.quux.__name__ = 'myquux'
self.assertEqual(A.quux.__name__, 'myquux')
self.assertEqual(A.bar.__name__, 'bar') # not affected
A.quux.__name__ = None
self.assertIsNone(A.quux.__name__)
with self.assertRaisesRegex(
AttributeError, "'property' object has no attribute '__name__'"
):
property(None, setter).__name__
with self.assertRaisesRegex(
AttributeError, "'property' object has no attribute '__name__'"
):
property(1).__name__
class Err:
def __getattr__(self, attr):
raise RuntimeError('fail')
p = property(Err())
with self.assertRaisesRegex(RuntimeError, 'fail'):
p.__name__
p.__name__ = 'not_fail'
self.assertEqual(p.__name__, 'not_fail')
def test_property_set_name_incorrect_args(self): def test_property_set_name_incorrect_args(self):
p = property() p = property()

View File

@ -1162,6 +1162,17 @@ class PydocImportTest(PydocBaseTest):
self.assertEqual(loaded_pydoc.__spec__, pydoc.__spec__) self.assertEqual(loaded_pydoc.__spec__, pydoc.__spec__)
class Rect:
@property
def area(self):
'''Area of the rect'''
return self.w * self.h
class Square(Rect):
area = property(lambda self: self.side**2)
class TestDescriptions(unittest.TestCase): class TestDescriptions(unittest.TestCase):
def test_module(self): def test_module(self):
@ -1550,13 +1561,13 @@ cm(x) class method of test.test_pydoc.test_pydoc.X
@requires_docstrings @requires_docstrings
def test_property(self): def test_property(self):
class Rect:
@property
def area(self):
'''Area of the rect'''
return self.w * self.h
self.assertEqual(self._get_summary_lines(Rect.area), """\ self.assertEqual(self._get_summary_lines(Rect.area), """\
area
Area of the rect
""")
# inherits the docstring from Rect.area
self.assertEqual(self._get_summary_lines(Square.area), """\
area
Area of the rect Area of the rect
""") """)
self.assertIn(""" self.assertIn("""

View File

@ -0,0 +1 @@
Expose ``__name__`` attribute on property.

View File

@ -1519,22 +1519,34 @@ class property(object):
self.__doc__ = doc self.__doc__ = doc
except AttributeError: # read-only or dict-less class except AttributeError: # read-only or dict-less class
pass pass
self.__name = None
def __set_name__(self, owner, name):
self.__name = name
@property
def __name__(self):
return self.__name if self.__name is not None else self.fget.__name__
@__name__.setter
def __name__(self, value):
self.__name = value
def __get__(self, inst, type=None): def __get__(self, inst, type=None):
if inst is None: if inst is None:
return self return self
if self.__get is None: if self.__get is None:
raise AttributeError, "property has no getter" raise AttributeError("property has no getter")
return self.__get(inst) return self.__get(inst)
def __set__(self, inst, value): def __set__(self, inst, value):
if self.__set is None: if self.__set is None:
raise AttributeError, "property has no setter" raise AttributeError("property has no setter")
return self.__set(inst, value) return self.__set(inst, value)
def __delete__(self, inst): def __delete__(self, inst):
if self.__del is None: if self.__del is None:
raise AttributeError, "property has no deleter" raise AttributeError("property has no deleter")
return self.__del(inst) return self.__del(inst)
*/ */
@ -1628,6 +1640,20 @@ property_dealloc(PyObject *self)
Py_TYPE(self)->tp_free(self); Py_TYPE(self)->tp_free(self);
} }
static int
property_name(propertyobject *prop, PyObject **name)
{
if (prop->prop_name != NULL) {
*name = Py_NewRef(prop->prop_name);
return 1;
}
if (prop->prop_get == NULL) {
*name = NULL;
return 0;
}
return PyObject_GetOptionalAttr(prop->prop_get, &_Py_ID(__name__), name);
}
static PyObject * static PyObject *
property_descr_get(PyObject *self, PyObject *obj, PyObject *type) property_descr_get(PyObject *self, PyObject *obj, PyObject *type)
{ {
@ -1637,11 +1663,15 @@ 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) {
PyObject *propname;
if (property_name(gs, &propname) < 0) {
return NULL;
}
PyObject *qualname = PyType_GetQualName(Py_TYPE(obj)); PyObject *qualname = PyType_GetQualName(Py_TYPE(obj));
if (gs->prop_name != NULL && qualname != NULL) { if (propname != NULL && qualname != NULL) {
PyErr_Format(PyExc_AttributeError, PyErr_Format(PyExc_AttributeError,
"property %R of %R object has no getter", "property %R of %R object has no getter",
gs->prop_name, propname,
qualname); qualname);
} }
else if (qualname != NULL) { else if (qualname != NULL) {
@ -1652,6 +1682,7 @@ property_descr_get(PyObject *self, PyObject *obj, PyObject *type)
PyErr_SetString(PyExc_AttributeError, PyErr_SetString(PyExc_AttributeError,
"property has no getter"); "property has no getter");
} }
Py_XDECREF(propname);
Py_XDECREF(qualname); Py_XDECREF(qualname);
return NULL; return NULL;
} }
@ -1673,16 +1704,20 @@ property_descr_set(PyObject *self, PyObject *obj, PyObject *value)
} }
if (func == NULL) { if (func == NULL) {
PyObject *propname;
if (property_name(gs, &propname) < 0) {
return -1;
}
PyObject *qualname = NULL; PyObject *qualname = NULL;
if (obj != NULL) { if (obj != NULL) {
qualname = PyType_GetQualName(Py_TYPE(obj)); qualname = PyType_GetQualName(Py_TYPE(obj));
} }
if (gs->prop_name != NULL && qualname != NULL) { if (propname != NULL && qualname != NULL) {
PyErr_Format(PyExc_AttributeError, PyErr_Format(PyExc_AttributeError,
value == NULL ? value == NULL ?
"property %R of %R object has no deleter" : "property %R of %R object has no deleter" :
"property %R of %R object has no setter", "property %R of %R object has no setter",
gs->prop_name, propname,
qualname); qualname);
} }
else if (qualname != NULL) { else if (qualname != NULL) {
@ -1698,6 +1733,7 @@ property_descr_set(PyObject *self, PyObject *obj, PyObject *value)
"property has no deleter" : "property has no deleter" :
"property has no setter"); "property has no setter");
} }
Py_XDECREF(propname);
Py_XDECREF(qualname); Py_XDECREF(qualname);
return -1; return -1;
} }
@ -1883,6 +1919,28 @@ property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset,
return 0; return 0;
} }
static PyObject *
property_get__name__(propertyobject *prop, void *Py_UNUSED(ignored))
{
PyObject *name;
if (property_name(prop, &name) < 0) {
return NULL;
}
if (name == NULL) {
PyErr_SetString(PyExc_AttributeError,
"'property' object has no attribute '__name__'");
}
return name;
}
static int
property_set__name__(propertyobject *prop, PyObject *value,
void *Py_UNUSED(ignored))
{
Py_XSETREF(prop->prop_name, Py_XNewRef(value));
return 0;
}
static PyObject * static PyObject *
property_get___isabstractmethod__(propertyobject *prop, void *closure) property_get___isabstractmethod__(propertyobject *prop, void *closure)
{ {
@ -1913,6 +1971,7 @@ property_get___isabstractmethod__(propertyobject *prop, void *closure)
} }
static PyGetSetDef property_getsetlist[] = { static PyGetSetDef property_getsetlist[] = {
{"__name__", (getter)property_get__name__, (setter)property_set__name__},
{"__isabstractmethod__", {"__isabstractmethod__",
(getter)property_get___isabstractmethod__, NULL, (getter)property_get___isabstractmethod__, NULL,
NULL, NULL,