From 5cf2b7253dc43b203c2f918416b4d25ad1dbfa7d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 3 Apr 2015 22:38:53 +0300 Subject: [PATCH] Issue #15582: inspect.getdoc() now follows inheritance chains. --- Doc/library/inspect.rst | 3 ++ Lib/inspect.py | 73 ++++++++++++++++++++++++++++++++++++++ Lib/test/inspect_fodder.py | 14 ++++++-- Lib/test/test_inspect.py | 23 +++++++++++- Misc/NEWS | 2 ++ 5 files changed, 112 insertions(+), 3 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 3d2132fd864..471200fa787 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -356,6 +356,9 @@ Retrieving source code .. function:: getdoc(object) Get the documentation string for an object, cleaned up with :func:`cleandoc`. + If the documentation string for an object is not provided and the object is + a class, a method, a property or a descriptor, retrieve the documentation + string from the inheritance hierarchy. .. function:: getcomments(object) diff --git a/Lib/inspect.py b/Lib/inspect.py index 98d665dd45f..81b1ce87098 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -468,6 +468,74 @@ def indentsize(line): expline = line.expandtabs() return len(expline) - len(expline.lstrip()) +def _findclass(func): + cls = sys.modules.get(func.__module__) + if cls is None: + return None + for name in func.__qualname__.split('.')[:-1]: + cls = getattr(cls, name) + if not isclass(cls): + return None + return cls + +def _finddoc(obj): + if isclass(obj): + for base in obj.__mro__: + if base is not object: + try: + doc = base.__doc__ + except AttributeError: + continue + if doc is not None: + return doc + return None + + if ismethod(obj): + name = obj.__func__.__name__ + self = obj.__self__ + if (isclass(self) and + getattr(getattr(self, name, None), '__func__') is obj.__func__): + # classmethod + cls = self + else: + cls = self.__class__ + elif isfunction(obj): + name = obj.__name__ + cls = _findclass(obj) + if cls is None or getattr(cls, name) is not obj: + return None + elif isbuiltin(obj): + name = obj.__name__ + self = obj.__self__ + if (isclass(self) and + self.__qualname__ + '.' + name == obj.__qualname__): + # classmethod + cls = self + else: + cls = self.__class__ + elif ismethoddescriptor(obj) or isdatadescriptor(obj): + name = obj.__name__ + cls = obj.__objclass__ + if getattr(cls, name) is not obj: + return None + elif isinstance(obj, property): + func = f.fget + name = func.__name__ + cls = _findclass(func) + if cls is None or getattr(cls, name) is not obj: + return None + else: + return None + + for base in cls.__mro__: + try: + doc = getattr(base, name).__doc__ + except AttributeError: + continue + if doc is not None: + return doc + return None + def getdoc(object): """Get the documentation string for an object. @@ -478,6 +546,11 @@ def getdoc(object): doc = object.__doc__ except AttributeError: return None + if doc is None: + try: + doc = _finddoc(object) + except (AttributeError, TypeError): + return None if not isinstance(doc, str): return None return cleandoc(doc) diff --git a/Lib/test/inspect_fodder.py b/Lib/test/inspect_fodder.py index 0c1d8103e9d..6f0cad942d3 100644 --- a/Lib/test/inspect_fodder.py +++ b/Lib/test/inspect_fodder.py @@ -45,9 +45,16 @@ class StupidGit: self.ex = sys.exc_info() self.tr = inspect.trace() + def contradiction(self): + 'The automatic gainsaying.' + pass + # line 48 class MalodorousPervert(StupidGit): - pass + def abuse(self, a, b, c): + pass + def contradiction(self): + pass Tit = MalodorousPervert @@ -55,4 +62,7 @@ class ParrotDroppings: pass class FesteringGob(MalodorousPervert, ParrotDroppings): - pass + def abuse(self, a, b, c): + pass + def contradiction(self): + pass diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 401e6cf4d49..2f7e58207bc 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -292,6 +292,27 @@ class TestRetrievingSourceCode(GetSourceBase): self.assertEqual(inspect.getdoc(git.abuse), 'Another\n\ndocstring\n\ncontaining\n\ntabs') + @unittest.skipIf(sys.flags.optimize >= 2, + "Docstrings are omitted with -O2 and above") + def test_getdoc_inherited(self): + self.assertEqual(inspect.getdoc(mod.FesteringGob), + 'A longer,\n\nindented\n\ndocstring.') + self.assertEqual(inspect.getdoc(mod.FesteringGob.abuse), + 'Another\n\ndocstring\n\ncontaining\n\ntabs') + self.assertEqual(inspect.getdoc(mod.FesteringGob().abuse), + 'Another\n\ndocstring\n\ncontaining\n\ntabs') + self.assertEqual(inspect.getdoc(mod.FesteringGob.contradiction), + 'The automatic gainsaying.') + + @unittest.skipIf(MISSING_C_DOCSTRINGS, "test requires docstrings") + def test_finddoc(self): + finddoc = inspect._finddoc + self.assertEqual(finddoc(int), int.__doc__) + self.assertEqual(finddoc(int.to_bytes), int.to_bytes.__doc__) + self.assertEqual(finddoc(int().to_bytes), int.to_bytes.__doc__) + self.assertEqual(finddoc(int.from_bytes), int.from_bytes.__doc__) + self.assertEqual(finddoc(int.real), int.real.__doc__) + def test_cleandoc(self): self.assertEqual(inspect.cleandoc('An\n indented\n docstring.'), 'An\nindented\ndocstring.') @@ -316,7 +337,7 @@ class TestRetrievingSourceCode(GetSourceBase): def test_getsource(self): self.assertSourceEqual(git.abuse, 29, 39) - self.assertSourceEqual(mod.StupidGit, 21, 46) + self.assertSourceEqual(mod.StupidGit, 21, 50) def test_getsourcefile(self): self.assertEqual(normcase(inspect.getsourcefile(mod.spam)), modfile) diff --git a/Misc/NEWS b/Misc/NEWS index b2306749b48..199473c144a 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -16,6 +16,8 @@ Core and Builtins Library ------- +- Issue #15582: inspect.getdoc() now follows inheritance chains. + - Issue #2175: SAX parsers now support a character stream of InputSource object. - Issue #16840: Tkinter now supports 64-bit integers added in Tcl 8.4 and