Skip to content

Commit 5cf2b72

Browse files
Issue #15582: inspect.getdoc() now follows inheritance chains.
1 parent 41525e3 commit 5cf2b72

File tree

5 files changed

+112
-3
lines changed

5 files changed

+112
-3
lines changed

Doc/library/inspect.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,9 @@ Retrieving source code
356356
.. function:: getdoc(object)
357357

358358
Get the documentation string for an object, cleaned up with :func:`cleandoc`.
359+
If the documentation string for an object is not provided and the object is
360+
a class, a method, a property or a descriptor, retrieve the documentation
361+
string from the inheritance hierarchy.
359362

360363

361364
.. function:: getcomments(object)

Lib/inspect.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,74 @@ def indentsize(line):
468468
expline = line.expandtabs()
469469
return len(expline) - len(expline.lstrip())
470470

471+
def _findclass(func):
472+
cls = sys.modules.get(func.__module__)
473+
if cls is None:
474+
return None
475+
for name in func.__qualname__.split('.')[:-1]:
476+
cls = getattr(cls, name)
477+
if not isclass(cls):
478+
return None
479+
return cls
480+
481+
def _finddoc(obj):
482+
if isclass(obj):
483+
for base in obj.__mro__:
484+
if base is not object:
485+
try:
486+
doc = base.__doc__
487+
except AttributeError:
488+
continue
489+
if doc is not None:
490+
return doc
491+
return None
492+
493+
if ismethod(obj):
494+
name = obj.__func__.__name__
495+
self = obj.__self__
496+
if (isclass(self) and
497+
getattr(getattr(self, name, None), '__func__') is obj.__func__):
498+
# classmethod
499+
cls = self
500+
else:
501+
cls = self.__class__
502+
elif isfunction(obj):
503+
name = obj.__name__
504+
cls = _findclass(obj)
505+
if cls is None or getattr(cls, name) is not obj:
506+
return None
507+
elif isbuiltin(obj):
508+
name = obj.__name__
509+
self = obj.__self__
510+
if (isclass(self) and
511+
self.__qualname__ + '.' + name == obj.__qualname__):
512+
# classmethod
513+
cls = self
514+
else:
515+
cls = self.__class__
516+
elif ismethoddescriptor(obj) or isdatadescriptor(obj):
517+
name = obj.__name__
518+
cls = obj.__objclass__
519+
if getattr(cls, name) is not obj:
520+
return None
521+
elif isinstance(obj, property):
522+
func = f.fget
523+
name = func.__name__
524+
cls = _findclass(func)
525+
if cls is None or getattr(cls, name) is not obj:
526+
return None
527+
else:
528+
return None
529+
530+
for base in cls.__mro__:
531+
try:
532+
doc = getattr(base, name).__doc__
533+
except AttributeError:
534+
continue
535+
if doc is not None:
536+
return doc
537+
return None
538+
471539
def getdoc(object):
472540
"""Get the documentation string for an object.
473541
@@ -478,6 +546,11 @@ def getdoc(object):
478546
doc = object.__doc__
479547
except AttributeError:
480548
return None
549+
if doc is None:
550+
try:
551+
doc = _finddoc(object)
552+
except (AttributeError, TypeError):
553+
return None
481554
if not isinstance(doc, str):
482555
return None
483556
return cleandoc(doc)

Lib/test/inspect_fodder.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,24 @@ def argue(self, a, b, c):
4545
self.ex = sys.exc_info()
4646
self.tr = inspect.trace()
4747

48+
def contradiction(self):
49+
'The automatic gainsaying.'
50+
pass
51+
4852
# line 48
4953
class MalodorousPervert(StupidGit):
50-
pass
54+
def abuse(self, a, b, c):
55+
pass
56+
def contradiction(self):
57+
pass
5158

5259
Tit = MalodorousPervert
5360

5461
class ParrotDroppings:
5562
pass
5663

5764
class FesteringGob(MalodorousPervert, ParrotDroppings):
58-
pass
65+
def abuse(self, a, b, c):
66+
pass
67+
def contradiction(self):
68+
pass

Lib/test/test_inspect.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,27 @@ def test_getdoc(self):
292292
self.assertEqual(inspect.getdoc(git.abuse),
293293
'Another\n\ndocstring\n\ncontaining\n\ntabs')
294294

295+
@unittest.skipIf(sys.flags.optimize >= 2,
296+
"Docstrings are omitted with -O2 and above")
297+
def test_getdoc_inherited(self):
298+
self.assertEqual(inspect.getdoc(mod.FesteringGob),
299+
'A longer,\n\nindented\n\ndocstring.')
300+
self.assertEqual(inspect.getdoc(mod.FesteringGob.abuse),
301+
'Another\n\ndocstring\n\ncontaining\n\ntabs')
302+
self.assertEqual(inspect.getdoc(mod.FesteringGob().abuse),
303+
'Another\n\ndocstring\n\ncontaining\n\ntabs')
304+
self.assertEqual(inspect.getdoc(mod.FesteringGob.contradiction),
305+
'The automatic gainsaying.')
306+
307+
@unittest.skipIf(MISSING_C_DOCSTRINGS, "test requires docstrings")
308+
def test_finddoc(self):
309+
finddoc = inspect._finddoc
310+
self.assertEqual(finddoc(int), int.__doc__)
311+
self.assertEqual(finddoc(int.to_bytes), int.to_bytes.__doc__)
312+
self.assertEqual(finddoc(int().to_bytes), int.to_bytes.__doc__)
313+
self.assertEqual(finddoc(int.from_bytes), int.from_bytes.__doc__)
314+
self.assertEqual(finddoc(int.real), int.real.__doc__)
315+
295316
def test_cleandoc(self):
296317
self.assertEqual(inspect.cleandoc('An\n indented\n docstring.'),
297318
'An\nindented\ndocstring.')
@@ -316,7 +337,7 @@ def test_getmodule(self):
316337

317338
def test_getsource(self):
318339
self.assertSourceEqual(git.abuse, 29, 39)
319-
self.assertSourceEqual(mod.StupidGit, 21, 46)
340+
self.assertSourceEqual(mod.StupidGit, 21, 50)
320341

321342
def test_getsourcefile(self):
322343
self.assertEqual(normcase(inspect.getsourcefile(mod.spam)), modfile)

Misc/NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Core and Builtins
1616
Library
1717
-------
1818

19+
- Issue #15582: inspect.getdoc() now follows inheritance chains.
20+
1921
- Issue #2175: SAX parsers now support a character stream of InputSource object.
2022

2123
- Issue #16840: Tkinter now supports 64-bit integers added in Tcl 8.4 and

0 commit comments

Comments
 (0)