Skip to content

Commit 0d81051

Browse files
committed
Rewrote the annotation formatting logic
This fixes multiple annoying issues and makes the code easier to read. Fixes #113.
1 parent 29da90a commit 0d81051

File tree

3 files changed

+182
-165
lines changed

3 files changed

+182
-165
lines changed

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
UNRELEASED
2+
==========
3+
4+
* Rewrote the annotation formatting logic (fixes Python 3.5.2 compatibility regressions and an
5+
``AttributeError`` regression introduced in v1.9.0)
6+
7+
18
1.9.0
29
=====
310

sphinx_autodoc_typehints.py

Lines changed: 125 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -2,148 +2,145 @@
22
import sys
33
import textwrap
44
import typing
5-
from typing import get_type_hints, TypeVar, Generic
5+
from typing import get_type_hints, TypeVar, Any, AnyStr, Tuple
66

77
from sphinx.util import logging
88
from sphinx.util.inspect import Signature
99

10-
try:
11-
from typing_extensions import Protocol
12-
except ImportError:
13-
Protocol = None
14-
1510
logger = logging.getLogger(__name__)
1611
pydata_annotations = {'Any', 'AnyStr', 'Callable', 'ClassVar', 'Literal', 'NoReturn', 'Optional',
1712
'Tuple', 'Union'}
1813

1914

20-
def format_annotation(annotation, fully_qualified=False):
21-
if inspect.isclass(annotation) and annotation.__module__ == 'builtins':
22-
if annotation.__qualname__ == 'NoneType':
23-
return '``None``'
24-
else:
25-
return ':py:class:`{}`'.format(annotation.__qualname__)
15+
def get_annotation_module(annotation) -> str:
16+
# Special cases
17+
if annotation is None:
18+
return 'builtins'
2619

27-
annotation_cls = annotation if inspect.isclass(annotation) else type(annotation)
28-
if annotation_cls.__module__ in ('typing', 'typing_extensions'):
29-
params = None
30-
module = 'typing'
31-
extra = ''
20+
if hasattr(annotation, '__module__'):
21+
return annotation.__module__
22+
23+
if hasattr(annotation, '__origin__'):
24+
return annotation.__origin__.__module__
25+
26+
raise ValueError('Cannot determine the module of {}'.format(annotation))
27+
28+
29+
def get_annotation_class_name(annotation) -> str:
30+
# Special cases
31+
if annotation is None:
32+
return 'None'
33+
elif annotation is Any:
34+
return 'Any'
35+
elif inspect.isfunction(annotation) and hasattr(annotation, '__supertype__'):
36+
return 'NewType'
3237

