From c0b0c2f2015fb27db4306109b2b3781eb2057c2b Mon Sep 17 00:00:00 2001 From: Eugene Toder Date: Tue, 20 Feb 2024 10:14:34 -0500 Subject: [PATCH] gh-101860: Expose __name__ on property (GH-101876) Useful for introspection and consistent with functions and other descriptors. --- Doc/howto/descriptor.rst | 19 ++++- Lib/inspect.py | 5 +- Lib/pydoc.py | 5 +- Lib/test/test_inspect/inspect_fodder.py | 4 +- Lib/test/test_property.py | 53 ++++++++++++++ Lib/test/test_pydoc/test_pydoc.py | 23 ++++-- ...-02-13-11-36-50.gh-issue-101860.CKCMbC.rst | 1 + Objects/descrobject.c | 73 +++++++++++++++++-- 8 files changed, 158 insertions(+), 25 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2023-02-13-11-36-50.gh-issue-101860.CKCMbC.rst diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index e72386a4da4..51f9f4a6556 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -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) diff --git a/Lib/inspect.py b/Lib/inspect.py index 450093a8b4c..da504037ac2 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -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): diff --git a/Lib/pydoc.py b/Lib/pydoc.py index 9bb64feca8f..d32fa8d0504 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -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): diff --git a/Lib/test/test_inspect/inspect_fodder.py b/Lib/test/test_inspect/inspect_fodder.py index 60ba7aa7839..febd54c86fe 100644 --- a/Lib/test/test_inspect/inspect_fodder.py +++ b/Lib/test/test_inspect/inspect_fodder.py @@ -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 diff --git a/Lib/test/test_property.py b/Lib/test/test_property.py index ad5ab5a87b5..408e64f5314 100644 --- a/Lib/test/test_property.py +++ b/Lib/test/test_property.py @@ -201,6 +201,59 @@ class PropertyTests(unittest.TestCase): 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() diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index d7a333a1103..b07d9119e49 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -1162,6 +1162,17 @@ class PydocImportTest(PydocBaseTest): 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): @@ -1550,13 +1561,13 @@ cm(x) class method of test.test_pydoc.test_pydoc.X @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(""" diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-02-13-11-36-50.gh-issue-101860.CKCMbC.rst b/Misc/NEWS.d/next/Core and Builtins/2023-02-13-11-36-50.gh-issue-101860.CKCMbC.rst new file mode 100644 index 00000000000..5a274353466 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-02-13-11-36-50.gh-issue-101860.CKCMbC.rst @@ -0,0 +1 @@ +Expose ``__name__`` attribute on property. diff --git a/Objects/descrobject.c b/Objects/descrobject.c index c4cd51bdae4..df546a090c2 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -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) */ @@ -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) { @@ -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) { @@ -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; } @@ -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) { @@ -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; } @@ -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) { @@ -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,