Skip to content

Commit 6e8bdfd

Browse files
committed
ENH: Improve formatting of Optional, Union and collection.abc types
Fixes #395
1 parent c706361 commit 6e8bdfd

File tree

2 files changed

+40
-12
lines changed

2 files changed

+40
-12
lines changed

pdoc/__init__.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,6 +1293,16 @@ def _formatannotation(annot):
12931293
'pdoc.MyType'
12941294
>>> _formatannotation(Optional[Tuple[Optional[int], None]])
12951295
'Optional[Tuple[Optional[int], None]]'
1296+
>>> _formatannotation(Optional[Union[int, float, None]])
1297+
'Optional[int | float]'
1298+
>>> _formatannotation(Union[int, float])
1299+
'int | float'
1300+
>>> from typing import Callable
1301+
>>> _formatannotation(Callable[[Optional[int]], float])
1302+
'Callable[[Optional[int]], float]'
1303+
>>> from collections.abc import Callable
1304+
>>> _formatannotation(Callable[[Optional[int]], float])
1305+
'Callable[[Optional[int]], float]'
12961306
"""
12971307
class force_repr(str):
12981308
__repr__ = str.__str__
@@ -1302,12 +1312,16 @@ def maybe_replace_reprs(a):
13021312
if a is type(None): # noqa: E721
13031313
return force_repr('None')
13041314
# Union[T, None] -> Optional[T]
1305-
if (getattr(a, '__origin__', None) is typing.Union and
1306-
len(a.__args__) == 2 and
1307-
type(None) in a.__args__):
1308-
t = inspect.formatannotation(
1309-
maybe_replace_reprs(next(filter(None, a.__args__))))
1310-
return force_repr(f'Optional[{t}]')
1315+
if getattr(a, '__origin__', None) is typing.Union:
1316+
union_args = a.__args__
1317+
is_optional = type(None) in union_args
1318+
if is_optional:
1319+
union_args = (x for x in union_args if x is not type(None))
1320+
t = ' | '.join(inspect.formatannotation(maybe_replace_reprs(x))
1321+
for x in union_args)
1322+
if is_optional:
1323+
t = f'Optional[{t}]'
1324+
return force_repr(t)
13111325
# typing.NewType('T', foo) -> T
13121326
module = getattr(a, '__module__', '')
13131327
if module == 'typing' and getattr(a, '__qualname__', '').startswith('NewType.'):
@@ -1316,11 +1330,25 @@ def maybe_replace_reprs(a):
13161330
if module.startswith('nptyping.'):
13171331
return force_repr(repr(a))
13181332
# Recurse into typing.Callable/etc. args
1319-
if hasattr(a, 'copy_with') and hasattr(a, '__args__'):
1320-
if a is typing.Callable:
1321-
# Bug on Python < 3.9, https://bugs.python.org/issue42195
1322-
return a
1323-
a = a.copy_with(tuple([maybe_replace_reprs(arg) for arg in a.__args__]))
1333+
if hasattr(a, '__args__'):
1334+
if hasattr(a, 'copy_with'):
1335+
if a is typing.Callable:
1336+
# Bug on Python < 3.9, https://bugs.python.org/issue42195
1337+
return a
1338+
a = a.copy_with(tuple([maybe_replace_reprs(arg) for arg in a.__args__]))
1339+
elif hasattr(a, '__origin__'):
1340+
args = tuple(map(maybe_replace_reprs, a.__args__))
1341+
try:
1342+
a = a.__origin__[args]
1343+
except TypeError:
1344+
# collections.abc.Callable takes "([in], out)"
1345+
a = a.__origin__[(args[:-1], args[-1])]
1346+
# Recurse into lists
1347+
if isinstance(a, (list, tuple)):
1348+
return type(a)(map(maybe_replace_reprs, a))
1349+
# Shorten standard collections: collections.abc.Callable -> Callable
1350+
if module == 'collections.abc':
1351+
return force_repr(repr(a).removeprefix('collections.abc.'))
13241352
return a
13251353

13261354
return str(inspect.formatannotation(maybe_replace_reprs(annot)))

pdoc/test/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -963,7 +963,7 @@ def test_test_Function_params_python38_specific(self):
963963
def test_Function_return_annotation(self):
964964
def f() -> typing.List[typing.Union[str, pdoc.Doc]]: return []
965965
func = pdoc.Function('f', DUMMY_PDOC_MODULE, f)
966-
self.assertEqual(func.return_annotation(), 'List[Union[str,\N{NBSP}pdoc.Doc]]')
966+
self.assertEqual(func.return_annotation(), 'List[str\N{NBSP}|\N{NBSP}pdoc.Doc]')
967967

968968
@ignore_warnings
969969
def test_Variable_type_annotation(self):

0 commit comments

Comments
 (0)