New upstream version 1.8.0
William Grzybowski
4 years ago
13 | 13 | env: TOXENV=flake8 |
14 | 14 | |
15 | 15 | - stage: test |
16 | env: TOXENV=py34 | |
17 | python: "3.4" | |
16 | env: TOXENV=py35 | |
17 | python: "3.5" | |
18 | 18 | after_success: &after_success |
19 | 19 | - pip install coveralls |
20 | 20 | - coveralls |
21 | ||
22 | - stage: test | |
23 | env: TOXENV=py35 | |
24 | python: "3.5" | |
25 | after_success: *after_success | |
26 | 21 | |
27 | 22 | - stage: test |
28 | 23 | env: TOXENV=py36 |
34 | 29 | python: "3.7" |
35 | 30 | after_success: *after_success |
36 | 31 | |
32 | - stage: test | |
33 | env: TOXENV=py38 | |
34 | python: "3.8-dev" | |
35 | after_success: *after_success | |
36 | ||
37 | 37 | - stage: deploy to pypi |
38 | 38 | install: skip |
39 | 39 | script: skip |
41 | 41 | provider: pypi |
42 | 42 | user: agronholm |
43 | 43 | password: |
44 | secure: EnJn6vbbOORBaEFLQ1ITpjR2+mXX+DGPCGklfdCQCdF9iLr1LKVDe5V+DRPPmrg7uXN1fNYPQawG16rqsmIKLkANq7S5pDZ1HpVffyAKN5DPWxP1+f97SE8I978NcgpZwrXIbcM6p8UMfOO0u11ICYdDsSU5mkE6qYTNFW1lDL3G06DW8dedR6zbZmVDBrrh6E4DmOjS8o+kxkmtv9W59ZvocwaQFrs+3zRe3ybzxDlk9DyincD5mRn7f3UOaMGUmI9rzA43v8MD34yB0pF22TlEeOV3WcnuXxSuqqMOdd8WaZMm2rH6zkIJJwSjkgEolE942CSkh+tuwUVBneG0FNXjiIaIDFBLic9o1hC8kcjkQEN/2d2ldVUDpJCQKvg6nc9y3CNUl935PIzMcit3csWlEbJsPdSCSkB8A1xX9XY/Y38A1V/iJ1GUKJjthQgiT2VL6g/tkdqlsn0jTsOODaS170ULOJhyehpDk3bQ2uLESnwOGIiscksYxy1XOTWrmB+it99eAniAqEZC77hZGL5ma577m7IU1ianTqXkRaoLwBV30zXQGXETTeVu0h46W0pkVcuBnwp8GprXtl10mtiMbbh85XAOLWA3k1S087leSIxWLPKP8FjfBdQvbuk+JMef4p3KzDjBbJZEtalBlhaGZ7lQuxoRV0fuL4uZuLs= | |
44 | secure: duaV12IvSrtlrjcqkbOToB0YTQkFRMM3SADKPVL4JapNYbhGCHsNgauAptnIZrTIFy3B2ZQH5QxOu1xapR3LHbwCrh9VV6QYTU1BFV8ju5gTcnCWcuN0Sr42LuwB3v5sCjijMrNIfo04ovhgJKCPfOiFV3bsXv+PSUm221qLixG8vHmoP2Vqhb+8+McV/JeMMjxfMv/XFb3fWoQwaspERVu/Xt4f/taJ7JFNOJBjYYwYY79mxE6TJOTypgnrgypO0YyqjrvVsjFNuCH3QeQYtDIcJRTekp/Oo9hiNt6T4nuf3X09F9vKhFuGXtpmdwjnIktQb2jkP4FSHGJ3z/6UJP7yPgMXaFezzih5WjBVuMwDu9HOo4EHE+0hgkL5aQfbFulF2moE7PGEqhTWZkEzxGKc/ds+YbfYGigrcpuCm+KvDtQHAUkrIa8mEw5wM5+QGiiBGEzxZ6ifsZzxADEoCNshU3r6rHBWlA4ze5Q0PFCC7Jns2uqe51+9qqBz+cGKjQafn+1DwGBIr/tZusx8cjRJpsvZ116Zq7viCfzBmxEt4yA5UPYmpljS7bBSJrbrXVRNZGmAm9oO5adI99MnrQsDdVMM3KoC0R3JOmiGaKuM573am57EZ6c/hKKqyLs4MS6WLkYbygNPq3N0bQG6JKtvVKGPB1xTA116Mve6cJc= | |
45 | 45 | distributions: sdist bdist_wheel |
46 | 46 | on: |
47 | 47 | tags: true |
0 | 1.8.0 | |
1 | ===== | |
2 | ||
3 | * Fixed regression which caused ``TypeError`` or ``OSError`` when trying to set annotations due to | |
4 | PR #87 | |
5 | * Fixed unintentional mangling of annotation type names | |
6 | * Added proper ``:py:data`` targets for ``NoReturn``, ``ClassVar`` and ``Tuple`` | |
7 | * Added support for inline type comments (like ``(int, str) -> None``) (PR by Bernát Gábor) | |
8 | * Use the native AST parser for type comment support on Python 3.8+ | |
9 | ||
10 | ||
11 | 1.7.0 | |
12 | ===== | |
13 | ||
14 | * Dropped support for Python 3.4 | |
15 | * Fixed unwrapped local functions causing errors (PR by Kimiyuki Onaka) | |
16 | * Fixed ``AttributeError`` when documenting the ``__init__()`` method of a data class | |
17 | * Added support for type hint comments (PR by Markus Unterwaditzer) | |
18 | * Added flag for rendering classes with their fully qualified names (PR by Holly Becker) | |
19 | ||
20 | ||
0 | 21 | 1.6.0 |
1 | 22 | ===== |
2 | 23 |
54 | 54 | |
55 | 55 | The following configuration options are accepted: |
56 | 56 | |
57 | * ``set_type_checking_flag`` (default: ``True``): if ``True``, set ``typing.TYPE_CHECKING`` to | |
57 | * ``set_type_checking_flag`` (default: ``False``): if ``True``, set ``typing.TYPE_CHECKING`` to | |
58 | 58 | ``True`` to enable "expensive" typing imports |
59 | * ``typehints_fully_qualified`` (default: ``False``): if ``True``, class names are always fully | |
60 | qualified (e.g. ``module.for.Class``). If ``False``, just the class name displays (e.g. | |
61 | ``Class``) | |
62 | * ``always_document_param_types`` (default: ``False``): If ``False``, do not add type info for | |
63 | undocumented parameters. If ``True``, add stub documentation for undocumented parameters to | |
64 | be able to add type info. | |
59 | 65 | |
60 | 66 | |
61 | 67 | How it works |
92 | 98 | ``def methodname(self, param1: 'othermodule.OtherClass'):``) |
93 | 99 | |
94 | 100 | On Python 3.7, you can even use ``from __future__ import annotations`` and remove the quotes. |
101 | ||
102 | ||
103 | Using type hint comments | |
104 | ------------------------ | |
105 | ||
106 | If you're documenting code that needs to stay compatible with Python 2.7, you cannot use regular | |
107 | type annotations. Instead, you must either be using Python 3.8 or later or have typed_ast_ | |
108 | installed. The package extras ``type_comments`` will pull in the appropiate dependencies automatically. | |
109 | Then you can add type hint comments in the following manner: | |
110 | ||
111 | .. code-block:: python | |
112 | ||
113 | def myfunction(arg1, arg2): | |
114 | # type: (int, str) -> int | |
115 | return 42 | |
116 | ||
117 | or alternatively: | |
118 | ||
119 | .. code-block:: python | |
120 | ||
121 | def myfunction( | |
122 | arg1, # type: int | |
123 | arg2 # type: str | |
124 | ): | |
125 | # type: (...) -> int | |
126 | return 42 | |
127 | ||
128 | .. _typed_ast: https://pypi.org/project/typed-ast/ |
0 | 0 | [build-system] |
1 | requires = ["setuptools >= 36.2.7", "wheel", "setuptools_scm >= 1.7.0"] | |
1 | requires = [ | |
2 | "setuptools >= 40.0.4", | |
3 | "setuptools_scm >= 2.0.0", | |
4 | "wheel >= 0.29.0", | |
5 | ] | |
6 | build-backend = 'setuptools.build_meta' |
5 | 5 | author_email = alex.gronholm@nextday.fi |
6 | 6 | license = MIT |
7 | 7 | project_urls = |
8 | Bug Tracker = https://github.com/agronholm/sphinx-autodoc-typehints/issues | |
9 | Source Code = https://github.com/agronholm/sphinx-autodoc-typehints | |
8 | Change log = https://github.com/agronholm/sphinx-autodoc-typehints/blob/master/CHANGELOG.rst | |
9 | Source code = https://github.com/agronholm/sphinx-autodoc-typehints | |
10 | Issue tracker = https://github.com/agronholm/sphinx-autodoc-typehints/issues | |
10 | 11 | classifiers = |
11 | 12 | Development Status :: 5 - Production/Stable |
12 | 13 | Framework :: Sphinx :: Extension |
16 | 17 | Topic :: Documentation :: Sphinx |
17 | 18 | Programming Language :: Python |
18 | 19 | Programming Language :: Python :: 3 |
19 | Programming Language :: Python :: 3.4 | |
20 | 20 | Programming Language :: Python :: 3.5 |
21 | 21 | Programming Language :: Python :: 3.6 |
22 | 22 | Programming Language :: Python :: 3.7 |
23 | Programming Language :: Python :: 3.8 | |
23 | 24 | |
24 | 25 | [options] |
25 | 26 | py_modules = sphinx_autodoc_typehints |
26 | python_requires = !=3.5.0, !=3.5.1 | |
27 | install_requires = | |
28 | Sphinx >= 1.7 | |
29 | typing >= 3.5; python_version == "3.4" | |
27 | python_requires = >=3.5.2 | |
28 | install_requires = Sphinx >= 2.1 | |
30 | 29 | |
31 | 30 | [options.extras_require] |
32 | 31 | test = |
33 | 32 | pytest >= 3.1.0 |
34 | 33 | typing_extensions >= 3.5 |
34 | dataclasses; python_version == "3.6" | |
35 | type_comments = | |
36 | typed_ast >= 1.4.0; python_version < "3.8" | |
35 | 37 | |
36 | 38 | [flake8] |
37 | 39 | max-line-length = 99 |
38 | 40 | |
39 | 41 | [tool:pytest] |
40 | testpaths = tests/ | |
42 | testpaths = tests |
0 | 0 | from setuptools import setup |
1 | 1 | |
2 | 2 | setup( |
3 | use_scm_version=True, | |
3 | use_scm_version={ | |
4 | 'version_scheme': 'post-release', | |
5 | 'local_scheme': 'dirty-tag' | |
6 | }, | |
4 | 7 | setup_requires=[ |
5 | 8 | 'setuptools_scm >= 1.7.0', |
6 | 9 | 'setuptools >= 36.2.7' |
0 | 0 | import inspect |
1 | import sys | |
2 | import textwrap | |
1 | 3 | import typing |
2 | 4 | from typing import get_type_hints, TypeVar, Any, AnyStr, Generic, Union |
3 | 5 | |
9 | 11 | except ImportError: |
10 | 12 | Protocol = None |
11 | 13 | |
12 | try: | |
13 | from inspect import unwrap | |
14 | except ImportError: | |
15 | def unwrap(func, *, stop=None): | |
16 | """This is the inspect.unwrap() method copied from Python 3.5's standard library.""" | |
17 | if stop is None: | |
18 | def _is_wrapper(f): | |
19 | return hasattr(f, '__wrapped__') | |
20 | else: | |
21 | def _is_wrapper(f): | |
22 | return hasattr(f, '__wrapped__') and not stop(f) | |
23 | f = func # remember the original func for error reporting | |
24 | memo = {id(f)} # Memoise by id to tolerate non-hashable objects | |
25 | while _is_wrapper(func): | |
26 | func = func.__wrapped__ | |
27 | id_func = id(func) | |
28 | if id_func in memo: | |
29 | raise ValueError('wrapper loop when unwrapping {!r}'.format(f)) | |
30 | memo.add(id_func) | |
31 | return func | |
32 | ||
33 | 14 | logger = logging.getLogger(__name__) |
34 | ||
35 | ||
36 | def format_annotation(annotation): | |
15 | pydata_annotations = {'Any', 'AnyStr', 'Callable', 'ClassVar', 'NoReturn', 'Optional', 'Tuple', | |
16 | 'Union'} | |
17 | ||
18 | ||
19 | def format_annotation(annotation, fully_qualified=False): | |
37 | 20 | if inspect.isclass(annotation) and annotation.__module__ == 'builtins': |
38 | 21 | if annotation.__qualname__ == 'NoneType': |
39 | 22 | return '``None``' |
41 | 24 | return ':py:class:`{}`'.format(annotation.__qualname__) |
42 | 25 | |
43 | 26 | annotation_cls = annotation if inspect.isclass(annotation) else type(annotation) |
44 | class_name = None | |
45 | 27 | if annotation_cls.__module__ == 'typing': |
28 | class_name = str(annotation).split('[')[0].split('.')[-1] | |
46 | 29 | params = None |
47 | prefix = ':py:class:' | |
48 | 30 | module = 'typing' |
49 | 31 | extra = '' |
50 | 32 | |
51 | if inspect.isclass(getattr(annotation, '__origin__', None)): | |
33 | origin = getattr(annotation, '__origin__', None) | |
34 | if inspect.isclass(origin): | |
52 | 35 | annotation_cls = annotation.__origin__ |
53 | 36 | try: |
54 | 37 | mro = annotation_cls.mro() |
58 | 41 | pass # annotation_cls was either the "type" object or typing.Type |
59 | 42 | |
60 | 43 | if annotation is Any: |
61 | return ':py:data:`~typing.Any`' | |
44 | return ':py:data:`{}typing.Any`'.format("" if fully_qualified else "~") | |
62 | 45 | elif annotation is AnyStr: |
63 | return ':py:data:`~typing.AnyStr`' | |
46 | return ':py:data:`{}typing.AnyStr`'.format("" if fully_qualified else "~") | |
64 | 47 | elif isinstance(annotation, TypeVar): |
65 | 48 | return '\\%r' % annotation |
66 | 49 | elif (annotation is Union or getattr(annotation, '__origin__', None) is Union or |
67 | 50 | hasattr(annotation, '__union_params__')): |
68 | prefix = ':py:data:' | |
69 | class_name = 'Union' | |
70 | 51 | if hasattr(annotation, '__union_params__'): |
71 | 52 | params = annotation.__union_params__ |
72 | 53 | elif hasattr(annotation, '__args__'): |
81 | 62 | if annotation.__tuple_use_ellipsis__: |
82 | 63 | params += (Ellipsis,) |
83 | 64 | elif annotation_cls.__qualname__ == 'Callable': |
84 | prefix = ':py:data:' | |
85 | 65 | arg_annotations = result_annotation = None |
86 | 66 | if hasattr(annotation, '__result__'): |
87 | 67 | arg_annotations = annotation.__args__ |
95 | 75 | elif arg_annotations is not None: |
96 | 76 | params = [ |
97 | 77 | '\\[{}]'.format( |
98 | ', '.join(format_annotation(param) for param in arg_annotations)), | |
78 | ', '.join( | |
79 | format_annotation(param, fully_qualified) | |
80 | for param in arg_annotations)), | |
99 | 81 | result_annotation |
100 | 82 | ] |
83 | elif str(annotation).startswith('typing.ClassVar[') and hasattr(annotation, '__type__'): | |
84 | # < py3.7 | |
85 | params = (annotation.__type__,) | |
101 | 86 | elif hasattr(annotation, 'type_var'): |
102 | 87 | # Type alias |
103 | 88 | class_name = annotation.name |
108 | 93 | params = annotation.__parameters__ |
109 | 94 | |
110 | 95 | if params: |
111 | extra = '\\[{}]'.format(', '.join(format_annotation(param) for param in params)) | |
112 | ||
113 | if not class_name: | |
114 | class_name = annotation_cls.__qualname__.title() | |
115 | ||
116 | return '{}`~{}.{}`{}'.format(prefix, module, class_name, extra) | |
96 | extra = '\\[{}]'.format(', '.join( | |
97 | format_annotation(param, fully_qualified) for param in params)) | |
98 | ||
99 | return '{prefix}`{qualify}{module}.{name}`{extra}'.format( | |
100 | prefix=':py:data:' if class_name in pydata_annotations else ':py:class:', | |
101 | qualify="" if fully_qualified else "~", | |
102 | module=module, | |
103 | name=class_name, | |
104 | extra=extra | |
105 | ) | |
117 | 106 | elif annotation is Ellipsis: |
118 | 107 | return '...' |
119 | 108 | elif (inspect.isfunction(annotation) and annotation.__module__ == 'typing' and |
120 | 109 | hasattr(annotation, '__name__') and hasattr(annotation, '__supertype__')): |
121 | return ':py:func:`~typing.NewType`\\(:py:data:`~{}`, {})'.format( | |
122 | annotation.__name__, format_annotation(annotation.__supertype__)) | |
110 | return ':py:func:`{qualify}typing.NewType`\\(:py:data:`~{name}`, {extra})'.format( | |
111 | qualify="" if fully_qualified else "~", | |
112 | name=annotation.__name__, | |
113 | extra=format_annotation(annotation.__supertype__, fully_qualified), | |
114 | ) | |
123 | 115 | elif inspect.isclass(annotation) or inspect.isclass(getattr(annotation, '__origin__', None)): |
124 | 116 | if not inspect.isclass(annotation): |
125 | 117 | annotation_cls = annotation.__origin__ |
130 | 122 | params = (getattr(annotation, '__parameters__', None) or |
131 | 123 | getattr(annotation, '__args__', None)) |
132 | 124 | if params: |
133 | extra = '\\[{}]'.format(', '.join(format_annotation(param) for param in params)) | |
134 | ||
135 | return ':py:class:`~{}.{}`{}'.format(annotation.__module__, annotation_cls.__qualname__, | |
136 | extra) | |
125 | extra = '\\[{}]'.format(', '.join( | |
126 | format_annotation(param, fully_qualified) for param in params)) | |
127 | ||
128 | return ':py:class:`{qualify}{module}.{name}`{extra}'.format( | |
129 | qualify="" if fully_qualified else "~", | |
130 | module=annotation.__module__, | |
131 | name=annotation_cls.__qualname__, | |
132 | extra=extra | |
133 | ) | |
137 | 134 | |
138 | 135 | return str(annotation) |
139 | 136 | |
148 | 145 | if not getattr(obj, '__annotations__', None): |
149 | 146 | return |
150 | 147 | |
151 | obj = unwrap(obj) | |
148 | obj = inspect.unwrap(obj) | |
152 | 149 | signature = Signature(obj) |
153 | 150 | parameters = [ |
154 | 151 | param.replace(annotation=inspect.Parameter.empty) |
155 | 152 | for param in signature.signature.parameters.values() |
156 | 153 | ] |
154 | ||
155 | if '<locals>' in obj.__qualname__: | |
156 | logger.warning( | |
157 | 'Cannot treat a function defined as a local function: "%s" (use @functools.wraps)', | |
158 | name) | |
159 | return | |
157 | 160 | |
158 | 161 | if parameters: |
159 | 162 | if what in ('class', 'exception'): |
171 | 174 | class_name = obj.__qualname__.split('.')[-2] |
172 | 175 | method_name = "_{c}{m}".format(c=class_name, m=method_name) |
173 | 176 | |
174 | method_object = outer.__dict__[method_name] | |
177 | method_object = outer.__dict__[method_name] if outer else obj | |
175 | 178 | if not isinstance(method_object, (classmethod, staticmethod)): |
176 | 179 | del parameters[0] |
177 | 180 | |
182 | 185 | return signature.format_args().replace('\\', '\\\\'), None |
183 | 186 | |
184 | 187 | |
188 | def get_all_type_hints(obj, name): | |
189 | rv = {} | |
190 | ||
191 | try: | |
192 | rv = get_type_hints(obj) | |
193 | except (AttributeError, TypeError, RecursionError): | |
194 | # Introspecting a slot wrapper will raise TypeError, and and some recursive type | |
195 | # definitions will cause a RecursionError (https://github.com/python/typing/issues/574). | |
196 | pass | |
197 | except NameError as exc: | |
198 | logger.warning('Cannot resolve forward reference in type annotations of "%s": %s', | |
199 | name, exc) | |
200 | rv = obj.__annotations__ | |
201 | ||
202 | if rv: | |
203 | return rv | |
204 | ||
205 | rv = backfill_type_hints(obj, name) | |
206 | ||
207 | try: | |
208 | obj.__annotations__ = rv | |
209 | except (AttributeError, TypeError): | |
210 | return rv | |
211 | ||
212 | try: | |
213 | rv = get_type_hints(obj) | |
214 | except (AttributeError, TypeError): | |
215 | pass | |
216 | except NameError as exc: | |
217 | logger.warning('Cannot resolve forward reference in type annotations of "%s": %s', | |
218 | name, exc) | |
219 | rv = obj.__annotations__ | |
220 | ||
221 | return rv | |
222 | ||
223 | ||
224 | def backfill_type_hints(obj, name): | |
225 | parse_kwargs = {} | |
226 | if sys.version_info < (3, 8): | |
227 | try: | |
228 | import typed_ast.ast3 as ast | |
229 | except ImportError: | |
230 | return {} | |
231 | else: | |
232 | import ast | |
233 | parse_kwargs = {'type_comments': True} | |
234 | ||
235 | def _one_child(module): | |
236 | children = module.body # use the body to ignore type comments | |
237 | ||
238 | if len(children) != 1: | |
239 | logger.warning( | |
240 | 'Did not get exactly one node from AST for "%s", got %s', name, len(children)) | |
241 | return | |
242 | ||
243 | return children[0] | |
244 | ||
245 | try: | |
246 | obj_ast = ast.parse(textwrap.dedent(inspect.getsource(obj)), **parse_kwargs) | |
247 | except TypeError: | |
248 | return {} | |
249 | ||
250 | obj_ast = _one_child(obj_ast) | |
251 | if obj_ast is None: | |
252 | return {} | |
253 | ||
254 | try: | |
255 | type_comment = obj_ast.type_comment | |
256 | except AttributeError: | |
257 | return {} | |
258 | ||
259 | if not type_comment: | |
260 | return {} | |
261 | ||
262 | try: | |
263 | comment_args_str, comment_returns = type_comment.split(' -> ') | |
264 | except ValueError: | |
265 | logger.warning('Unparseable type hint comment for "%s": Expected to contain ` -> `', name) | |
266 | return {} | |
267 | ||
268 | rv = {} | |
269 | if comment_returns: | |
270 | rv['return'] = comment_returns | |
271 | ||
272 | args = load_args(obj_ast) | |
273 | comment_args = split_type_comment_args(comment_args_str) | |
274 | is_inline = len(comment_args) == 1 and comment_args[0] == "..." | |
275 | if not is_inline: | |
276 | if args and args[0].arg in ("self", "cls") and len(comment_args) != len(args): | |
277 | comment_args.insert(0, None) # self/cls may be omitted in type comments, insert blank | |
278 | ||
279 | if len(args) != len(comment_args): | |
280 | logger.warning('Not enough type comments found on "%s"', name) | |
281 | return rv | |
282 | ||
283 | for at, arg in enumerate(args): | |
284 | arg_key = getattr(arg, "arg", None) | |
285 | if arg_key is None: | |
286 | continue | |
287 | ||
288 | if is_inline: # the type information now is tied to the argument | |
289 | value = getattr(arg, "type_comment", None) | |
290 | else: # type data from comment | |
291 | value = comment_args[at] | |
292 | ||
293 | if value is not None: | |
294 | rv[arg_key] = value | |
295 | ||
296 | return rv | |
297 | ||
298 | ||
299 | def load_args(obj_ast): | |
300 | func_args = obj_ast.args | |
301 | args = [] | |
302 | pos_only = getattr(func_args, 'posonlyargs', None) | |
303 | if pos_only: | |
304 | args.extend(pos_only) | |
305 | ||
306 | args.extend(func_args.args) | |
307 | if func_args.vararg: | |
308 | args.append(func_args.vararg) | |
309 | ||
310 | args.extend(func_args.kwonlyargs) | |
311 | if func_args.kwarg: | |
312 | args.append(func_args.kwarg) | |
313 | ||
314 | return args | |
315 | ||
316 | ||
317 | def split_type_comment_args(comment): | |
318 | def add(val): | |
319 | result.append(val.strip().lstrip("*")) # remove spaces, and var/kw arg marker | |
320 | ||
321 | comment = comment.strip().lstrip("(").rstrip(")") | |
322 | result = [] | |
323 | if not comment: | |
324 | return result | |
325 | ||
326 | brackets, start_arg_at, at = 0, 0, 0 | |
327 | for at, char in enumerate(comment): | |
328 | if char in ("[", "("): | |
329 | brackets += 1 | |
330 | elif char in ("]", ")"): | |
331 | brackets -= 1 | |
332 | elif char == "," and brackets == 0: | |
333 | add(comment[start_arg_at:at]) | |
334 | start_arg_at = at + 1 | |
335 | ||
336 | add(comment[start_arg_at: at + 1]) | |
337 | return result | |
338 | ||
339 | ||
185 | 340 | def process_docstring(app, what, name, obj, options, lines): |
186 | 341 | if isinstance(obj, property): |
187 | 342 | obj = obj.fget |
190 | 345 | if what in ('class', 'exception'): |
191 | 346 | obj = getattr(obj, '__init__') |
192 | 347 | |
193 | obj = unwrap(obj) | |
194 | try: | |
195 | type_hints = get_type_hints(obj) | |
196 | except (AttributeError, TypeError): | |
197 | # Introspecting a slot wrapper will raise TypeError | |
198 | return | |
199 | except NameError as exc: | |
200 | logger.warning('Cannot resolve forward reference in type annotations of "%s": %s', | |
201 | name, exc) | |
202 | type_hints = obj.__annotations__ | |
348 | obj = inspect.unwrap(obj) | |
349 | type_hints = get_all_type_hints(obj, name) | |
203 | 350 | |
204 | 351 | for argname, annotation in type_hints.items(): |
352 | if argname == 'return': | |
353 | continue # this is handled separately later | |
205 | 354 | if argname.endswith('_'): |
206 | 355 | argname = '{}\\_'.format(argname[:-1]) |
207 | 356 | |
208 | formatted_annotation = format_annotation(annotation) | |
209 | ||
210 | if argname == 'return': | |
211 | if what in ('class', 'exception'): | |
212 | # Don't add return type None from __init__() | |
213 | continue | |
214 | ||
357 | formatted_annotation = format_annotation( | |
358 | annotation, fully_qualified=app.config.typehints_fully_qualified) | |
359 | ||
360 | searchfor = ':param {}:'.format(argname) | |
361 | insert_index = None | |
362 | ||
363 | for i, line in enumerate(lines): | |
364 | if line.startswith(searchfor): | |
365 | insert_index = i | |
366 | break | |
367 | ||
368 | if insert_index is None and app.config.always_document_param_types: | |
369 | lines.append(searchfor) | |
215 | 370 | insert_index = len(lines) |
216 | for i, line in enumerate(lines): | |
217 | if line.startswith(':rtype:'): | |
218 | insert_index = None | |
219 | break | |
220 | elif line.startswith(':return:') or line.startswith(':returns:'): | |
221 | insert_index = i | |
222 | ||
223 | if insert_index is not None: | |
224 | if insert_index == len(lines): | |
225 | # Ensure that :rtype: doesn't get joined with a paragraph of text, which | |
226 | # prevents it being interpreted. | |
227 | lines.append('') | |
228 | insert_index += 1 | |
229 | ||
230 | lines.insert(insert_index, ':rtype: {}'.format(formatted_annotation)) | |
231 | else: | |
232 | searchfor = ':param {}:'.format(argname) | |
233 | for i, line in enumerate(lines): | |
234 | if line.startswith(searchfor): | |
235 | lines.insert(i, ':type {}: {}'.format(argname, formatted_annotation)) | |
236 | break | |
371 | ||
372 | if insert_index is not None: | |
373 | lines.insert( | |
374 | insert_index, | |
375 | ':type {}: {}'.format(argname, formatted_annotation) | |
376 | ) | |
377 | ||
378 | if 'return' in type_hints and what not in ('class', 'exception'): | |
379 | formatted_annotation = format_annotation( | |
380 | type_hints['return'], fully_qualified=app.config.typehints_fully_qualified) | |
381 | ||
382 | insert_index = len(lines) | |
383 | for i, line in enumerate(lines): | |
384 | if line.startswith(':rtype:'): | |
385 | insert_index = None | |
386 | break | |
387 | elif line.startswith(':return:') or line.startswith(':returns:'): | |
388 | insert_index = i | |
389 | ||
390 | if insert_index is not None: | |
391 | if insert_index == len(lines): | |
392 | # Ensure that :rtype: doesn't get joined with a paragraph of text, which | |
393 | # prevents it being interpreted. | |
394 | lines.append('') | |
395 | insert_index += 1 | |
396 | ||
397 | lines.insert(insert_index, ':rtype: {}'.format(formatted_annotation)) | |
237 | 398 | |
238 | 399 | |
239 | 400 | def builder_ready(app): |
243 | 404 | |
244 | 405 | def setup(app): |
245 | 406 | app.add_config_value('set_type_checking_flag', False, 'html') |
407 | app.add_config_value('always_document_param_types', False, 'html') | |
408 | app.add_config_value('typehints_fully_qualified', False, 'env') | |
246 | 409 | app.connect('builder-inited', builder_ready) |
247 | 410 | app.connect('autodoc-process-signature', process_signature) |
248 | 411 | app.connect('autodoc-process-docstring', process_docstring) |
8 | 8 | collect_ignore = ['roots'] |
9 | 9 | |
10 | 10 | |
11 | @pytest.fixture(scope='session', autouse=True) | |
11 | @pytest.fixture(autouse=True) | |
12 | 12 | def remove_sphinx_projects(sphinx_test_tempdir): |
13 | 13 | # Remove any directory which appears to be a Sphinx project from |
14 | 14 | # the temporary directory area. |
0 | 0 | import typing |
1 | from typing import Callable, Union | |
2 | ||
3 | try: | |
4 | from dataclasses import dataclass | |
5 | except ImportError: | |
6 | def dataclass(cls): | |
7 | return cls | |
8 | ||
9 | ||
10 | def get_local_function(): | |
11 | def wrapper(self) -> str: | |
12 | """ | |
13 | Wrapper | |
14 | """ | |
15 | return wrapper | |
1 | 16 | |
2 | 17 | |
3 | 18 | class Class: |
86 | 101 | |
87 | 102 | :param x: foo |
88 | 103 | """ |
104 | ||
105 | locally_defined_callable_field = get_local_function() | |
89 | 106 | |
90 | 107 | |
91 | 108 | class DummyException(Exception): |
119 | 136 | """ |
120 | 137 | |
121 | 138 | |
122 | def function_with_unresolvable_annotation(x: 'a.b.c'): | |
123 | """ | |
124 | Function docstring. | |
125 | ||
126 | :param x: foo | |
127 | """ | |
139 | def function_with_unresolvable_annotation(x: 'a.b.c'): # noqa: F821 | |
140 | """ | |
141 | Function docstring. | |
142 | ||
143 | :param x: foo | |
144 | """ | |
145 | ||
146 | ||
147 | def function_with_typehint_comment( | |
148 | x, # type: int | |
149 | y # type: str | |
150 | ): | |
151 | # type: (...) -> None | |
152 | """ | |
153 | Function docstring. | |
154 | ||
155 | :param x: foo | |
156 | :param y: bar | |
157 | """ | |
158 | ||
159 | ||
160 | class ClassWithTypehints(object): | |
161 | """ | |
162 | Class docstring. | |
163 | ||
164 | :param x: foo | |
165 | """ | |
166 | ||
167 | def __init__( | |
168 | self, | |
169 | x # type: int | |
170 | ): | |
171 | # type: (...) -> None | |
172 | pass | |
173 | ||
174 | def foo( | |
175 | self, | |
176 | x # type: str | |
177 | ): | |
178 | # type: (...) -> int | |
179 | """ | |
180 | Method docstring. | |
181 | ||
182 | :param x: foo | |
183 | """ | |
184 | return 42 | |
185 | ||
186 | ||
187 | def function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs): | |
188 | # type: (Union[str, bytes], *str, bytes, **int) -> None | |
189 | """ | |
190 | Function docstring. | |
191 | ||
192 | :param x: foo | |
193 | :param y: bar | |
194 | :param z: baz | |
195 | :param kwargs: some kwargs | |
196 | """ | |
197 | ||
198 | ||
199 | class ClassWithTypehintsNotInline(object): | |
200 | """ | |
201 | Class docstring. | |
202 | ||
203 | :param x: foo | |
204 | """ | |
205 | ||
206 | def __init__(self, x=None): | |
207 | # type: (Callable[[int, bytes], int]) -> None | |
208 | pass | |
209 | ||
210 | def foo(self, x=1): | |
211 | # type: (Callable[[int, bytes], int]) -> int | |
212 | """ | |
213 | Method docstring. | |
214 | ||
215 | :param x: foo | |
216 | """ | |
217 | return x(1, b'') | |
218 | ||
219 | @classmethod | |
220 | def mk(cls, x=None): | |
221 | # type: (Callable[[int, bytes], int]) -> ClassWithTypehintsNotInline | |
222 | """ | |
223 | Method docstring. | |
224 | ||
225 | :param x: foo | |
226 | """ | |
227 | return cls(x) | |
228 | ||
229 | ||
230 | def undocumented_function(x: int) -> str: | |
231 | """Hi""" | |
232 | ||
233 | return str(x) | |
234 | ||
235 | ||
236 | @dataclass | |
237 | class DataClass: | |
238 | """Class docstring.""" |
15 | 15 | .. autofunction:: dummy_module.function_with_escaped_default |
16 | 16 | |
17 | 17 | .. autofunction:: dummy_module.function_with_unresolvable_annotation |
18 | ||
19 | .. autofunction:: dummy_module.function_with_typehint_comment | |
20 | ||
21 | .. autoclass:: dummy_module.ClassWithTypehints | |
22 | :members: | |
23 | ||
24 | .. autofunction:: dummy_module.function_with_typehint_comment_not_inline | |
25 | ||
26 | .. autoclass:: dummy_module.ClassWithTypehintsNotInline | |
27 | :members: | |
28 | ||
29 | .. autofunction:: dummy_module.undocumented_function | |
30 | ||
31 | .. autoclass:: dummy_module.DataClass | |
32 | :undoc-members: | |
33 | :special-members: __init__ |
9 | 9 | |
10 | 10 | from sphinx_autodoc_typehints import format_annotation, process_docstring |
11 | 11 | |
12 | try: | |
13 | from typing import ClassVar # not available prior to Python 3.5.3 | |
14 | except ImportError: | |
15 | ClassVar = None | |
16 | ||
17 | try: | |
18 | from typing import NoReturn # not available prior to Python 3.6.5 | |
19 | except ImportError: | |
20 | NoReturn = None | |
21 | ||
12 | 22 | T = TypeVar('T') |
13 | 23 | U = TypeVar('U', covariant=True) |
14 | 24 | V = TypeVar('V', contravariant=True) |
44 | 54 | (str, ':py:class:`str`'), |
45 | 55 | (int, ':py:class:`int`'), |
46 | 56 | (type(None), '``None``'), |
57 | pytest.param(NoReturn, ':py:data:`~typing.NoReturn`', | |
58 | marks=[pytest.mark.skipif(NoReturn is None, | |
59 | reason='typing.NoReturn is not available')]), | |
60 | pytest.param(ClassVar[str], ':py:data:`~typing.ClassVar`\\[:py:class:`str`]', | |
61 | marks=[pytest.mark.skipif(ClassVar is None, | |
62 | reason='typing.ClassVar is not available')]), | |
47 | 63 | (Any, ':py:data:`~typing.Any`'), |
48 | 64 | (AnyStr, ':py:data:`~typing.AnyStr`'), |
49 | 65 | (Generic[T], ':py:class:`~typing.Generic`\\[\\~T]'), |
59 | 75 | (Dict[T, U], ':py:class:`~typing.Dict`\\[\\~T, \\+U]'), |
60 | 76 | (Dict[str, bool], ':py:class:`~typing.Dict`\\[:py:class:`str`, ' |
61 | 77 | ':py:class:`bool`]'), |
62 | (Tuple, ':py:class:`~typing.Tuple`'), | |
63 | (Tuple[str, bool], ':py:class:`~typing.Tuple`\\[:py:class:`str`, ' | |
64 | ':py:class:`bool`]'), | |
65 | (Tuple[int, int, int], ':py:class:`~typing.Tuple`\\[:py:class:`int`, ' | |
78 | (Tuple, ':py:data:`~typing.Tuple`'), | |
79 | (Tuple[str, bool], ':py:data:`~typing.Tuple`\\[:py:class:`str`, ' | |
80 | ':py:class:`bool`]'), | |
81 | (Tuple[int, int, int], ':py:data:`~typing.Tuple`\\[:py:class:`int`, ' | |
66 | 82 | ':py:class:`int`, :py:class:`int`]'), |
67 | (Tuple[str, ...], ':py:class:`~typing.Tuple`\\[:py:class:`str`, ...]'), | |
83 | (Tuple[str, ...], ':py:data:`~typing.Tuple`\\[:py:class:`str`, ...]'), | |
68 | 84 | (Union, ':py:data:`~typing.Union`'), |
69 | 85 | (Union[str, bool], ':py:data:`~typing.Union`\\[:py:class:`str`, ' |
70 | 86 | ':py:class:`bool`]'), |
98 | 114 | assert result == expected_result |
99 | 115 | |
100 | 116 | |
117 | @pytest.mark.parametrize('annotation, expected_result', [ | |
118 | (str, ':py:class:`str`'), | |
119 | (int, ':py:class:`int`'), | |
120 | (type(None), '``None``'), | |
121 | pytest.param(NoReturn, ':py:data:`typing.NoReturn`', | |
122 | marks=[pytest.mark.skipif(NoReturn is None, | |
123 | reason='typing.NoReturn is not available')]), | |
124 | pytest.param(ClassVar[str], ':py:data:`typing.ClassVar`\\[:py:class:`str`]', | |
125 | marks=[pytest.mark.skipif(ClassVar is None, | |
126 | reason='typing.ClassVar is not available')]), | |
127 | (Any, ':py:data:`typing.Any`'), | |
128 | (AnyStr, ':py:data:`typing.AnyStr`'), | |
129 | (Generic[T], ':py:class:`typing.Generic`\\[\\~T]'), | |
130 | (Mapping, ':py:class:`typing.Mapping`\\[\\~KT, \\+VT_co]'), | |
131 | (Mapping[T, int], ':py:class:`typing.Mapping`\\[\\~T, :py:class:`int`]'), | |
132 | (Mapping[str, V], ':py:class:`typing.Mapping`\\[:py:class:`str`, \\-V]'), | |
133 | (Mapping[T, U], ':py:class:`typing.Mapping`\\[\\~T, \\+U]'), | |
134 | (Mapping[str, bool], ':py:class:`typing.Mapping`\\[:py:class:`str`, ' | |
135 | ':py:class:`bool`]'), | |
136 | (Dict, ':py:class:`typing.Dict`\\[\\~KT, \\~VT]'), | |
137 | (Dict[T, int], ':py:class:`typing.Dict`\\[\\~T, :py:class:`int`]'), | |
138 | (Dict[str, V], ':py:class:`typing.Dict`\\[:py:class:`str`, \\-V]'), | |
139 | (Dict[T, U], ':py:class:`typing.Dict`\\[\\~T, \\+U]'), | |
140 | (Dict[str, bool], ':py:class:`typing.Dict`\\[:py:class:`str`, ' | |
141 | ':py:class:`bool`]'), | |
142 | (Tuple, ':py:data:`typing.Tuple`'), | |
143 | (Tuple[str, bool], ':py:data:`typing.Tuple`\\[:py:class:`str`, ' | |
144 | ':py:class:`bool`]'), | |
145 | (Tuple[int, int, int], ':py:data:`typing.Tuple`\\[:py:class:`int`, ' | |
146 | ':py:class:`int`, :py:class:`int`]'), | |
147 | (Tuple[str, ...], ':py:data:`typing.Tuple`\\[:py:class:`str`, ...]'), | |
148 | (Union, ':py:data:`typing.Union`'), | |
149 | (Union[str, bool], ':py:data:`typing.Union`\\[:py:class:`str`, ' | |
150 | ':py:class:`bool`]'), | |
151 | (Optional[str], ':py:data:`typing.Optional`\\[:py:class:`str`]'), | |
152 | (Callable, ':py:data:`typing.Callable`'), | |
153 | (Callable[..., int], ':py:data:`typing.Callable`\\[..., :py:class:`int`]'), | |
154 | (Callable[[int], int], ':py:data:`typing.Callable`\\[\\[:py:class:`int`], ' | |
155 | ':py:class:`int`]'), | |
156 | (Callable[[int, str], bool], ':py:data:`typing.Callable`\\[\\[:py:class:`int`, ' | |
157 | ':py:class:`str`], :py:class:`bool`]'), | |
158 | (Callable[[int, str], None], ':py:data:`typing.Callable`\\[\\[:py:class:`int`, ' | |
159 | ':py:class:`str`], ``None``]'), | |
160 | (Callable[[T], T], ':py:data:`typing.Callable`\\[\\[\\~T], \\~T]'), | |
161 | (Pattern, ':py:class:`typing.Pattern`\\[:py:data:`typing.AnyStr`]'), | |
162 | (Pattern[str], ':py:class:`typing.Pattern`\\[:py:class:`str`]'), | |
163 | (A, ':py:class:`%s.A`' % __name__), | |
164 | (B, ':py:class:`%s.B`\\[\\~T]' % __name__), | |
165 | (B[int], ':py:class:`%s.B`\\[:py:class:`int`]' % __name__), | |
166 | (C, ':py:class:`%s.C`' % __name__), | |
167 | (D, ':py:class:`%s.D`' % __name__), | |
168 | (E, ':py:class:`%s.E`\\[\\~T]' % __name__), | |
169 | (E[int], ':py:class:`%s.E`\\[:py:class:`int`]' % __name__), | |
170 | (W, ':py:func:`typing.NewType`\\(:py:data:`~W`, :py:class:`str`)') | |
171 | ]) | |
172 | def test_format_annotation_fully_qualified(annotation, expected_result): | |
173 | result = format_annotation(annotation, fully_qualified=True) | |
174 | assert result == expected_result | |
175 | ||
176 | ||
101 | 177 | @pytest.mark.parametrize('type_param, expected_result', [ |
102 | 178 | (None, ':py:class:`~typing.Type`\\[\\+CT'), |
103 | 179 | (A, ':py:class:`~typing.Type`\\[:py:class:`~%s.A`]' % __name__) |
114 | 190 | assert not lines |
115 | 191 | |
116 | 192 | |
193 | @pytest.mark.parametrize('always_document_param_types', [True, False]) | |
117 | 194 | @pytest.mark.sphinx('text', testroot='dummy') |
118 | def test_sphinx_output(app, status, warning): | |
195 | def test_sphinx_output(app, status, warning, always_document_param_types): | |
119 | 196 | test_path = pathlib.Path(__file__).parent |
120 | 197 | |
121 | 198 | # Add test directory to sys.path to allow imports of dummy module. |
122 | 199 | if str(test_path) not in sys.path: |
123 | 200 | sys.path.insert(0, str(test_path)) |
124 | 201 | |
202 | app.config.always_document_param_types = always_document_param_types | |
125 | 203 | app.build() |
126 | 204 | |
127 | 205 | assert 'build succeeded' in status.getvalue() # Build succeeded |
128 | 206 | |
129 | 207 | # There should be a warning about an unresolved forward reference |
130 | 208 | warnings = warning.getvalue().strip() |
131 | assert 'Cannot resolve forward reference in type annotations of ' in warnings | |
209 | assert 'Cannot resolve forward reference in type annotations of ' in warnings, warnings | |
210 | ||
211 | if always_document_param_types: | |
212 | undoc_params = ''' | |
213 | ||
214 | Parameters: | |
215 | **x** ("int") --''' | |
216 | ||
217 | else: | |
218 | undoc_params = "" | |
132 | 219 | |
133 | 220 | text_path = pathlib.Path(app.srcdir) / '_build' / 'text' / 'index.txt' |
134 | 221 | with text_path.open('r') as f: |
135 | 222 | text_contents = f.read().replace('–', '--') |
136 | assert text_contents == textwrap.dedent('''\ | |
223 | expected_contents = textwrap.dedent('''\ | |
137 | 224 | Dummy Module |
138 | 225 | ************ |
139 | 226 | |
230 | 317 | Return type: |
231 | 318 | "str" |
232 | 319 | |
233 | a_property | |
320 | property a_property | |
234 | 321 | |
235 | 322 | Property docstring |
236 | 323 | |
247 | 334 | * **y** ("int") – bar |
248 | 335 | |
249 | 336 | * **z** ("Optional"["str"]) – baz |
337 | ||
338 | Return type: | |
339 | "str" | |
340 | ||
341 | locally_defined_callable_field() -> str | |
342 | ||
343 | Wrapper | |
250 | 344 | |
251 | 345 | Return type: |
252 | 346 | "str" |
288 | 382 | |
289 | 383 | Parameters: |
290 | 384 | **x** (*a.b.c*) – foo |
291 | ''').replace('–', '--') | |
385 | ||
386 | dummy_module.function_with_typehint_comment(x, y) | |
387 | ||
388 | Function docstring. | |
389 | ||
390 | Parameters: | |
391 | * **x** ("int") – foo | |
392 | ||
393 | * **y** ("str") – bar | |
394 | ||
395 | Return type: | |
396 | "None" | |
397 | ||
398 | class dummy_module.ClassWithTypehints(x) | |
399 | ||
400 | Class docstring. | |
401 | ||
402 | Parameters: | |
403 | **x** ("int") -- foo | |
404 | ||
405 | foo(x) | |
406 | ||
407 | Method docstring. | |
408 | ||
409 | Parameters: | |
410 | **x** ("str") -- foo | |
411 | ||
412 | Return type: | |
413 | "int" | |
414 | ||
415 | dummy_module.function_with_typehint_comment_not_inline(x=None, *y, z, **kwargs) | |
416 | ||
417 | Function docstring. | |
418 | ||
419 | Parameters: | |
420 | * **x** ("Union"["str", "bytes", "None"]) -- foo | |
421 | ||
422 | * **y** ("str") -- bar | |
423 | ||
424 | * **z** ("bytes") -- baz | |
425 | ||
426 | * **kwargs** ("int") -- some kwargs | |
427 | ||
428 | Return type: | |
429 | "None" | |
430 | ||
431 | class dummy_module.ClassWithTypehintsNotInline(x=None) | |
432 | ||
433 | Class docstring. | |
434 | ||
435 | Parameters: | |
436 | **x** ("Optional"["Callable"[["int", "bytes"], "int"]]) -- foo | |
437 | ||
438 | foo(x=1) | |
439 | ||
440 | Method docstring. | |
441 | ||
442 | Parameters: | |
443 | **x** ("Callable"[["int", "bytes"], "int"]) -- foo | |
444 | ||
445 | Return type: | |
446 | "int" | |
447 | ||
448 | classmethod mk(x=None) | |
449 | ||
450 | Method docstring. | |
451 | ||
452 | Parameters: | |
453 | **x** (*Callable**[**[**int**, **bytes**]**, **int**]*) -- | |
454 | foo | |
455 | ||
456 | Return type: | |
457 | ClassWithTypehintsNotInline | |
458 | ||
459 | dummy_module.undocumented_function(x) | |
460 | ||
461 | Hi{undoc_params} | |
462 | ||
463 | Return type: | |
464 | "str" | |
465 | ||
466 | class dummy_module.DataClass | |
467 | ||
468 | Class docstring. | |
469 | ||
470 | __init__() | |
471 | '''.format(undoc_params=undoc_params)).replace('–', '--') | |
472 | ||
473 | if sys.version_info < (3, 6): | |
474 | expected_contents += ''' | |
475 | Initialize self. See help(type(self)) for accurate signature. | |
476 | ''' | |
477 | else: | |
478 | expected_contents += ''' | |
479 | Return type: | |
480 | "None" | |
481 | ''' | |
482 | ||
483 | assert text_contents == expected_contents |
0 | 0 | [tox] |
1 | minversion = 2.5.0 | |
2 | envlist = py34, py35, py36, py37, flake8 | |
1 | minversion = 3.3.0 | |
2 | envlist = py35, py36, py37, py38, flake8 | |
3 | 3 | skip_missing_interpreters = true |
4 | isolated_build = true | |
4 | 5 | |
5 | 6 | [testenv] |
6 | extras = test | |
7 | extras = test, type_comments | |
7 | 8 | commands = python -m pytest {posargs} |
8 | 9 | |
9 | 10 | [testenv:flake8] |
10 | 11 | deps = flake8 |
11 | commands = flake8 sphinx_autodoc_typehints.py tests/test_sphinx_autodoc_typehints.py | |
12 | commands = flake8 sphinx_autodoc_typehints.py tests | |
12 | 13 | skip_install = true |