]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-41287: Handle `doc` argument of `property.__init__` in subclasses (#23205)
authorSergei Izmailov <sergei.a.izmailov@gmail.com>
Sun, 29 May 2022 03:25:51 +0000 (06:25 +0300)
committerGitHub <noreply@github.com>
Sun, 29 May 2022 03:25:51 +0000 (20:25 -0700)
Fixes #85459

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Lib/test/test_property.py
Misc/NEWS.d/next/Library/2022-04-03-11-25-02.bpo-41287.8CTdwf.rst [new file with mode: 0644]
Objects/descrobject.c

index d91ad1c191275e1ee3d0ad38839345b63727e02b..d07b8632aa8722ef4e8ed1e36d6cad3bb8c495a4 100644 (file)
@@ -219,6 +219,9 @@ class PropertyTests(unittest.TestCase):
 class PropertySub(property):
     """This is a subclass of property"""
 
+class PropertySubWoDoc(property):
+    pass
+
 class PropertySubSlots(property):
     """This is a subclass of property that defines __slots__"""
     __slots__ = ()
@@ -237,6 +240,38 @@ class PropertySubclassTests(unittest.TestCase):
         else:
             raise Exception("AttributeError not raised")
 
+    @unittest.skipIf(sys.flags.optimize >= 2,
+                     "Docstrings are omitted with -O2 and above")
+    def test_issue41287(self):
+
+        self.assertEqual(PropertySub.__doc__, "This is a subclass of property",
+                         "Docstring of `property` subclass is ignored")
+
+        doc = PropertySub(None, None, None, "issue 41287 is fixed").__doc__
+        self.assertEqual(doc, "issue 41287 is fixed",
+                         "Subclasses of `property` ignores `doc` constructor argument")
+
+        def getter(x):
+            """Getter docstring"""
+
+        def getter_wo_doc(x):
+            pass
+
+        for ps in property, PropertySub, PropertySubWoDoc:
+            doc = ps(getter, None, None, "issue 41287 is fixed").__doc__
+            self.assertEqual(doc, "issue 41287 is fixed",
+                             "Getter overrides explicit property docstring (%s)" % ps.__name__)
+
+            doc = ps(getter, None, None, None).__doc__
+            self.assertEqual(doc, "Getter docstring", "Getter docstring is not picked-up (%s)" % ps.__name__)
+
+            doc = ps(getter_wo_doc, None, None, "issue 41287 is fixed").__doc__
+            self.assertEqual(doc, "issue 41287 is fixed",
+                             "Getter overrides explicit property docstring (%s)" % ps.__name__)
+
+            doc = ps(getter_wo_doc, None, None, None).__doc__
+            self.assertIsNone(doc, "Property class doc appears in instance __doc__ (%s)" % ps.__name__)
+
     @unittest.skipIf(sys.flags.optimize >= 2,
                      "Docstrings are omitted with -O2 and above")
     def test_docstring_copy(self):
@@ -249,6 +284,66 @@ class PropertySubclassTests(unittest.TestCase):
             Foo.spam.__doc__,
             "spam wrapped in property subclass")
 
+    @unittest.skipIf(sys.flags.optimize >= 2,
+                     "Docstrings are omitted with -O2 and above")
+    def test_docstring_copy2(self):
+        """
+        Property tries to provide the best docstring it finds for its instances.
+        If a user-provided docstring is available, it is preserved on copies.
+        If no docstring is available during property creation, the property
+        will utilize the docstring from the getter if available.
+        """
+        def getter1(self):
+            return 1
+        def getter2(self):
+            """doc 2"""
+            return 2
+        def getter3(self):
+            """doc 3"""
+            return 3
+
+        # Case-1: user-provided doc is preserved in copies
+        #         of property with undocumented getter
+        p = property(getter1, None, None, "doc-A")
+
+        p2 = p.getter(getter2)
+        self.assertEqual(p.__doc__, "doc-A")
+        self.assertEqual(p2.__doc__, "doc-A")
+
+        # Case-2: user-provided doc is preserved in copies
+        #         of property with documented getter
+        p = property(getter2, None, None, "doc-A")
+
+        p2 = p.getter(getter3)
+        self.assertEqual(p.__doc__, "doc-A")
+        self.assertEqual(p2.__doc__, "doc-A")
+
+        # Case-3: with no user-provided doc new getter doc
+        #         takes precendence
+        p = property(getter2, None, None, None)
+
+        p2 = p.getter(getter3)
+        self.assertEqual(p.__doc__, "doc 2")
+        self.assertEqual(p2.__doc__, "doc 3")
+
+        # Case-4: A user-provided doc is assigned after property construction
+        #         with documented getter. The doc IS NOT preserved.
+        #         It's an odd behaviour, but it's a strange enough
+        #         use case with no easy solution.
+        p = property(getter2, None, None, None)
+        p.__doc__ = "user"
+        p2 = p.getter(getter3)
+        self.assertEqual(p.__doc__, "user")
+        self.assertEqual(p2.__doc__, "doc 3")
+
+        # Case-5: A user-provided doc is assigned after property construction
+        #         with UNdocumented getter. The doc IS preserved.
+        p = property(getter1, None, None, None)
+        p.__doc__ = "user"
+        p2 = p.getter(getter2)
+        self.assertEqual(p.__doc__, "user")
+        self.assertEqual(p2.__doc__, "user")
+
     @unittest.skipIf(sys.flags.optimize >= 2,
                      "Docstrings are omitted with -O2 and above")
     def test_property_setter_copies_getter_docstring(self):
