diff --git a/Lib/test/test_property.py b/Lib/test/test_property.py index 4b6e20caa39..7a8003f62de 100644 --- a/Lib/test/test_property.py +++ b/Lib/test/test_property.py @@ -60,6 +60,22 @@ class PropertyDocSub(PropertyDocBase): """The decorator does not use this doc string""" return self._spam +class PropertySubNewGetter(BaseClass): + @BaseClass.spam.getter + def spam(self): + """new docstring""" + return 5 + +class PropertyNewGetter(object): + @property + def spam(self): + """original docstring""" + return 1 + @spam.getter + def spam(self): + """new docstring""" + return 8 + class PropertyTests(unittest.TestCase): def test_property_decorator_baseclass(self): # see #1620 @@ -91,8 +107,106 @@ class PropertyTests(unittest.TestCase): self.assertEqual(base.__class__.spam.__doc__, "spam spam spam") self.assertEqual(sub.__class__.spam.__doc__, "spam spam spam") + def test_property_getter_doc_override(self): + newgettersub = PropertySubNewGetter() + self.assertEqual(newgettersub.spam, 5) + self.assertEqual(newgettersub.__class__.spam.__doc__, "new docstring") + newgetter = PropertyNewGetter() + self.assertEqual(newgetter.spam, 8) + self.assertEqual(newgetter.__class__.spam.__doc__, "new docstring") + + +# Issue 5890: subclasses of property do not preserve method __doc__ strings +class PropertySub(property): + """This is a subclass of property""" + +class PropertySubSlots(property): + """This is a subclass of property that defines __slots__""" + __slots__ = () + +class PropertySubclassTests(unittest.TestCase): + + def test_docstring_copy(self): + class Foo(object): + @PropertySub + def spam(self): + """spam wrapped in property subclass""" + return 1 + self.assertEqual( + Foo.spam.__doc__, + "spam wrapped in property subclass") + + def test_slots_docstring_copy_exception(self): + try: + class Foo(object): + @PropertySubSlots + def spam(self): + """Trying to copy this docstring will raise an exception""" + return 1 + except AttributeError: + pass + else: + raise Exception("AttributeError not raised") + + 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 + def spam(self, value): + """this docstring is ignored""" + self._spam = value + foo = Foo() + self.assertEqual(foo.spam, 1) + foo.spam = 2 + self.assertEqual(foo.spam, 2) + self.assertEqual( + Foo.spam.__doc__, + "spam wrapped in property subclass") + class FooSub(Foo): + @Foo.spam.setter + def spam(self, value): + """another ignored docstring""" + self._spam = 'eggs' + foosub = FooSub() + self.assertEqual(foosub.spam, 1) + foosub.spam = 7 + self.assertEqual(foosub.spam, 'eggs') + self.assertEqual( + FooSub.spam.__doc__, + "spam wrapped in property subclass") + + def test_property_new_getter_new_docstring(self): + + class Foo(object): + @PropertySub + def spam(self): + """a docstring""" + return 1 + @spam.getter + def spam(self): + """a new docstring""" + return 2 + self.assertEqual(Foo.spam.__doc__, "a new docstring") + class FooBase(object): + @PropertySub + def spam(self): + """a docstring""" + return 1 + class Foo2(FooBase): + @FooBase.spam.getter + def spam(self): + """a new docstring""" + return 2 + self.assertEqual(Foo.spam.__doc__, "a new docstring") + + + def test_main(): - run_unittest(PropertyTests) + run_unittest(PropertyTests, PropertySubclassTests) if __name__ == '__main__': test_main() diff --git a/Misc/NEWS b/Misc/NEWS index c8ae9184050..268a9bb139a 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -12,6 +12,10 @@ What's New in Python 2.7 alpha 1 Core and Builtins ----------------- +- Issue #5890: in subclasses of 'property' the __doc__ attribute was + shadowed by classtype's, even if it was None. property now + inserts the __doc__ into the subclass instance __dict__. + - Issue #4426: The UTF-7 decoder was too strict and didn't accept some legal sequences. Patch by Nick Barnes and Victor Stinner. diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 536e5a84253..f6f5976d5f7 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -1233,25 +1233,19 @@ property_copy(PyObject *old, PyObject *get, PyObject *set, PyObject *del, } if (doc == NULL || doc == Py_None) { Py_XDECREF(doc); - doc = pold->prop_doc ? pold->prop_doc : Py_None; + if (pold->getter_doc && get != Py_None) { + /* make _init use __doc__ from getter */ + doc = Py_None; + } + else { + doc = pold->prop_doc ? pold->prop_doc : Py_None; + } } - + new = PyObject_CallFunction(type, "OOOO", get, set, del, doc); Py_DECREF(type); if (new == NULL) return NULL; - pnew = (propertyobject *)new; - - if (pold->getter_doc && get != Py_None) { - PyObject *get_doc = PyObject_GetAttrString(get, "__doc__"); - if (get_doc != NULL) { - Py_XDECREF(pnew->prop_doc); - pnew->prop_doc = get_doc; /* get_doc already INCREF'd by GetAttr */ - pnew->getter_doc = 1; - } else { - PyErr_Clear(); - } - } return new; } @@ -1288,8 +1282,21 @@ property_init(PyObject *self, PyObject *args, PyObject *kwds) if ((doc == NULL || doc == Py_None) && get != NULL) { PyObject *get_doc = PyObject_GetAttrString(get, "__doc__"); if (get_doc != NULL) { - Py_XDECREF(prop->prop_doc); - prop->prop_doc = get_doc; /* get_doc already INCREF'd by GetAttr */ + /* get_doc already INCREF'd by GetAttr */ + if (Py_TYPE(self)==&PyProperty_Type) { + Py_XDECREF(prop->prop_doc); + prop->prop_doc = get_doc; + } else { + /* Put __doc__ in dict of the subclass instance instead, + otherwise it gets shadowed by class's __doc__. */ + if (PyObject_SetAttrString(self, "__doc__", get_doc) != 0) + { + /* DECREF for props handled by _dealloc */ + Py_DECREF(get_doc); + return -1; + } + Py_DECREF(get_doc); + } prop->getter_doc = 1; } else { PyErr_Clear();