https://github.com/python/cpython/commit/c0b0c2f2015fb27db4306109b2b3781eb2057c2b
commit: c0b0c2f2015fb27db4306109b2b3781eb2057c2b
branch: main
author: Eugene Toder <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2024-02-20T17:14:34+02:00
summary:

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

Useful for introspection and consistent with functions and other
descriptors.

files:
A Misc/NEWS.d/next/Core and 
Builtins/2023-02-13-11-36-50.gh-issue-101860.CKCMbC.rst
M Doc/howto/descriptor.rst
M Lib/inspect.py
M Lib/pydoc.py
M Lib/test/test_inspect/inspect_fodder.py
M Lib/test/test_property.py
M Lib/test/test_pydoc/test_pydoc.py
M Objects/descrobject.c

diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst
index e72386a4da4f8a..51f9f4a6556e57 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 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 ad5ab5a87b5a66..408e64f53142db 100644
--- a/Lib/test/test_property.py
+++ b/Lib/test/test_property.py
@@ -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()
 
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 c4cd51bdae45ab..df546a090c28e4 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,

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to