33-
if inspect.isclass(annotation):
34-
class_name = annotation.__name__
38+
if getattr(annotation, '__name__', None):
39+
return annotation.__name__
40+
elif getattr(annotation, '_name', None): # Required for generic aliases on Python 3.7+
41+
return annotation._name
42+
elif getattr(annotation, 'name', None): # Required for at least Pattern
43+
return annotation.name
44+
45+
origin = getattr(annotation, '__origin__', None)
46+
if origin:
47+
if getattr(origin, '__name__', None): # Required for Protocol subclasses
48+
return origin.__name__
49+
elif getattr(origin, '_name', None): # Required for Union on Python 3.7+
50+
return origin._name
3551
else:
36-
class_name = str(annotation).split('[')[0].split('.')[-1]
37-
38-
origin = getattr(annotation, '__origin__', None)
39-
if inspect.isclass(origin):
40-
annotation_cls = annotation.__origin__
41-
try:
42-
mro = annotation_cls.mro()
43-
if Generic in mro or (Protocol and Protocol in mro):
44-
module = annotation_cls.__module__
45-
except TypeError:
46-
pass # annotation_cls was either the "type" object or typing.Type
47-
48-
if class_name == 'Any':
49-
return ':py:data:`{}typing.Any`'.format("" if fully_qualified else "~")
50-
elif class_name == '~AnyStr':
51-
return ':py:data:`{}typing.AnyStr`'.format("" if fully_qualified else "~")
52-
elif isinstance(annotation, TypeVar):
53-
return '\\%r' % annotation
54-
elif class_name == 'Union':
55-
if hasattr(annotation, '__union_params__'):
56-
params = annotation.__union_params__
57-
elif hasattr(annotation, '__args__'):
58-
params = annotation.__args__
59-
60-
if params and len(params) == 2 and (hasattr(params[1], '__qualname__') and
61-
params[1].__qualname__ == 'NoneType'):
62-
class_name = 'Optional'
63-
params = (params[0],)
64-
elif class_name == 'Tuple' and hasattr(annotation, '__tuple_params__'):
65-
params = annotation.__tuple_params__
66-
if annotation.__tuple_use_ellipsis__:
67-
params += (Ellipsis,)
68-
elif class_name == 'Callable':
69-
arg_annotations = result_annotation = None
70-
if hasattr(annotation, '__result__'):
71-
arg_annotations = annotation.__args__
72-
result_annotation = annotation.__result__
73-
elif getattr(annotation, '__args__', None):
74-
arg_annotations = annotation.__args__[:-1]
75-
result_annotation = annotation.__args__[-1]
76-
77-
if arg_annotations in (Ellipsis, (Ellipsis,)):
78-
params = [Ellipsis, result_annotation]
79-
elif arg_annotations is not None:
80-
params = [
81-
'\\[{}]'.format(
82-
', '.join(
83-
format_annotation(param, fully_qualified)
84-
for param in arg_annotations)),
85-
result_annotation
86-
]
87-
elif class_name == 'Literal':
88-
annotation_args = getattr(annotation, '__args__', ()) or annotation.__values__
89-
extra = '\\[{}]'.format(', '.join(repr(arg) for arg in annotation_args))
90-
elif class_name == 'ClassVar' and hasattr(annotation, '__type__'):
91-
# < py3.7
92-
params = (annotation.__type__,)
93-
elif hasattr(annotation, 'type_var'):
94-
# Type alias
95-
class_name = annotation.name
96-
params = (annotation.type_var,)
97-
elif getattr(annotation, '__args__', None) is not None:
98-
params = annotation.__args__
99-
elif hasattr(annotation, '__parameters__'):
100-
params = annotation.__parameters__
101-
102-
if params and annotation is not getattr(sys.modules[module], class_name):
103-
extra = '\\[{}]'.format(', '.join(
104-
format_annotation(param, fully_qualified) for param in params))
105-
106-
return '{prefix}`{qualify}{module}.{name}`{extra}'.format(
107-
prefix=':py:data:' if class_name in pydata_annotations else ':py:class:',
108-
qualify="" if fully_qualified else "~",
109-
module=module,
110-
name=class_name,
111-
extra=extra
112-
)
52+
return origin.__class__.__name__.lstrip('_') # Required for Union on Python < 3.7
53+
54+
annotation_cls = annotation if inspect.isclass(annotation) else annotation.__class__
55+
return annotation_cls.__name__.lstrip('_')
56+
57+
58+
def get_annotation_args(annotation, module: str, class_name: str) -> Tuple:
59+
try:
60+
original = getattr(sys.modules[module], class_name)
61+
except AttributeError:
62+
pass
63+
else:
64+
if annotation is original:
65+
return () # This is the original, unparametrized type
66+
67+
# Special cases
68+
if class_name in ('Pattern', 'Match') and hasattr(annotation, 'type_var'): # Python < 3.7
69+
return annotation.type_var,
70+
elif class_name == 'Callable' and hasattr(annotation, '__result__'): # Python < 3.5.3
71+
argtypes = (Ellipsis,) if annotation.__args__ is Ellipsis else annotation.__args__
72+
return argtypes + (annotation.__result__,)
73+
elif class_name == 'Union' and hasattr(annotation, '__union_params__'): # Union on Python 3.5
74+
return annotation.__union_params__
75+
elif class_name == 'Tuple' and hasattr(annotation, '__tuple_params__'): # Tuple on Python 3.5
76+
params = annotation.__tuple_params__
77+
if getattr(annotation, '__tuple_use_ellipsis__', False):
78+
params += (Ellipsis,)
79+
80+
return params
81+
elif class_name == 'ClassVar' and hasattr(annotation, '__type__'): # ClassVar on Python < 3.7
82+
return annotation.__type__,
83+
elif class_name == 'NewType' and hasattr(annotation, '__supertype__'):
84+
return annotation.__supertype__,
85+
elif class_name == 'Literal' and hasattr(annotation, '__values__'):
86+
return annotation.__values__
87+
elif class_name == 'Generic':
88+
return annotation.__parameters__
89+
90+
return getattr(annotation, '__args__', ())
91+
92+
93+
def format_annotation(annotation, fully_qualified: bool = False) -> str:
94+
# Special cases
95+
if annotation is None or annotation is type(None): # noqa: E721
96+
return '``None``'
11397
elif annotation is Ellipsis:
11498
return '...'
115-
elif (inspect.isfunction(annotation) and annotation.__module__ == 'typing' and
116-
hasattr(annotation, '__name__') and hasattr(annotation, '__supertype__')):
117-
return ':py:func:`{qualify}typing.NewType`\\(:py:data:`~{name}`, {extra})'.format(
118-
qualify="" if fully_qualified else "~",
119-
name=annotation.__name__,
120-
extra=format_annotation(annotation.__supertype__, fully_qualified),
121-
)
122-
elif inspect.isclass(annotation) or inspect.isclass(getattr(annotation, '__origin__', None)):
123-
if not inspect.isclass(annotation):
124-
annotation_cls = annotation.__origin__
125-
126-
extra = ''
127-
try:
128-
mro = annotation_cls.mro()
129-
except TypeError:
130-
pass
131-
else:
132-
if Generic in mro or (Protocol and Protocol in mro):
133-
params = (getattr(annotation, '__parameters__', None) or
134-
getattr(annotation, '__args__', None))
135-
if params:
136-
extra = '\\[{}]'.format(', '.join(
137-
format_annotation(param, fully_qualified) for param in params))
138-
139-
return ':py:class:`{qualify}{module}.{name}`{extra}'.format(
140-
qualify="" if fully_qualified else "~",
141-
module=annotation.__module__,
142-
name=annotation_cls.__qualname__,
143-
extra=extra
144-
)
145-
146-
return str(annotation)
99+
100+
# Type variables are also handled specially
101+
try:
102+
if isinstance(annotation, TypeVar) and annotation is not AnyStr:
103+
return '\\' + repr(annotation)
104+
except TypeError:
105+
pass
106+
107+
try:
108+
module = get_annotation_module(annotation)
109+
class_name = get_annotation_class_name(annotation)
110+
args = get_annotation_args(annotation, module, class_name)
111+
except ValueError:
112+
return str(annotation)
113+
114+
# Redirect all typing_extensions types to the stdlib typing module
115+
if module == 'typing_extensions':
116+
module = 'typing'
117+
118+
full_name = (module + '.' + class_name) if module != 'builtins' else class_name
119+
prefix = '' if fully_qualified or full_name == class_name else '~'
120+
role = 'data' if class_name in pydata_annotations else 'class'
121+
args_format = '\\[{}]'
122+
formatted_args = ''
123+
124+
# Some types require special handling
125+
if full_name == 'typing.NewType':
126+
args_format = '\\(:py:data:`~{name}`, {{}})'.format(prefix=prefix,
127+
name=annotation.__name__)
128+
role = 'func'
129+
elif full_name == 'typing.Union' and len(args) == 2 and type(None) in args:
130+
full_name = 'typing.Optional'
131+
args = tuple(x for x in args if x is not type(None)) # noqa: E721
132+
elif full_name == 'typing.Callable' and args and args[0] is not ...:
133+
formatted_args = '\\[\\[' + ', '.join(format_annotation(arg) for arg in args[:-1]) + ']'
134+
formatted_args += ', ' + format_annotation(args[-1]) + ']'
135+
elif full_name == 'typing.Literal':
136+
formatted_args = '\\[' + ', '.join(repr(arg) for arg in args) + ']'
137+
138+
if args and not formatted_args:
139+
formatted_args = args_format.format(', '.join(format_annotation(arg, fully_qualified)
140+
for arg in args))
141+
142+
return ':py:{role}:`{prefix}{full_name}`{formatted_args}'.format(
143+
role=role, prefix=prefix, full_name=full_name, formatted_args=formatted_args)
147144

148145

149146
def process_signature(app, what: str, name: str, obj, options, signature, return_annotation):

0 commit comments

Comments
 (0)