Skip to content

bpo-40296: Fix supporting generic aliases in pydoc #30253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 18, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions Lib/pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class or function within a module or module in a package. If the
import sysconfig
import time
import tokenize
import types
import urllib.parse
import warnings
from collections import deque
Expand All @@ -90,21 +91,24 @@ def pathdirs():
normdirs.append(normdir)
return dirs

def _isclass(object):
return inspect.isclass(object) and not isinstance(object, types.GenericAlias)

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):
if not _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
if (_isclass(self) and
getattr(getattr(self, name, None), '__func__') is obj.__func__):
# classmethod
cls = self
Expand All @@ -118,7 +122,7 @@ def _finddoc(obj):
elif inspect.isbuiltin(obj):
name = obj.__name__
self = obj.__self__
if (inspect.isclass(self) and
if (_isclass(self) and
self.__qualname__ + '.' + name == obj.__qualname__):
# classmethod
cls = self
Expand Down Expand Up @@ -205,7 +209,7 @@ def classname(object, modname):

def isdata(object):
"""Check if an object is of a type that probably means it's data."""
return not (inspect.ismodule(object) or inspect.isclass(object) or
return not (inspect.ismodule(object) or _isclass(object) or
inspect.isroutine(object) or inspect.isframe(object) or
inspect.istraceback(object) or inspect.iscode(object))

Expand Down Expand Up @@ -470,7 +474,7 @@ def document(self, object, name=None, *args):
# by lacking a __name__ attribute) and an instance.
try:
if inspect.ismodule(object): return self.docmodule(*args)
if inspect.isclass(object): return self.docclass(*args)
if _isclass(object): return self.docclass(*args)
if inspect.isroutine(object): return self.docroutine(*args)
except AttributeError:
pass
Expand Down Expand Up @@ -772,7 +776,7 @@ def docmodule(self, object, name=None, mod=None, *ignored):
modules = inspect.getmembers(object, inspect.ismodule)

classes, cdict = [], {}
for key, value in inspect.getmembers(object, inspect.isclass):
for key, value in inspect.getmembers(object, _isclass):
# if __all__ exists, believe it. Otherwise use old heuristic.
if (all is not None or
(inspect.getmodule(value) or object) is object):
Expand Down Expand Up @@ -1212,7 +1216,7 @@ def docmodule(self, object, name=None, mod=None):
result = result + self.section('DESCRIPTION', desc)

classes = []
for key, value in inspect.getmembers(object, inspect.isclass):
for key, value in inspect.getmembers(object, _isclass):
# if __all__ exists, believe it. Otherwise use old heuristic.
if (all is not None
or (inspect.getmodule(value) or object) is object):
Expand Down Expand Up @@ -1696,7 +1700,7 @@ def describe(thing):
return 'member descriptor %s.%s.%s' % (
thing.__objclass__.__module__, thing.__objclass__.__name__,
thing.__name__)
if inspect.isclass(thing):
if _isclass(thing):
return 'class ' + thing.__name__
if inspect.isfunction(thing):
return 'function ' + thing.__name__
Expand Down Expand Up @@ -1757,7 +1761,7 @@ def render_doc(thing, title='Python Library Documentation: %s', forceload=0,
desc += ' in module ' + module.__name__

if not (inspect.ismodule(object) or
inspect.isclass(object) or
_isclass(object) or
inspect.isroutine(object) or
inspect.isdatadescriptor(object) or
_getdoc(object)):
Expand Down
37 changes: 37 additions & 0 deletions Lib/test/test_pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,43 @@ class C: "New-style class"
expected = 'C in module %s object' % __name__
self.assertIn(expected, pydoc.render_doc(c))

def test_generic_alias(self):
self.assertEqual(pydoc.describe(typing.List[int]), '_GenericAlias')
doc = pydoc.render_doc(typing.List[int], renderer=pydoc.plaintext)
self.assertIn('_GenericAlias in module typing', doc)
self.assertIn('List = class list(object)', doc)
self.assertIn(list.__doc__.strip().splitlines()[0], doc)

self.assertEqual(pydoc.describe(list[int]), 'GenericAlias')
doc = pydoc.render_doc(list[int], renderer=pydoc.plaintext)
self.assertIn('GenericAlias in module builtins', doc)
self.assertIn('\nclass list(object)', doc)
self.assertIn(list.__doc__.strip().splitlines()[0], doc)

def test_union_type(self):
self.assertEqual(pydoc.describe(typing.Union[int, str]), '_UnionGenericAlias')
doc = pydoc.render_doc(typing.Union[int, str], renderer=pydoc.plaintext)
self.assertIn('_UnionGenericAlias in module typing', doc)
self.assertIn('Union = typing.Union', doc)
if typing.Union.__doc__:
self.assertIn(typing.Union.__doc__.strip().splitlines()[0], doc)

self.assertEqual(pydoc.describe(int | str), 'UnionType')
doc = pydoc.render_doc(int | str, renderer=pydoc.plaintext)
self.assertIn('UnionType in module types object', doc)
self.assertIn('\nclass UnionType(builtins.object)', doc)
self.assertIn(types.UnionType.__doc__.strip().splitlines()[0], doc)

def test_special_form(self):
self.assertEqual(pydoc.describe(typing.Any), '_SpecialForm')
doc = pydoc.render_doc(typing.Any, renderer=pydoc.plaintext)
self.assertIn('_SpecialForm in module typing', doc)
if typing.Any.__doc__:
self.assertIn('Any = typing.Any', doc)
self.assertIn(typing.Any.__doc__.strip().splitlines()[0], doc)
else:
self.assertIn('Any = class _SpecialForm(_Final)', doc)

def test_typing_pydoc(self):
def foo(data: typing.List[typing.Any],
x: int) -> typing.Iterator[typing.Tuple[int, typing.Any]]:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix supporting generic aliases in :mod:`pydoc`.