Skip to content

Commit

Permalink
bpo-40257: Revert changes to inspect.getdoc()
Browse files Browse the repository at this point in the history
  • Loading branch information
serhiy-storchaka committed May 13, 2020
1 parent a15c9b3 commit a72dd03
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 40 deletions.
5 changes: 1 addition & 4 deletions Doc/library/inspect.rst
Expand Up @@ -473,15 +473,12 @@ Retrieving source code

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 method, a property or a descriptor, retrieve the documentation
a class, a method, a property or a descriptor, retrieve the documentation
string from the inheritance hierarchy.

.. versionchanged:: 3.5
Documentation strings are now inherited if not overridden.

.. versionchanged:: 3.9
Documentation strings for classes are no longer inherited.


.. function:: getcomments(object)

Expand Down
5 changes: 0 additions & 5 deletions Doc/whatsnew/3.9.rst
Expand Up @@ -771,11 +771,6 @@ Changes in the Python API
:class:`ftplib.FTP_TLS` as a keyword-only parameter, and the default encoding
is changed from Latin-1 to UTF-8 to follow :rfc:`2640`.

* :func:`inspect.getdoc` no longer returns docstring inherited from the type
of the object or from parent class if it is a class if it is not defined
in the object itself.
(Contributed by Serhiy Storchaka in :issue:`40257`.)

* :meth:`asyncio.loop.shutdown_default_executor` has been added to
:class:`~asyncio.AbstractEventLoop`, meaning alternative event loops that
inherit from it should have this method defined.
Expand Down
33 changes: 16 additions & 17 deletions Lib/inspect.py
Expand Up @@ -543,6 +543,17 @@ def _findclass(func):
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__
Expand Down Expand Up @@ -586,35 +597,23 @@ def _finddoc(obj):
return None
for base in cls.__mro__:
try:
doc = _getowndoc(getattr(base, name))
doc = getattr(base, name).__doc__
except AttributeError:
continue
if doc is not None:
return doc
return None

def _getowndoc(obj):
"""Get the documentation string for an object if it is not
inherited from its class."""
try:
doc = object.__getattribute__(obj, '__doc__')
if doc is None:
return None
if obj is not type:
typedoc = type(obj).__doc__
if isinstance(typedoc, str) and typedoc == doc:
return None
return doc
except AttributeError:
return None

def getdoc(object):
"""Get the documentation string for an object.
All tabs are expanded to spaces. To clean up docstrings that are
indented to line up with blocks of code, any whitespace than can be
uniformly removed from the second line onwards is removed."""
doc = _getowndoc(object)
try:
doc = object.__doc__
except AttributeError:
return None
if doc is None:
try:
doc = _finddoc(object)
Expand Down
96 changes: 94 additions & 2 deletions Lib/pydoc.py
Expand Up @@ -90,9 +90,101 @@ def pathdirs():
normdirs.append(normdir)
return dirs

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 inspect.isclass(cls):
return None
return cls

def _finddoc(obj):
if inspect.ismethod(obj):
name = obj.__func__.__name__
self = obj.__self__
if (inspect.isclass(self) and
getattr(getattr(self, name, None), '__func__') is obj.__func__):
# classmethod
cls = self
else:
cls = self.__class__
elif inspect.isfunction(obj):
name = obj.__name__
cls = _findclass(obj)
if cls is None or getattr(cls, name) is not obj:
return None
elif inspect.isbuiltin(obj):
name = obj.__name__
self = obj.__self__
if (inspect.isclass(self) and
self.__qualname__ + '.' + name == obj.__qualname__):
# classmethod
cls = self
else:
cls = self.__class__
# Should be tested before isdatadescriptor().
elif isinstance(obj, property):
func = obj.fget
name = func.__name__
cls = _findclass(func)
if cls is None or getattr(cls, name) is not obj:
return None
elif inspect.ismethoddescriptor(obj) or inspect.isdatadescriptor(obj):
name = obj.__name__
cls = obj.__objclass__
if getattr(cls, name) is not obj:
return None
if inspect.ismemberdescriptor(obj):
slots = getattr(cls, '__slots__', None)
if isinstance(slots, dict) and name in slots:
return slots[name]
else:
return None
for base in cls.__mro__:
try:
doc = _getowndoc(getattr(base, name))
except AttributeError:
continue
if doc is not None:
return doc
return None

def _getowndoc(obj):
"""Get the documentation string for an object if it is not
inherited from its class."""
try:
doc = object.__getattribute__(obj, '__doc__')
if doc is None:
return None
if obj is not type:
typedoc = type(obj).__doc__
if isinstance(typedoc, str) and typedoc == doc:
return None
return doc
except AttributeError:
return None

def _getdoc(object):
"""Get the documentation string for an object.
All tabs are expanded to spaces. To clean up docstrings that are
indented to line up with blocks of code, any whitespace than can be
uniformly removed from the second line onwards is removed."""
doc = _getowndoc(object)
if doc is None:
try:
doc = _finddoc(object)
except (AttributeError, TypeError):
return None
if not isinstance(doc, str):
return None
return inspect.cleandoc(doc)

def getdoc(object):
"""Get the doc string or comments for an object."""
result = inspect.getdoc(object) or inspect.getcomments(object)
result = _getdoc(object) or inspect.getcomments(object)
return result and re.sub('^ *\n', '', result.rstrip()) or ''

def splitdoc(doc):
Expand Down Expand Up @@ -1669,7 +1761,7 @@ def render_doc(thing, title='Python Library Documentation: %s', forceload=0,
inspect.isclass(object) or
inspect.isroutine(object) or
inspect.isdatadescriptor(object) or
inspect.getdoc(object)):
_getdoc(object)):
# If the passed object is a piece of data or an instance,
# document its available methods instead of its value.
if hasattr(object, '__origin__'):
Expand Down
15 changes: 3 additions & 12 deletions Lib/test/test_inspect.py
Expand Up @@ -439,28 +439,19 @@ def test_getdoc(self):
@unittest.skipIf(sys.flags.optimize >= 2,
"Docstrings are omitted with -O2 and above")
def test_getdoc_inherited(self):
self.assertIsNone(inspect.getdoc(mod.FesteringGob))
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_getowndoc(self):
getowndoc = inspect._getowndoc
self.assertEqual(getowndoc(type), type.__doc__)
self.assertEqual(getowndoc(int), int.__doc__)
self.assertEqual(getowndoc(int.to_bytes), int.to_bytes.__doc__)
self.assertEqual(getowndoc(int().to_bytes), int.to_bytes.__doc__)
self.assertEqual(getowndoc(int.from_bytes), int.from_bytes.__doc__)
self.assertEqual(getowndoc(int.real), int.real.__doc__)

@unittest.skipIf(MISSING_C_DOCSTRINGS, "test requires docstrings")
def test_finddoc(self):
finddoc = inspect._finddoc
self.assertIsNone(finddoc(int))
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__)
Expand Down
@@ -0,0 +1 @@
Revert changes to :func:`inspect.getdoc`.

0 comments on commit a72dd03

Please sign in to comment.