Skip to content

Commit c0b0c2f

Browse files
authored
gh-101860: Expose __name__ on property (GH-101876)
Useful for introspection and consistent with functions and other descriptors.
1 parent 9af80ec commit c0b0c2f

File tree

8 files changed

+158
-25
lines changed

8 files changed

+158
-25
lines changed

Doc/howto/descriptor.rst

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,31 +1004,42 @@ here is a pure Python equivalent:
10041004
if doc is None and fget is not None:
10051005
doc = fget.__doc__
10061006
self.__doc__ = doc
1007-
self._name = ''
1007+
self._name = None
10081008

10091009
def __set_name__(self, owner, name):
10101010
self._name = name
10111011

1012+
@property
1013+
def __name__(self):
1014+
return self._name if self._name is not None else self.fget.__name__
1015+
1016+
@__name__.setter
1017+
def __name__(self, value):
1018+
self._name = value
1019+
10121020
def __get__(self, obj, objtype=None):
10131021
if obj is None:
10141022
return self
10151023
if self.fget is None:
10161024
raise AttributeError(
1017-
f'property {self._name!r} of {type(obj).__name__!r} object has no getter'
1025+
f'property {self.__name__!r} of {type(obj).__name__!r} '
1026+
'object has no getter'
10181027
)
10191028
return self.fget(obj)
10201029

10211030
def __set__(self, obj, value):
10221031
if self.fset is None:
10231032
raise AttributeError(
1024-
f'property {self._name!r} of {type(obj).__name__!r} object has no setter'
1033+
f'property {self.__name__!r} of {type(obj).__name__!r} '
1034+
'object has no setter'
10251035
)
10261036
self.fset(obj, value)
10271037

10281038
def __delete__(self, obj):
10291039
if self.fdel is None:
10301040
raise AttributeError(
1031-
f'property {self._name!r} of {type(obj).__name__!r} object has no deleter'
1041+
f'property {self.__name__!r} of {type(obj).__name__!r} '
1042+
'object has no deleter'
10321043
)
10331044
self.fdel(obj)
10341045

Lib/inspect.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -834,9 +834,8 @@ def _finddoc(obj):
834834
cls = self.__class__
835835
# Should be tested before isdatadescriptor().
836836
elif isinstance(obj, property):
837-
func = obj.fget
838-
name = func.__name__
839-
cls = _findclass(func)
837+
name = obj.__name__
838+
cls = _findclass(obj.fget)
840839
if cls is None or getattr(cls, name) is not obj:
841840
return None
842841
elif ismethoddescriptor(obj) or isdatadescriptor(obj):

Lib/pydoc.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,8 @@ def _finddoc(obj):
127127
cls = self.__class__
128128
# Should be tested before isdatadescriptor().
129129
elif isinstance(obj, property):
130-
func = obj.fget
131-
name = func.__name__
132-
cls = _findclass(func)
130+
name = obj.__name__
131+
cls = _findclass(obj.fget)
133132
if cls is None or getattr(cls, name) is not obj:
134133
return None
135134
elif inspect.ismethoddescriptor(obj) or inspect.isdatadescriptor(obj):

Lib/test/test_inspect/inspect_fodder.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ class FesteringGob(MalodorousPervert, ParrotDroppings):
6868
def abuse(self, a, b, c):
6969
pass
7070

71-
@property
72-
def contradiction(self):
71+
def _getter(self):
7372
pass
73+
contradiction = property(_getter)
7474

7575
async def lobbest(grenade):
7676
pass

Lib/test/test_property.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,59 @@ def test_gh_115618(self):
201201
self.assertIsNone(prop.fdel)
202202
self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10)
203203

