Skip to content

Commit

Permalink
bpo-40296: Fix supporting generic aliases in pydoc (GH-30253)
Browse files Browse the repository at this point in the history
  • Loading branch information
serhiy-storchaka committed Mar 18, 2022
1 parent a0db11b commit cd44afc
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 9 deletions.
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
12 changes: 12 additions & 0 deletions Lib/test/pydoc_mod.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""This is a test module for test_pydoc"""

import types
import typing

__author__ = "Benjamin Peterson"
__credits__ = "Nobody"
__version__ = "1.2.3.4"
Expand All @@ -24,6 +27,8 @@ def get_answer(self):
def is_it_true(self):
""" Return self.get_answer() """
return self.get_answer()
def __class_getitem__(self, item):
return types.GenericAlias(self, item)

def doc_func():
"""
Expand All @@ -35,3 +40,10 @@ def doc_func():

def nodoc_func():
pass


list_alias1 = typing.List[int]
list_alias2 = list[int]
c_alias = C[int]
type_union1 = typing.Union[int, str]
type_union2 = int | str
58 changes: 58 additions & 0 deletions Lib/test/test_pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ class C(builtins.object)
| say_no(self)
|\x20\x20
| ----------------------------------------------------------------------
| Class methods defined here:
|\x20\x20
| __class_getitem__(item) from builtins.type
|\x20\x20
| ----------------------------------------------------------------------
| Data descriptors defined here:
|\x20\x20
| __dict__
Expand All @@ -114,6 +119,11 @@ class C(builtins.object)
DATA
__xyz__ = 'X, Y and Z'
c_alias = test.pydoc_mod.C[int]
list_alias1 = typing.List[int]
list_alias2 = list[int]
type_union1 = typing.Union[int, str]
type_union2 = int | str
VERSION
1.2.3.4
Expand All @@ -135,6 +145,10 @@ class C(builtins.object)
test.pydoc_mod (version 1.2.3.4)
This is a test module for test_pydoc
Modules
types
typing
Classes
builtins.object
A
Expand Down Expand Up @@ -172,6 +186,8 @@ class C(builtins.object)
is_it_true(self)
Return self.get_answer()
say_no(self)
Class methods defined here:
__class_getitem__(item) from builtins.type
Data descriptors defined here:
__dict__
dictionary for instance variables (if defined)
Expand All @@ -188,6 +204,11 @@ class C(builtins.object)
Data
__xyz__ = 'X, Y and Z'
c_alias = test.pydoc_mod.C[int]
list_alias1 = typing.List[int]
list_alias2 = list[int]
type_union1 = typing.Union[int, str]
type_union2 = int | str
Author
Benjamin Peterson
Expand Down Expand Up @@ -1000,6 +1021,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`.

0 comments on commit cd44afc

Please sign in to comment.