Skip to content

Commit cd44afc

Browse files
authoredMar 18, 2022
bpo-40296: Fix supporting generic aliases in pydoc (GH-30253)
1 parent a0db11b commit cd44afc

File tree

4 files changed

+84
-9
lines changed

4 files changed

+84
-9
lines changed
 

‎Lib/pydoc.py

+13-9
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class or function within a module or module in a package. If the
6969
import sysconfig
7070
import time
7171
import tokenize
72+
import types
7273
import urllib.parse
7374
import warnings
7475
from collections import deque
@@ -90,21 +91,24 @@ def pathdirs():
9091
normdirs.append(normdir)
9192
return dirs
9293

94+
def _isclass(object):
95+
return inspect.isclass(object) and not isinstance(object, types.GenericAlias)
96+
9397
def _findclass(func):
9498
cls = sys.modules.get(func.__module__)
9599
if cls is None:
96100
return None
97101
for name in func.__qualname__.split('.')[:-1]:
98102
cls = getattr(cls, name)
99-
if not inspect.isclass(cls):
103+
if not _isclass(cls):
100104
return None
101105
return cls
102106

103107
def _finddoc(obj):
104108
if inspect.ismethod(obj):
105109
name = obj.__func__.__name__
106110
self = obj.__self__
107-
if (inspect.isclass(self) and
111+
if (_isclass(self) and
108112
getattr(getattr(self, name, None), '__func__') is obj.__func__):
109113
# classmethod
110114
cls = self
@@ -118,7 +122,7 @@ def _finddoc(obj):
118122
elif inspect.isbuiltin(obj):
119123
name = obj.__name__
120124
self = obj.__self__
121-
if (inspect.isclass(self) and
125+
if (_isclass(self) and
122126
self.__qualname__ + '.' + name == obj.__qualname__):
123127
# classmethod
124128
cls = self
@@ -205,7 +209,7 @@ def classname(object, modname):
205209

206210
def isdata(object):
207211
"""Check if an object is of a type that probably means it's data."""
208-
return not (inspect.ismodule(object) or inspect.isclass(object) or
212+
return not (inspect.ismodule(object) or _isclass(object) or
209213
inspect.isroutine(object) or inspect.isframe(object) or
210214
inspect.istraceback(object) or inspect.iscode(object))
211215

@@ -470,7 +474,7 @@ def document(self, object, name=None, *args):
470474
# by lacking a __name__ attribute) and an instance.
471475
try:
472476
if inspect.ismodule(object): return self.docmodule(*args)
473-
if inspect.isclass(object): return self.docclass(*args)
477+
if _isclass(object): return self.docclass(*args)
474478
if inspect.isroutine(object): return self.docroutine(*args)
475479
except AttributeError:
476480
pass
@@ -772,7 +776,7 @@ def docmodule(self, object, name=None, mod=None, *ignored):
772776
modules = inspect.getmembers(object, inspect.ismodule)
773777

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

12141218
classes = []
1215-
for key, value in inspect.getmembers(object, inspect.isclass):
1219+
for key, value in inspect.getmembers(object, _isclass):
12161220
# if __all__ exists, believe it. Otherwise use old heuristic.
12171221
if (all is not None
12181222
or (inspect.getmodule(value) or object) is object):
@@ -1696,7 +1700,7 @@ def describe(thing):
16961700
return 'member descriptor %s.%s.%s' % (
16971701
thing.__objclass__.__module__, thing.__objclass__.__name__,
16981702
thing.__name__)
1699-
if inspect.isclass(thing):
1703+
if _isclass(thing):
17001704
return 'class ' + thing.__name__
17011705
if inspect.isfunction(thing):
17021706
return 'function ' + thing.__name__
@@ -1757,7 +1761,7 @@ def render_doc(thing, title='Python Library Documentation: %s', forceload=0,
17571761
desc += ' in module ' + module.__name__
17581762

17591763
if not (inspect.ismodule(object) or
1760-
inspect.isclass(object) or
1764+
_isclass(object) or
17611765
inspect.isroutine(object) or
17621766
inspect.isdatadescriptor(object) or
17631767
_getdoc(object)):

‎Lib/test/pydoc_mod.py

+12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""This is a test module for test_pydoc"""
22

3+
import types
4+
import typing
5+
36
__author__ = "Benjamin Peterson"
47
__credits__ = "Nobody"
58
__version__ = "1.2.3.4"
@@ -24,6 +27,8 @@ def get_answer(self):
2427
def is_it_true(self):
2528
""" Return self.get_answer() """
2629
return self.get_answer()
30+
def __class_getitem__(self, item):
31+
return types.GenericAlias(self, item)
2732

2833
def doc_func():
2934
"""
@@ -35,3 +40,10 @@ def doc_func():
3540

3641
def nodoc_func():
3742
pass
43+
44+
45+
list_alias1 = typing.List[int]
46+
list_alias2 = list[int]
47+
c_alias = C[int]
48+
type_union1 = typing.Union[int, str]
49+
type_union2 = int | str

‎Lib/test/test_pydoc.py

+58
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ class C(builtins.object)
9595
| say_no(self)
9696
|\x20\x20
9797
| ----------------------------------------------------------------------
98+
| Class methods defined here:
99+
|\x20\x20
100+
| __class_getitem__(item) from builtins.type
101+
|\x20\x20
102+
| ----------------------------------------------------------------------
98103
| Data descriptors defined here:
99104
|\x20\x20
100105
| __dict__
@@ -114,6 +119,11 @@ class C(builtins.object)
114119
115120
DATA
116121
__xyz__ = 'X, Y and Z'
122+
c_alias = test.pydoc_mod.C[int]
123+
list_alias1 = typing.List[int]
124+
list_alias2 = list[int]
125+
type_union1 = typing.Union[int, str]
126+
type_union2 = int | str
117127
118128
VERSION
119129
1.2.3.4
@@ -135,6 +145,10 @@ class C(builtins.object)
135145
test.pydoc_mod (version 1.2.3.4)
136146
This is a test module for test_pydoc
137147
148+
Modules
149+
types
150+
typing
151+
138152
Classes
139153
builtins.object
140154
A
@@ -172,6 +186,8 @@ class C(builtins.object)
172186
is_it_true(self)
173187
Return self.get_answer()
174188
say_no(self)
189+
Class methods defined here:
190+
__class_getitem__(item) from builtins.type
175191
Data descriptors defined here:
176192
__dict__
177193
dictionary for instance variables (if defined)
@@ -188,6 +204,11 @@ class C(builtins.object)
188204
189205
Data
190206
__xyz__ = 'X, Y and Z'
207+
c_alias = test.pydoc_mod.C[int]
208+
list_alias1 = typing.List[int]
209+
list_alias2 = list[int]
210+
type_union1 = typing.Union[int, str]
211+
type_union2 = int | str
191212
192213
Author
193214
Benjamin Peterson
@@ -1000,6 +1021,43 @@ class C: "New-style class"
10001021
expected = 'C in module %s object' % __name__
10011022
self.assertIn(expected, pydoc.render_doc(c))
10021023

1024+
def test_generic_alias(self):
1025+
self.assertEqual(pydoc.describe(typing.List[int]), '_GenericAlias')
1026+
doc = pydoc.render_doc(typing.List[int], renderer=pydoc.plaintext)
1027+
self.assertIn('_GenericAlias in module typing', doc)
1028+
self.assertIn('List = class list(object)', doc)
1029+
self.assertIn(list.__doc__.strip().splitlines()[0], doc)
1030+
1031+
self.assertEqual(pydoc.describe(list[int]), 'GenericAlias')
1032+
doc = pydoc.render_doc(list[int], renderer=pydoc.plaintext)
1033+
self.assertIn('GenericAlias in module builtins', doc)
1034+
self.assertIn('\nclass list(object)', doc)
1035+
self.assertIn(list.__doc__.strip().splitlines()[0], doc)
1036+
1037+
def test_union_type(self):
1038+
self.assertEqual(pydoc.describe(typing.Union[int, str]), '_UnionGenericAlias')
1039+
doc = pydoc.render_doc(typing.Union[int, str], renderer=pydoc.plaintext)
1040+
self.assertIn('_UnionGenericAlias in module typing', doc)
1041+
self.assertIn('Union = typing.Union', doc)
1042+
if typing.Union.__doc__:
1043+
self.assertIn(typing.Union.__doc__.strip().splitlines()[0], doc)
1044+
1045+
self.assertEqual(pydoc.describe(int | str), 'UnionType')
1046+
doc = pydoc.render_doc(int | str, renderer=pydoc.plaintext)
1047+
self.assertIn('UnionType in module types object', doc)
1048+
self.assertIn('\nclass UnionType(builtins.object)', doc)
1049+
self.assertIn(types.UnionType.__doc__.strip().splitlines()[0], doc)
1050+
1051+
def test_special_form(self):
1052+
self.assertEqual(pydoc.describe(typing.Any), '_SpecialForm')
1053+
doc = pydoc.render_doc(typing.Any, renderer=pydoc.plaintext)
1054+
self.assertIn('_SpecialForm in module typing', doc)
1055+
if typing.Any.__doc__:
1056+
self.assertIn('Any = typing.Any', doc)
1057+
self.assertIn(typing.Any.__doc__.strip().splitlines()[0], doc)
1058+
else:
1059+
self.assertIn('Any = class _SpecialForm(_Final)', doc)
1060+
10031061
def test_typing_pydoc(self):
10041062
def foo(data: typing.List[typing.Any],
10051063
x: int) -> typing.Iterator[typing.Tuple[int, typing.Any]]:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix supporting generic aliases in :mod:`pydoc`.

0 commit comments

Comments
 (0)
Please sign in to comment.