204+
def test_property_name(self):
205+
def getter(self):
206+
return 42
207+
208+
def setter(self, value):
209+
pass
210+
211+
class A:
212+
@property
213+
def foo(self):
214+
return 1
215+
216+
@foo.setter
217+
def oof(self, value):
218+
pass
219+
220+
bar = property(getter)
221+
baz = property(None, setter)
222+
223+
self.assertEqual(A.foo.__name__, 'foo')
224+
self.assertEqual(A.oof.__name__, 'oof')
225+
self.assertEqual(A.bar.__name__, 'bar')
226+
self.assertEqual(A.baz.__name__, 'baz')
227+
228+
A.quux = property(getter)
229+
self.assertEqual(A.quux.__name__, 'getter')
230+
A.quux.__name__ = 'myquux'
231+
self.assertEqual(A.quux.__name__, 'myquux')
232+
self.assertEqual(A.bar.__name__, 'bar') # not affected
233+
A.quux.__name__ = None
234+
self.assertIsNone(A.quux.__name__)
235+
236+
with self.assertRaisesRegex(
237+
AttributeError, "'property' object has no attribute '__name__'"
238+
):
239+
property(None, setter).__name__
240+
241+
with self.assertRaisesRegex(
242+
AttributeError, "'property' object has no attribute '__name__'"
243+
):
244+
property(1).__name__
245+
246+
class Err:
247+
def __getattr__(self, attr):
248+
raise RuntimeError('fail')
249+
250+
p = property(Err())
251+
with self.assertRaisesRegex(RuntimeError, 'fail'):
252+
p.__name__
253+
254+
p.__name__ = 'not_fail'
255+
self.assertEqual(p.__name__, 'not_fail')
256+
204257
def test_property_set_name_incorrect_args(self):
205258
p = property()
206259

Lib/test/test_pydoc/test_pydoc.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,17 @@ def test_importfile(self):
11621162
self.assertEqual(loaded_pydoc.__spec__, pydoc.__spec__)
11631163

11641164

1165+
class Rect:
1166+
@property
1167+
def area(self):
1168+
'''Area of the rect'''
1169+
return self.w * self.h
1170+
1171+
1172+
class Square(Rect):
1173+
area = property(lambda self: self.side**2)
1174+
1175+
11651176
class TestDescriptions(unittest.TestCase):
11661177

11671178
def test_module(self):
@@ -1550,13 +1561,13 @@ def test_namedtuple_field_descriptor(self):
15501561

