Skip to content

Commit 66f5add

Browse files
bpo-41287: Handle doc argument of property.__init__ in subclasses (python#23205)
Fixes python#85459 Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
1 parent 877ad7b commit 66f5add

File tree

3 files changed

+131
-18
lines changed

3 files changed

+131
-18
lines changed

Lib/test/test_property.py

+95
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ def test_property_set_name_incorrect_args(self):
219219
class PropertySub(property):
220220
"""This is a subclass of property"""
221221

222+
class PropertySubWoDoc(property):
223+
pass
224+
222225
class PropertySubSlots(property):
223226
"""This is a subclass of property that defines __slots__"""
224227
__slots__ = ()
@@ -237,6 +240,38 @@ def spam(self):
237240
else:
238241
raise Exception("AttributeError not raised")
239242

243+
@unittest.skipIf(sys.flags.optimize >= 2,
244+
"Docstrings are omitted with -O2 and above")
245+
def test_issue41287(self):
246+
247+
self.assertEqual(PropertySub.__doc__, "This is a subclass of property",
248+
"Docstring of `property` subclass is ignored")
249+
250+
doc = PropertySub(None, None, None, "issue 41287 is fixed").__doc__
251+
self.assertEqual(doc, "issue 41287 is fixed",
252+
"Subclasses of `property` ignores `doc` constructor argument")
253+
254+
def getter(x):
255+
"""Getter docstring"""
256+
257+
def getter_wo_doc(x):
258+
pass
259+
260+
for ps in property, PropertySub, PropertySubWoDoc:
261+
doc = ps(getter, None, None, "issue 41287 is fixed").__doc__
262+
self.assertEqual(doc, "issue 41287 is fixed",
263+
"Getter overrides explicit property docstring (%s)" % ps.__name__)
264+
265+
doc = ps(getter, None, None, None).__doc__
266+
self.assertEqual(doc, "Getter docstring", "Getter docstring is not picked-up (%s)" % ps.__name__)
267+
268+
doc = ps(getter_wo_doc, None, None, "issue 41287 is fixed").__doc__
269+
self.assertEqual(doc, "issue 41287 is fixed",
270+
"Getter overrides explicit property docstring (%s)" % ps.__name__)
271+
272+
doc = ps(getter_wo_doc, None, None, None).__doc__
273+
self.assertIsNone(doc, "Property class doc appears in instance __doc__ (%s)" % ps.__name__)
274+
240275
@unittest.skipIf(sys.flags.optimize >= 2,
241276
"Docstrings are omitted with -O2 and above")
242277
def test_docstring_copy(self):
@@ -249,6 +284,66 @@ def spam(self):
249284
Foo.spam.__doc__,
250285
"spam wrapped in property subclass")
251286

287+
@unittest.skipIf(sys.flags.optimize >= 2,
288+
"Docstrings are omitted with -O2 and above")
289+
def test_docstring_copy2(self):
290+
"""
291+
Property tries to provide the best docstring it finds for its instances.
292+
If a user-provided docstring is available, it is preserved on copies.
293+
If no docstring is available during property creation, the property
294+
will utilize the docstring from the getter if available.
295+
"""
296+
def getter1(self):
297+
return 1
298+
def getter2(self):
299+
"""doc 2"""
300+
return 2
301+
def getter3(self):
302+
"""doc 3"""
303+
return 3
304+
305+
# Case-1: user-provided doc is preserved in copies
306+
# of property with undocumented getter
307+
p = property(getter1, None, None, "doc-A")
308+
309+
p2 = p.getter(getter2)
310+
self.assertEqual(p.__doc__, "doc-A")
311+
self.assertEqual(p2.__doc__, "doc-A")
312+
313+
# Case-2: user-provided doc is preserved in copies
314+
# of property with documented getter
315+
p = property(getter2, None, None, "doc-A")
316+
317+
p2 = p.getter(getter3)
318+
self.assertEqual(p.__doc__, "doc-A")
319+
self.assertEqual(p2.__doc__, "doc-A")
320+
321+
# Case-3: with no user-provided doc new getter doc
322+
# takes precendence
323+
p = property(getter2, None, None, None)
324+
325+
p2 = p.getter(getter3)
326+
self.assertEqual(p.__doc__, "doc 2")
327+
self.assertEqual(p2.__doc__, "doc 3")
328+
329+
# Case-4: A user-provided doc is assigned after property construction
330+
# with documented getter. The doc IS NOT preserved.
331+
# It's an odd behaviour, but it's a strange enough
332+
# use case with no easy solution.
333+
p = property(getter2, None, None, None)
334+
p.__doc__ = "user"
335+
p2 = p.getter(getter3)
336+
self.assertEqual(p.__doc__, "user")
337+
self.assertEqual(p2.__doc__, "doc 3")
338+
339+
# Case-5: A user-provided doc is assigned after property construction
340+
# with UNdocumented getter. The doc IS preserved.
341+
p = property(getter1, None, None, None)
342+
p.__doc__ = "user"
343+
p2 = p.getter(getter2)
344+
self.assertEqual(p.__doc__, "user")
345+
self.assertEqual(p2.__doc__, "user")
346+
252347
@unittest.skipIf(sys.flags.optimize >= 2,
253348
"Docstrings are omitted with -O2 and above")
254349
def test_property_setter_copies_getter_docstring(self):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix handling of the ``doc`` argument in subclasses of :func:`property`.

