diff options
author | Sergei Izmailov <sergei.a.izmailov@gmail.com> | 2022-05-29 03:25:51 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-05-29 03:25:51 (GMT) |
commit | 66f5add1f0ac801cba5ece36e6cfd68985d82029 (patch) | |
tree | 19d220f21e859f9529c364c3dcc118c97489c4b4 | |
parent | 877ad7b3b2778a305d3989d58ebd68cb01baf26e (diff) | |
download | cpython-66f5add1f0ac801cba5ece36e6cfd68985d82029.zip cpython-66f5add1f0ac801cba5ece36e6cfd68985d82029.tar.gz cpython-66f5add1f0ac801cba5ece36e6cfd68985d82029.tar.bz2 |
bpo-41287: Handle `doc` argument of `property.__init__` in subclasses (#23205)
Fixes #85459
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
-rw-r--r-- | Lib/test/test_property.py | 95 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2022-04-03-11-25-02.bpo-41287.8CTdwf.rst | 1 | ||||
-rw-r--r-- | Objects/descrobject.c | 53 |
3 files changed, 131 insertions, 18 deletions
diff --git a/Lib/test/test_property.py b/Lib/test/test_property.py index d91ad1c..d07b863 100644 --- a/Lib/test/test_property.py +++ b/Lib/test/test_property.py @@ -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__ = () @@ -239,6 +242,38 @@ class PropertySubclassTests(unittest.TestCase): @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): class Foo(object): @PropertySub @@ -251,6 +286,66 @@ class PropertySubclassTests(unittest.TestCase): @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): class Foo(object): def __init__(self): self._spam = 1 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 index 0000000..ef80ec6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-04-03-11-25-02.bpo-41287.8CTdwf.rst @@ -0,0 +1 @@ +Fix handling of the ``doc`` argument in subclasses of :func:`property`. diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 8e8a46c..05797e7 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -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; |