diff --git a/Misc/NEWS.d/next/Library/2022-04-03-11-25-02.bpo-41287.8CTdwf.rst b/Misc/NEWS.d/next/Library/2022-04-03-11-25-02.bpo-41287.8CTdwf.rst
new file mode 100644 (file)
index 0000000..ef80ec6
--- /dev/null
@@ -0,0 +1 @@
+Fix handling of the ``doc`` argument in subclasses of :func:`property`.
index 8e8a46ceca6b3a0baef3bd345266055a9291ee5d..05797e72bcd41ab1cb72685782602280574a9953 100644 (file)
@@ -1782,38 +1782,55 @@ property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset,
     Py_XINCREF(fget);
     Py_XINCREF(fset);
     Py_XINCREF(fdel);
-    Py_XINCREF(doc);
 
     Py_XSETREF(self->prop_get, fget);
     Py_XSETREF(self->prop_set, fset);
     Py_XSETREF(self->prop_del, fdel);
-    Py_XSETREF(self->prop_doc, doc);
+    Py_XSETREF(self->prop_doc, NULL);
     Py_XSETREF(self->prop_name, NULL);
 
     self->getter_doc = 0;
+    PyObject *prop_doc = NULL;
 
+    if (doc != NULL && doc != Py_None) {
+        prop_doc = doc;
+        Py_XINCREF(prop_doc);
+    }
     /* if no docstring given and the getter has one, use that one */
-    if ((doc == NULL || doc == Py_None) && fget != NULL) {
-        PyObject *get_doc;
-        int rc = _PyObject_LookupAttr(fget, &_Py_ID(__doc__), &get_doc);
+    else if (fget != NULL) {
+        int rc = _PyObject_LookupAttr(fget, &_Py_ID(__doc__), &prop_doc);
         if (rc <= 0) {
             return rc;
         }
-        if (Py_IS_TYPE(self, &PyProperty_Type)) {
-            Py_XSETREF(self->prop_doc, get_doc);
+        if (prop_doc == Py_None) {
+            prop_doc = NULL;
+            Py_DECREF(Py_None);
         }
-        else {
-            /* If this is a property subclass, put __doc__
-               in dict of the subclass instance instead,
-               otherwise it gets shadowed by __doc__ in the
-               class's dict. */
-            int err = PyObject_SetAttr(
-                    (PyObject *)self, &_Py_ID(__doc__), get_doc);
-            Py_DECREF(get_doc);
-            if (err < 0)
-                return -1;
+        if (prop_doc != NULL){
+            self->getter_doc = 1;
+        }
+    }
+
+    /* At this point `prop_doc` is either NULL or
+       a non-None object with incremented ref counter */
+
+    if (Py_IS_TYPE(self, &PyProperty_Type)) {
+        Py_XSETREF(self->prop_doc, prop_doc);
+    } else {
+        /* If this is a property subclass, put __doc__
+           in dict of the subclass instance instead,
+           otherwise it gets shadowed by __doc__ in the
+           class's dict. */
+
+        if (prop_doc == NULL) {
+            prop_doc = Py_None;
+            Py_INCREF(prop_doc);
         }
-        self->getter_doc = 1;
+        int err = PyObject_SetAttr(
+                    (PyObject *)self, &_Py_ID(__doc__), prop_doc);
+        Py_XDECREF(prop_doc);
+        if (err < 0)
+            return -1;
     }
 
     return 0;