Skip to content

Commit 6bd00e1

Browse files
committed
ENH: Improved typing annotation formatting
Fixes #324
1 parent 4d67976 commit 6bd00e1

File tree

2 files changed

+43
-15
lines changed

2 files changed

+43
-15
lines changed

pdoc/__init__.py

+37-14
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
from functools import lru_cache, reduce, partial
2323
from itertools import tee, groupby
2424
from types import ModuleType
25-
from typing import (
26-
cast, Any, Callable, Dict, Generator, Iterable, List, Mapping, Optional, Set, Tuple,
27-
Type, TypeVar, Union,
25+
from typing import ( # noqa: F401
26+
cast, Any, Callable, Dict, Generator, Iterable, List, Mapping, NewType,
27+
Optional, Set, Tuple, Type, TypeVar, Union,
2828
)
2929
from warnings import warn
3030

@@ -1242,20 +1242,43 @@ def _link_inheritance(self):
12421242

12431243
def _formatannotation(annot):
12441244
"""
1245-
Format annotation, properly handling NewType types
1245+
Format typing annotation with better handling of `typing.NewType`,
1246+
`typing.Optional`, `nptyping.NDArray` and other types.
12461247
1247-
>>> import typing
1248-
>>> _formatannotation(typing.NewType('MyType', str))
1248+
# >>> import typing
1249+
>>> _formatannotation(NewType('MyType', str))
12491250
'MyType'
1251+
>>> _formatannotation(Optional[Tuple[Optional[int], None]])
1252+
'Optional[Tuple[Optional[int], None]]'
12501253
"""
1251-
module = getattr(annot, '__module__', '')
1252-
is_newtype = (getattr(annot, '__qualname__', '').startswith('NewType.') and
1253-
module == 'typing')
1254-
if is_newtype:
1255-
return annot.__name__
1256-
if module.startswith('nptyping'): # GH-231
1257-
return repr(annot)
1258-
return inspect.formatannotation(annot)
1254+
class force_repr(str):
1255+
__repr__ = str.__str__
1256+
1257+
def maybe_replace_reprs(a):
1258+
# NoneType -> None
1259+
if a is type(None): # noqa: E721
1260+
return force_repr('None')
1261+
# Union[T, None] -> Optional[T]
1262+
if (getattr(a, '__origin__', None) is typing.Union and
1263+
len(a.__args__) == 2 and
1264+
a.__args__[1] is type(None)): # noqa: E721
1265+
t = inspect.formatannotation(maybe_replace_reprs(a.__args__[0]))
1266+
return force_repr(f'Optional[{t}]')
1267+
# typing.NewType('T', foo) -> T
1268+
module = getattr(a, '__module__', '')
1269+
if module == 'typing' and getattr(a, '__qualname__', '').startswith('NewType.'):
1270+
return force_repr(a.__name__)
1271+
# nptyping.types._ndarray.NDArray -> NDArray[(Any,), Int[64]] # GH-231
1272+
if module.startswith('nptyping.'):
1273+
return force_repr(repr(a))
1274+
# Recurse into args
1275+
try:
1276+
a = a.copy_with(tuple([maybe_replace_reprs(arg) for arg in a.__args__]))
1277+
except Exception:
1278+
pass # Not a typing._GenericAlias
1279+
return a
1280+
1281+
return str(inspect.formatannotation(maybe_replace_reprs(annot)))
12591282

12601283

12611284
class Function(Doc):

pdoc/test/__init__.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Unit tests for pdoc package.
33
"""
4+
import doctest
45
import enum
56
import inspect
67
import os
@@ -125,6 +126,10 @@ class CliTest(unittest.TestCase):
125126
def setUp(self):
126127
pdoc.reset()
127128

129+
def test_project_doctests(self):
130+
doctests = doctest.testmod(pdoc)
131+
assert not doctests.failed and doctests.attempted, doctests
132+
128133
def _basic_html_assertions(self, expected_files=PUBLIC_FILES):
129134
# Output directory tree layout is as expected
130135
files = glob('**', recursive=True)
@@ -927,7 +932,7 @@ def prop(self) -> typing.Optional[int]:
927932

928933
mod = DUMMY_PDOC_MODULE
929934
cls = pdoc.Class('Foobar', mod, Foobar)
930-
self.assertEqual(cls.doc['prop'].type_annotation(), 'Union[int,\N{NBSP}NoneType]')
935+
self.assertEqual(cls.doc['prop'].type_annotation(), 'Optional[int]')
931936

932937
@ignore_warnings
933938
def test_Variable_type_annotation_py36plus(self):

0 commit comments

Comments
 (0)