15511562
@requires_docstrings
15521563
def test_property(self):
1553-
class Rect:
1554-
@property
1555-
def area(self):
1556-
'''Area of the rect'''
1557-
return self.w * self.h
1558-
15591564
self.assertEqual(self._get_summary_lines(Rect.area), """\
1565+
area
1566+
Area of the rect
1567+
""")
1568+
# inherits the docstring from Rect.area
1569+
self.assertEqual(self._get_summary_lines(Square.area), """\
1570+
area
15601571
Area of the rect
15611572
""")
15621573
self.assertIn("""
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Expose ``__name__`` attribute on property.

Objects/descrobject.c

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1519,22 +1519,34 @@ class property(object):
15191519
self.__doc__ = doc
15201520
except AttributeError: # read-only or dict-less class
15211521
pass
1522+
self.__name = None
1523+
1524+
def __set_name__(self, owner, name):
1525+
self.__name = name
1526+
1527+
@property
1528+
def __name__(self):
1529+
return self.__name if self.__name is not None else self.fget.__name__
1530+
1531+
@__name__.setter
1532+
def __name__(self, value):
1533+
self.__name = value
15221534
15231535
def __get__(self, inst, type=None):
15241536
if inst is None:
15251537
return self
15261538
if self.__get is None:
1527-
raise AttributeError, "property has no getter"
1539+
raise AttributeError("property has no getter")
15281540
return self.__get(inst)
15291541
15301542
def __set__(self, inst, value):
15311543
if self.__set is None:
1532-
raise AttributeError, "property has no setter"
1544+
raise AttributeError("property has no setter")
15331545
return self.__set(inst, value)
15341546
15351547
def __delete__(self, inst):
15361548
if self.__del is None:
1537-
raise AttributeError, "property has no deleter"
1549+
raise AttributeError("property has no deleter")
15381550
return self.__del(inst)
15391551
15401552
*/
@@ -1628,6 +1640,20 @@ property_dealloc(PyObject *self)
16281640
Py_TYPE(self)->tp_free(self);
16291641
}
16301642

1643+
static int
1644+
property_name(propertyobject *prop, PyObject **name)
1645+
{
1646+
if (prop->prop_name != NULL) {
1647+
*name = Py_NewRef(prop->prop_name);
1648+
return 1;
1649+
}
1650+
if (prop->prop_get == NULL) {
1651+
*name = NULL;
1652+
return 0;
1653+
}
1654+
return PyObject_GetOptionalAttr(prop->prop_get, &_Py_ID(__name__), name);
1655+
}
1656+
16311657
static PyObject *
16321658
property_descr_get(PyObject *self, PyObject *obj, PyObject *type)
16331659
{
@@ -1637,11 +1663,15 @@ property_descr_get(PyObject *self, PyObject *obj, PyObject *type)
16371663

16381664
propertyobject *gs = (propertyobject *)self;
16391665
if (gs->prop_get == NULL) {
1666+
PyObject *propname;
1667+
if (property_name(gs, &propname) < 0) {
1668+
return NULL;
1669+
}
16401670
PyObject *qualname = PyType_GetQualName(Py_TYPE(obj));
1641-
if (gs->prop_name != NULL && qualname != NULL) {
1671+
if (propname != NULL && qualname != NULL) {
16421672
PyErr_Format(PyExc_AttributeError,
16431673
"property %R of %R object has no getter",
1644-
gs->prop_name,
1674+
propname,
16451675
qualname);
16461676
}
16471677
else if (qualname != NULL) {
@@ -1652,6 +1682,7 @@ property_descr_get(PyObject *self, PyObject *obj, PyObject *type)
16521682
PyErr_SetString(PyExc_AttributeError,
16531683
"property has no getter");
16541684
}
1685+
Py_XDECREF(propname);
16551686
Py_XDECREF(qualname);
16561687
return NULL;
16571688
}
@@ -1673,16 +1704,20 @@ property_descr_set(PyObject *self, PyObject *obj, PyObject *value)
16731704
}
16741705

16751706
if (func == NULL) {
1707+
PyObject *propname;
1708+
if (property_name(gs, &propname) < 0) {
1709+
return -1;
1710+
}
16761711
PyObject *qualname = NULL;
16771712
if (obj != NULL) {
16781713
qualname = PyType_GetQualName(Py_TYPE(obj));
16791714
}
1680-
if (gs->prop_name != NULL && qualname != NULL) {
1715+
if (propname != NULL && qualname != NULL) {
16811716
PyErr_Format(PyExc_AttributeError,
16821717
value == NULL ?
16831718
"property %R of %R object has no deleter" :
16841719
"property %R of %R object has no setter",
1685-
gs->prop_name,
1720+
propname,
16861721
qualname);
16871722
}
16881723
else if (qualname != NULL) {
@@ -1698,6 +1733,7 @@ property_descr_set(PyObject *self, PyObject *obj, PyObject *value)
16981733
"property has no deleter" :
16991734
"property has no setter");
17001735
}
1736+
Py_XDECREF(propname);
17011737
Py_XDECREF(qualname);
17021738
return -1;
17031739
}
@@ -1883,6 +1919,28 @@ property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset,
18831919
return 0;
18841920
}
18851921

1922+
static PyObject *
1923+
property_get__name__(propertyobject *prop, void *Py_UNUSED(ignored))
1924+
{
1925+
PyObject *name;
1926+
if (property_name(prop, &name) < 0) {
1927+
return NULL;
1928+
}
1929+
if (name == NULL) {
1930+
PyErr_SetString(PyExc_AttributeError,
1931+
"'property' object has no attribute '__name__'");
1932+
}
1933+
return name;
1934+
}
1935+
1936+
static int
1937+
property_set__name__(propertyobject *prop, PyObject *value,
1938+
void *Py_UNUSED(ignored))
1939+
{
1940+
Py_XSETREF(prop->prop_name, Py_XNewRef(value));
1941+
return 0;
1942+
}
1943+
18861944
static PyObject *
18871945
property_get___isabstractmethod__(propertyobject *prop, void *closure)
18881946
{
@@ -1913,6 +1971,7 @@ property_get___isabstractmethod__(propertyobject *prop, void *closure)
19131971
}
19141972

19151973
static PyGetSetDef property_getsetlist[] = {
1974+
{"__name__", (getter)property_get__name__, (setter)property_set__name__},
19161975
{"__isabstractmethod__",
19171976
(getter)property_get___isabstractmethod__, NULL,
19181977
NULL,

0 commit comments

Comments
 (0)