From 8ec12516f547b18be613e2d849975fca924603ea Mon Sep 17 00:00:00 2001 From: Eugene Toder Date: Mon, 13 Feb 2023 11:21:38 -0500 Subject: [PATCH 1/3] gh-101860: Expose __name__ on property 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 | 20 ++++++++++++++++ Lib/test/test_pydoc/test_pydoc.py | 23 ++++++++++++++----- ...-02-13-11-36-50.gh-issue-101860.CKCMbC.rst | 1 + Objects/descrobject.c | 5 ++++ 8 files changed, 60 insertions(+), 22 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 75346f2c7618c2..ed5d61acfb19a8 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1004,47 +1004,50 @@ 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 + self.__name__ = name 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) def getter(self, fget): prop = type(self)(fget, self.fset, self.fdel, self.__doc__) - prop._name = self._name + prop.__name__ = self.__name__ return prop def setter(self, fset): prop = type(self)(self.fget, fset, self.fdel, self.__doc__) - prop._name = self._name + prop.__name__ = self.__name__ return prop def deleter(self, fdel): prop = type(self)(self.fget, self.fset, fdel, self.__doc__) - prop._name = self._name + prop.__name__ = self.__name__ return prop .. testcode:: diff --git a/Lib/inspect.py b/Lib/inspect.py index 450093a8b4c1ee..da504037ac282c 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 9bb64feca8f93e..d32fa8d0504417 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 60ba7aa78394e8..febd54c86fe1d1 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 8ace9fd17ab96e..f537d5dfa628b1 100644 --- a/Lib/test/test_property.py +++ b/Lib/test/test_property.py @@ -183,6 +183,26 @@ def test_refleaks_in___init__(self): fake_prop.__init__('fget', 'fset', 'fdel', 'doc') self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10) + def test_property_name(self): + def getter(self): + return 42 + + class A: + @property + def foo(self): + return 1 + + bar = property(getter) + + self.assertEqual(A.foo.__name__, 'foo') + self.assertEqual(A.bar.__name__, 'bar') + + A.baz = property(getter) + self.assertIsNone(A.baz.__name__) + A.baz.__name__ = 'mybaz' + self.assertEqual(A.baz.__name__, 'mybaz') + self.assertEqual(A.bar.__name__, 'bar') # not affected + 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 d7a333a1103eac..b07d9119e49401 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -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): @@ -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(""" 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 00000000000000..5a274353466973 --- /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 805de2971ba475..a80fe6c2023406 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1519,6 +1519,10 @@ 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 def __get__(self, inst, type=None): if inst is None: @@ -1547,6 +1551,7 @@ static PyMemberDef property_members[] = { {"fset", _Py_T_OBJECT, offsetof(propertyobject, prop_set), Py_READONLY}, {"fdel", _Py_T_OBJECT, offsetof(propertyobject, prop_del), Py_READONLY}, {"__doc__", _Py_T_OBJECT, offsetof(propertyobject, prop_doc), 0}, + {"__name__", _Py_T_OBJECT, offsetof(propertyobject, prop_name), 0}, {0} }; From e5144729ba867d46fa83472d5b50bb0bd6eb2a73 Mon Sep 17 00:00:00 2001 From: Eugene Toder Date: Sat, 17 Feb 2024 11:58:25 -0500 Subject: [PATCH 2/3] Fallback to `func.__name__` if property `__name__` not set. --- Lib/inspect.py | 5 +++-- Lib/pydoc.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index da504037ac282c..9b9585493e9b21 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -834,8 +834,9 @@ def _finddoc(obj): cls = self.__class__ # Should be tested before isdatadescriptor(). elif isinstance(obj, property): - name = obj.__name__ - cls = _findclass(obj.fget) + func = obj.fget + name = obj.__name__ or func.__name__ + cls = _findclass(func) 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 d32fa8d0504417..036c7a5c921356 100755 --- a/Lib/pydoc.py +++ b/Lib/pydoc.py @@ -127,8 +127,9 @@ def _finddoc(obj): cls = self.__class__ # Should be tested before isdatadescriptor(). elif isinstance(obj, property): - name = obj.__name__ - cls = _findclass(obj.fget) + func = obj.fget + name = obj.__name__ or func.__name__ + cls = _findclass(func) if cls is None or getattr(cls, name) is not obj: return None elif inspect.ismethoddescriptor(obj) or inspect.isdatadescriptor(obj): From c20e9a78fadfa8c58dda10adb9a07e627239e371 Mon Sep 17 00:00:00 2001 From: Eugene Toder Date: Mon, 19 Feb 2024 19:20:26 -0500 Subject: [PATCH 3/3] Fallback to `fget.__name__` if name is not set Raise AttributeError if no fget or it doesn't have `__name__`. --- Doc/howto/descriptor.rst | 18 +++++++--- Lib/inspect.py | 5 ++- Lib/pydoc.py | 5 ++- Lib/test/test_property.py | 41 +++++++++++++++++++--- Objects/descrobject.c | 74 +++++++++++++++++++++++++++++++++------ 5 files changed, 118 insertions(+), 25 deletions(-) diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index ed5d61acfb19a8..29630aaed8a66a 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -1004,10 +1004,18 @@ here is a pure Python equivalent: if doc is None and fget is not None: doc = fget.__doc__ self.__doc__ = doc - self.__name__ = None + self._name = None 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): if obj is None: @@ -1037,17 +1045,17 @@ here is a pure Python equivalent: def getter(self, fget): prop = type(self)(fget, self.fset, self.fdel, self.__doc__) - prop.__name__ = self.__name__ + prop._name = self._name return prop def setter(self, fset): prop = type(self)(self.fget, fset, self.fdel, self.__doc__) - prop.__name__ = self.__name__ + prop._name = self._name return prop def deleter(self, fdel): prop = type(self)(self.fget, self.fset, fdel, self.__doc__) - prop.__name__ = self.__name__ + prop._name = self._name return prop .. testcode:: diff --git a/Lib/inspect.py b/Lib/inspect.py index 9b9585493e9b21..da504037ac282c 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 = obj.__name__ or 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 036c7a5c921356..d32fa8d0504417 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 = obj.__name__ or 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_property.py b/Lib/test/test_property.py index ff5dfb496557ea..408e64f53142db 100644 --- a/Lib/test/test_property.py +++ b/Lib/test/test_property.py @@ -205,21 +205,54 @@ 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.baz = property(getter) - self.assertIsNone(A.baz.__name__) - A.baz.__name__ = 'mybaz' - self.assertEqual(A.baz.__name__, 'mybaz') + 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/Objects/descrobject.c b/Objects/descrobject.c index e037cd7d418d7b..df546a090c28e4 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1519,26 +1519,34 @@ class property(object): self.__doc__ = doc except AttributeError: # read-only or dict-less class pass - self.__name__ = None + self.__name = None 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, 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) */ @@ -1551,7 +1559,6 @@ static PyMemberDef property_members[] = { {"fset", _Py_T_OBJECT, offsetof(propertyobject, prop_set), Py_READONLY}, {"fdel", _Py_T_OBJECT, offsetof(propertyobject, prop_del), Py_READONLY}, {"__doc__", _Py_T_OBJECT, offsetof(propertyobject, prop_doc), 0}, - {"__name__", _Py_T_OBJECT, offsetof(propertyobject, prop_name), 0}, {0} }; @@ -1633,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) { @@ -1642,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) { @@ -1657,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; } @@ -1678,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) { @@ -1703,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; } @@ -1888,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) { @@ -1918,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,