Objects/descrobject.c

+35-18
Original file line numberDiff line numberDiff line change
@@ -1782,38 +1782,55 @@ property_init_impl(propertyobject *self, PyObject *fget, PyObject *fset,
17821782
Py_XINCREF(fget);
17831783
Py_XINCREF(fset);
17841784
Py_XINCREF(fdel);
1785-
Py_XINCREF(doc);
17861785

17871786
Py_XSETREF(self->prop_get, fget);
17881787
Py_XSETREF(self->prop_set, fset);
17891788
Py_XSETREF(self->prop_del, fdel);
1790-
Py_XSETREF(self->prop_doc, doc);
1789+
Py_XSETREF(self->prop_doc, NULL);
17911790
Py_XSETREF(self->prop_name, NULL);
17921791

17931792
self->getter_doc = 0;
1793+
PyObject *prop_doc = NULL;
17941794

1795+
if (doc != NULL && doc != Py_None) {
1796+
prop_doc = doc;
1797+
Py_XINCREF(prop_doc);
1798+
}
17951799
/* if no docstring given and the getter has one, use that one */
1796-
if ((doc == NULL || doc == Py_None) && fget != NULL) {
1797-
PyObject *get_doc;
1798-
int rc = _PyObject_LookupAttr(fget, &_Py_ID(__doc__), &get_doc);
1800+
else if (fget != NULL) {
1801+
int rc = _PyObject_LookupAttr(fget, &_Py_ID(__doc__), &prop_doc);
17991802
if (rc <= 0) {
18001803
return rc;
18011804
}
1802-
if (Py_IS_TYPE(self, &PyProperty_Type)) {
1803-
Py_XSETREF(self->prop_doc, get_doc);
1805+
if (prop_doc == Py_None) {
1806+
prop_doc = NULL;
1807+
Py_DECREF(Py_None);
18041808
}
1805-
else {
1806-
/* If this is a property subclass, put __doc__
1807-
in dict of the subclass instance instead,
1808-
otherwise it gets shadowed by __doc__ in the
1809-
class's dict. */
1810-
int err = PyObject_SetAttr(
1811-
(PyObject *)self, &_Py_ID(__doc__), get_doc);
1812-
Py_DECREF(get_doc);
1813-
if (err < 0)
1814-
return -1;
1809+
if (prop_doc != NULL){
1810+
self->getter_doc = 1;
1811+
}
1812+
}
1813+
1814+
/* At this point `prop_doc` is either NULL or
1815+
a non-None object with incremented ref counter */
1816+
1817+
if (Py_IS_TYPE(self, &PyProperty_Type)) {
1818+
Py_XSETREF(self->prop_doc, prop_doc);
1819+
} else {
1820+
/* If this is a property subclass, put __doc__
1821+
in dict of the subclass instance instead,
1822+
otherwise it gets shadowed by __doc__ in the
1823+
class's dict. */
1824+
1825+
if (prop_doc == NULL) {
1826+
prop_doc = Py_None;
1827+
Py_INCREF(prop_doc);
18151828
}
1816-
self->getter_doc = 1;
1829+
int err = PyObject_SetAttr(
1830+
(PyObject *)self, &_Py_ID(__doc__), prop_doc);
1831+
Py_XDECREF(prop_doc);
1832+
if (err < 0)
1833+
return -1;
18171834
}
18181835

18191836
return 0;

0 commit comments

Comments
 (0)