Skip to content

gh-101860: Expose __name__ on property #101876

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

Merged
merged 4 commits into from
Feb 20, 2024
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
19 changes: 15 additions & 4 deletions Doc/howto/descriptor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1004,31 +1004,42 @@ here is a pure Python equivalent:
if doc is None and fget is not None:
doc = fget.__doc__
self.__doc__ = doc
self._name = ''
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, obj, objtype=None):
if obj is None:
return self
if self.fget is None:
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)

def __set__(self, obj, value):
if self.fset is None:
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)

def __delete__(self, obj):
if self.fdel is None:
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)

Expand Down
5 changes: 2 additions & 3 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,9 +834,8 @@ def _finddoc(obj):
cls = self.__class__
# Should be tested before isdatadescriptor().
elif isinstance(obj, property):
func = obj.fget
name = func.__name__
cls = _findclass(func)
name = obj.__name__
cls = _findclass(obj.fget)
if cls is None or getattr(cls, name) is not obj:
return None
elif ismethoddescriptor(obj) or isdatadescriptor(obj):
Expand Down
5 changes: 2 additions & 3 deletions Lib/pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,8 @@ def _finddoc(obj):
cls = self.__class__
# Should be tested before isdatadescriptor().
elif isinstance(obj, property):
func = obj.fget
name = func.__name__
cls = _findclass(func)
name = obj.__name__
cls = _findclass(obj.fget)
if cls is None or getattr(cls, name) is not obj:
return None
elif inspect.ismethoddescriptor(obj) or inspect.isdatadescriptor(obj):
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_inspect/inspect_fodder.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ class FesteringGob(MalodorousPervert, ParrotDroppings):
def abuse(self, a, b, c):
pass

@property
def contradiction(self):
def _getter(self):
pass
contradiction = property(_getter)

async def lobbest(grenade):
pass
Expand Down
53 changes: 53 additions & 0 deletions Lib/test/test_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,59 @@ def test_gh_115618(self):
self.assertIsNone(prop.fdel)
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):
p = property()

Expand Down
23 changes: 17 additions & 6 deletions Lib/test/test_pydoc/test_pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,17 @@ def test_importfile(self):
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):

def test_module(self):
Expand Down Expand Up @@ -1550,13 +1561,13 @@ def test_namedtuple_field_descriptor(self):

@requires_docstrings
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), """\
area
Area of the rect
""")
# inherits the docstring from Rect.area
self.assertEqual(self._get_summary_lines(Square.area), """\
area
Area of the rect
""")
self.assertIn("""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Expose ``__name__`` attribute on property.
73 changes: 66 additions & 7 deletions Objects/descrobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1519,22 +1519,34 @@ class property(object):
self.__doc__ = doc
except AttributeError: # read-only or dict-less class
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):
if inst is None:
return self
if self.__get is None:
raise AttributeError, "property has no getter"
raise AttributeError("property has no getter")
return self.__get(inst)

def __set__(self, inst, value):
if self.__set is None:
raise AttributeError, "property has no setter"
raise AttributeError("property has no setter")
return self.__set(inst, value)

def __delete__(self, inst):
if self.__del is None:
raise AttributeError, "property has no deleter"
raise AttributeError("property has no deleter")
return self.__del(inst)

*/
Expand Down Expand Up @@ -1628,6 +1640,20 @@ property_dealloc(PyObject *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 *
property_descr_get(PyObject *self, PyObject *obj, PyObject *type)
{
Expand All @@ -1637,11 +1663,15 @@ property_descr_get(PyObject *self, PyObject *obj, PyObject *type)

propertyobject *gs = (propertyobject *)self;
if (gs->prop_get == NULL) {
PyObject *propname;
if (property_name(gs, &propname) < 0) {
return NULL;
}
PyObject *qualname = PyType_GetQualName(Py_TYPE(obj));
if (gs->prop_name != NULL && qualname != NULL) {
if (propname != NULL && qualname != NULL) {
PyErr_Format(PyExc_AttributeError,
"property %R of %R object has no getter",
gs->prop_name,
propname,
qualname);
}
else if (qualname != NULL) {
Expand All @@ -1652,6 +1682,7 @@ property_descr_get(PyObject *self, PyObject *obj, PyObject *type)
PyErr_SetString(PyExc_AttributeError,
"property has no getter");
}
Py_XDECREF(propname);
Py_XDECREF(qualname);
return NULL;
}
Expand All @@ -1673,16 +1704,20 @@ property_descr_set(PyObject *self, PyObject *obj, PyObject *value)
}

if (func == NULL) {
PyObject *propname;
if (property_name(gs, &propname) < 0) {
return -1;
}
PyObject *qualname = NULL;
if (obj != NULL) {
qualname = PyType_GetQualName(Py_TYPE(obj));
}
if (gs->prop_name != NULL && qualname != NULL) {
if (propname != NULL && qualname != NULL) {
PyErr_Format(PyExc_AttributeError,
value == NULL ?
"property %R of %R object has no deleter" :
"property %R of %R object has no setter",
gs->prop_name,
propname,
qualname);
}
else if (qualname != NULL) {
Expand All @@ -1698,6 +1733,7 @@ property_descr_set(PyObject *self, PyObject *obj, PyObject *value)
"property has no deleter" :
"property has no setter");
}
Py_XDECREF(propname);
Py_XDECREF(qualname);
return -1;
}
Expand Down Expand Up @@ -1883,6 +1919,28 @@ property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset,
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 *
property_get___isabstractmethod__(propertyobject *prop, void *closure)
{
Expand Down Expand Up @@ -1913,6 +1971,7 @@ property_get___isabstractmethod__(propertyobject *prop, void *closure)
}

static PyGetSetDef property_getsetlist[] = {
{"__name__", (getter)property_get__name__, (setter)property_set__name__},
{"__isabstractmethod__",
(getter)property_get___isabstractmethod__, NULL,
NULL,
Expand Down