]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-101860: Expose __name__ on property (GH-101876)
authorEugene Toder <eltoder@users.noreply.github.com>
Tue, 20 Feb 2024 15:14:34 +0000 (10:14 -0500)
committerGitHub <noreply@github.com>
Tue, 20 Feb 2024 15:14:34 +0000 (17:14 +0200)
Useful for introspection and consistent with functions and other
descriptors.

Doc/howto/descriptor.rst
Lib/inspect.py
Lib/pydoc.py
Lib/test/test_inspect/inspect_fodder.py
Lib/test/test_property.py
Lib/test/test_pydoc/test_pydoc.py
Misc/NEWS.d/next/Core and Builtins/2023-02-13-11-36-50.gh-issue-101860.CKCMbC.rst [new file with mode: 0644]
Objects/descrobject.c

index e72386a4da4f8a4e18cac7d4efea37278ddd9125..51f9f4a6556e57637b974ce58d4a2bdeb8863579 100644 (file)
@@ -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)
 
index 450093a8b4c1ee6fc8a0d249008bd7c16748d1ee..da504037ac282c9bff1dd09c0166cb20321bc026 100644 (file)
@@ -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):
index 9bb64feca8f93e847ef50c5dd626ceb187b6dcc6..d32fa8d05044177efec0c39743f1474e365de121 100755 (executable)
@@ -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):
index 60ba7aa78394e87365dd646a4abddd8272acda06..febd54c86fe1d191b96c282eddd3decb992f851c 100644 (file)
@@ -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
index ad5ab5a87b5a663f45b6f468b45b1106c85eefc8..408e64f53142db1d1851b96d3bde760c62987d05 100644 (file)
@@ -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()
 
index d7a333a1103eaca75d7d6873c90891158b4f781f..b07d9119e4940173bebe324291799ba6d08fa52c 100644 (file)
@@ -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 (file)
index 0000000..5a27435
--- /dev/null
@@ -0,0 +1 @@
+Expose ``__name__`` attribute on property.
index c4cd51bdae45ab54a8be216a893b82704045cc82..df546a090c28e4564e0843b3ed877c0a108c7af4 100644 (file)
@@ -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,