diff --git a/Lib/test/test_property.py b/Lib/test/test_property.py --- a/Lib/test/test_property.py +++ b/Lib/test/test_property.py @@ -204,16 +204,38 @@ class PropertySubclassTests(unittest.Tes """spam wrapped in property subclass""" return 1 self.assertEqual( Foo.spam.__doc__, "spam wrapped in property subclass") @unittest.skipIf(sys.flags.optimize >= 2, "Docstrings are omitted with -O2 and above") + def test_prefer_explicit_doc(self): + # Issue 25757: subclasses of property lose docstring + self.assertEqual(property(doc="explicit doc").__doc__, "explicit doc") + self.assertEqual(PropertySub(doc="explicit doc").__doc__, "explicit doc") + + class Foo: + spam = PropertySub(doc="spam explicit doc") + + @spam.getter + def spam(self): + """ignored as doc already set""" + return 1 + + def _stuff_getter(self): + """ignored as doc set directly""" + stuff = PropertySub(doc="stuff doc argument", fget=_stuff_getter) + + self.assertEqual(Foo.spam.__doc__, "spam explicit doc") + self.assertEqual(Foo.stuff.__doc__, "stuff doc argument") + + @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 @PropertySub def spam(self): """spam wrapped in property subclass""" return self._spam @spam.setter diff --git a/Objects/descrobject.c b/Objects/descrobject.c --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1472,16 +1472,17 @@ property_copy(PyObject *old, PyObject *g if (new == NULL) return NULL; return new; } static int property_init(PyObject *self, PyObject *args, PyObject *kwds) { + _Py_IDENTIFIER(__doc__); PyObject *get = NULL, *set = NULL, *del = NULL, *doc = NULL; static char *kwlist[] = {"fget", "fset", "fdel", "doc", 0}; propertyobject *prop = (propertyobject *)self; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO:property", kwlist, &get, &set, &del, &doc)) return -1; @@ -1500,42 +1501,39 @@ property_init(PyObject *self, PyObject * prop->prop_get = get; prop->prop_set = set; prop->prop_del = del; prop->prop_doc = doc; prop->getter_doc = 0; /* if no docstring given and the getter has one, use that one */ if ((doc == NULL || doc == Py_None) && get != NULL) { - _Py_IDENTIFIER(__doc__); PyObject *get_doc = _PyObject_GetAttrId(get, &PyId___doc__); if (get_doc) { - if (Py_TYPE(self) == &PyProperty_Type) { - Py_XSETREF(prop->prop_doc, get_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. */ - int err = _PyObject_SetAttrId(self, &PyId___doc__, get_doc); - Py_DECREF(get_doc); - if (err < 0) - return -1; - } + Py_XSETREF(prop->prop_doc, get_doc); prop->getter_doc = 1; } else if (PyErr_ExceptionMatches(PyExc_Exception)) { PyErr_Clear(); } else { + Py_DECREF(get_doc); return -1; } } + /* If this is a property subclass, put __doc__ in dict of the subclass + instance as well, otherwise it gets shadowed by __doc__ in the + class's dict. */ + if (Py_TYPE(self) != &PyProperty_Type) { + int err = _PyObject_SetAttrId(self, &PyId___doc__, prop->prop_doc); + if (err < 0) + return -1; + } + return 0; } static PyObject * property_get___isabstractmethod__(propertyobject *prop, void *closure) { int res = _PyObject_IsAbstract(prop->prop_get); if (res == -1) {