Codebase list sphinx-autodoc-typehints / aa07135
New upstream version 1.8.0 William Grzybowski 4 years ago
12 changed file(s) with 673 addition(s) and 125 deletion(s). Raw diff Collapse all Expand all
1313 env: TOXENV=flake8
1414
1515 - stage: test
16 env: TOXENV=py34
17 python: "3.4"
16 env: TOXENV=py35
17 python: "3.5"
1818 after_success: &after_success
1919 - pip install coveralls
2020 - coveralls
21
22 - stage: test
23 env: TOXENV=py35
24 python: "3.5"
25 after_success: *after_success
2621
2722 - stage: test
2823 env: TOXENV=py36
3429 python: "3.7"
3530 after_success: *after_success
3631
32 - stage: test
33 env: TOXENV=py38
34 python: "3.8-dev"
35 after_success: *after_success
36
3737 - stage: deploy to pypi
3838 install: skip
3939 script: skip
4141 provider: pypi
4242 user: agronholm
4343 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=
4545 distributions: sdist bdist_wheel
4646 on:
4747 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
021 1.6.0
122 =====
223
5454
5555 The following configuration options are accepted:
5656
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
5858 ``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.
5965
6066
6167 How it works
9298 ``def methodname(self, param1: 'othermodule.OtherClass'):``)
9399
94100 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/
00 [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'
55 author_email = alex.gronholm@nextday.fi
66 license = MIT
77 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
1011 classifiers =
1112 Development Status :: 5 - Production/Stable
1213 Framework :: Sphinx :: Extension
1617 Topic :: Documentation :: Sphinx
1718 Programming Language :: Python
1819 Programming Language :: Python :: 3
19 Programming Language :: Python :: 3.4
2020 Programming Language :: Python :: 3.5
2121 Programming Language :: Python :: 3.6
2222 Programming Language :: Python :: 3.7
23 Programming Language :: Python :: 3.8
2324
2425 [options]
2526 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
3029
3130 [options.extras_require]
3231 test =
3332 pytest >= 3.1.0
3433 typing_extensions >= 3.5
34 dataclasses; python_version == "3.6"
35 type_comments =
36 typed_ast >= 1.4.0; python_version < "3.8"
3537
3638 [flake8]
3739 max-line-length = 99
3840
3941 [tool:pytest]
40 testpaths = tests/
42 testpaths = tests
00 from setuptools import setup
11
22 setup(
3 use_scm_version=True,
3 use_scm_version={
4 'version_scheme': 'post-release',
5 'local_scheme': 'dirty-tag'
6 },
47 setup_requires=[
58 'setuptools_scm >= 1.7.0',
69 'setuptools >= 36.2.7'
00 import inspect
1 import sys
2 import textwrap
13 import typing
24 from typing import get_type_hints, TypeVar, Any, AnyStr, Generic, Union
35
911 except ImportError:
1012 Protocol = None
1113
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
3314 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):
3720 if inspect.isclass(annotation) and annotation.__module__ == 'builtins':
3821 if annotation.__qualname__ == 'NoneType':
3922 return '``None``'
4124 return ':py:class:`{}`'.format(annotation.__qualname__)
4225
4326 annotation_cls = annotation if inspect.isclass(annotation) else type(annotation)
44 class_name = None
4527 if annotation_cls.__module__ == 'typing':
28 class_name = str(annotation).split('[')[0].split('.')[-1]
4629 params = None
47 prefix = ':py:class:'
4830 module = 'typing'
4931 extra = ''
5032
51 if inspect.isclass(getattr(annotation, '__origin__', None)):
33 origin = getattr(annotation, '__origin__', None)
34 if inspect.isclass(origin):
5235 annotation_cls = annotation.__origin__
5336 try:
5437 mro = annotation_cls.mro()
5841 pass # annotation_cls was either the "type" object or typing.Type
5942
6043 if annotation is Any:
61 return ':py:data:`~typing.Any`'
44 return ':py:data:`{}typing.Any`'.format("" if fully_qualified else "~")
6245 elif annotation is AnyStr:
63 return ':py:data:`~typing.AnyStr`'
46 return ':py:data:`{}typing.AnyStr`'.format("" if fully_qualified else "~")
6447 elif isinstance(annotation, TypeVar):
6548 return '\\%r' % annotation
6649 elif (annotation is Union or getattr(annotation, '__origin__', None) is Union or
6750 hasattr(annotation, '__union_params__')):
68 prefix = ':py:data:'
69 class_name = 'Union'
7051 if hasattr(annotation, '__union_params__'):
7152 params = annotation.__union_params__
7253 elif hasattr(annotation, '__args__'):
8162 if annotation.__tuple_use_ellipsis__:
8263 params += (Ellipsis,)
8364 elif annotation_cls.__qualname__ == 'Callable':
84 prefix = ':py:data:'
8565 arg_annotations = result_annotation = None
8666 if hasattr(annotation, '__result__'):
8767 arg_annotations = annotation.__args__
9575 elif arg_annotations is not None:
9676 params = [
9777 '\\[{}]'.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)),
9981 result_annotation
10082 ]
83 elif str(annotation).startswith('typing.ClassVar[') and hasattr(annotation, '__type__'):
84 # < py3.7
85 params = (annotation.__type__,)
10186 elif hasattr(annotation, 'type_var'):
10287 # Type alias
10388 class_name = annotation.name
10893 params = annotation.__parameters__
10994
11095 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 )
117106 elif annotation is Ellipsis:
118107 return '...'
119108 elif (inspect.isfunction(annotation) and annotation.__module__ == 'typing' and
120109 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 )
123115 elif inspect.isclass(annotation) or inspect.isclass(getattr(annotation, '__origin__', None)):
124116 if not inspect.isclass(annotation):
125117 annotation_cls = annotation.__origin__
130122 params = (getattr(annotation, '__parameters__', None) or
131123 getattr(annotation, '__args__', None))
132124 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 )
137134
138135 return str(annotation)
139136
148145 if not getattr(obj, '__annotations__', None):
149146 return
150147
151 obj = unwrap(obj)
148 obj = inspect.unwrap(obj)
152149 signature = Signature(obj)
153150 parameters = [
154151 param.replace(annotation=inspect.Parameter.empty)
155152 for param in signature.signature.parameters.values()
156153 ]
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
157160
158161 if parameters:
159162 if what in ('class', 'exception'):
171174 class_name = obj.__qualname__.split('.')[-2]
172175 method_name = "_{c}{m}".format(c=class_name, m=method_name)
173176
174 method_object = outer.__dict__[method_name]
177 method_object = outer.__dict__[method_name] if outer else obj
175178 if not isinstance(method_object, (classmethod, staticmethod)):
176179 del parameters[0]
177180
182185 return signature.format_args().replace('\\', '\\\\'), None
183186
184187
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
185340 def process_docstring(app, what, name, obj, options, lines):
186341 if isinstance(obj, property):
187342 obj = obj.fget
190345 if what in ('class', 'exception'):
191346 obj = getattr(obj, '__init__')
192347
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)
203350
204351 for argname, annotation in type_hints.items():
352 if argname == 'return':
353 continue # this is handled separately later
205354 if argname.endswith('_'):
206355 argname = '{}\\_'.format(argname[:-1])
207356
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)
215370 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))
237398
238399
239400 def builder_ready(app):
243404
244405 def setup(app):
245406 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')
246409 app.connect('builder-inited', builder_ready)
247410 app.connect('autodoc-process-signature', process_signature)
248411 app.connect('autodoc-process-docstring', process_docstring)
88 collect_ignore = ['roots']
99
1010
11 @pytest.fixture(scope='session', autouse=True)
11 @pytest.fixture(autouse=True)
1212 def remove_sphinx_projects(sphinx_test_tempdir):
1313 # Remove any directory which appears to be a Sphinx project from
1414 # the temporary directory area.
00 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
116
217
318 class Class:
86101
87102 :param x: foo
88103 """
104
105 locally_defined_callable_field = get_local_function()
89106
90107
91108 class DummyException(Exception):
119136 """
120137
121138
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."""
1515 .. autofunction:: dummy_module.function_with_escaped_default
1616
1717 .. 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__
99
1010 from sphinx_autodoc_typehints import format_annotation, process_docstring
1111
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
1222 T = TypeVar('T')
1323 U = TypeVar('U', covariant=True)
1424 V = TypeVar('V', contravariant=True)
4454 (str, ':py:class:`str`'),
4555 (int, ':py:class:`int`'),
4656 (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')]),
4763 (Any, ':py:data:`~typing.Any`'),
4864 (AnyStr, ':py:data:`~typing.AnyStr`'),
4965 (Generic[T], ':py:class:`~typing.Generic`\\[\\~T]'),
5975 (Dict[T, U], ':py:class:`~typing.Dict`\\[\\~T, \\+U]'),
6076 (Dict[str, bool], ':py:class:`~typing.Dict`\\[:py:class:`str`, '
6177 ':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`, '
6682 ':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`, ...]'),
6884 (Union, ':py:data:`~typing.Union`'),
6985 (Union[str, bool], ':py:data:`~typing.Union`\\[:py:class:`str`, '
7086 ':py:class:`bool`]'),
98114 assert result == expected_result
99115
100116
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
101177 @pytest.mark.parametrize('type_param, expected_result', [
102178 (None, ':py:class:`~typing.Type`\\[\\+CT'),
103179 (A, ':py:class:`~typing.Type`\\[:py:class:`~%s.A`]' % __name__)
114190 assert not lines
115191
116192
193 @pytest.mark.parametrize('always_document_param_types', [True, False])
117194 @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):
119196 test_path = pathlib.Path(__file__).parent
120197
121198 # Add test directory to sys.path to allow imports of dummy module.
122199 if str(test_path) not in sys.path:
123200 sys.path.insert(0, str(test_path))
124201
202 app.config.always_document_param_types = always_document_param_types
125203 app.build()
126204
127205 assert 'build succeeded' in status.getvalue() # Build succeeded
128206
129207 # There should be a warning about an unresolved forward reference
130208 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 = ""
132219
133220 text_path = pathlib.Path(app.srcdir) / '_build' / 'text' / 'index.txt'
134221 with text_path.open('r') as f:
135222 text_contents = f.read().replace('–', '--')
136 assert text_contents == textwrap.dedent('''\
223 expected_contents = textwrap.dedent('''\
137224 Dummy Module
138225 ************
139226
230317 Return type:
231318 "str"
232319
233 a_property
320 property a_property
234321
235322 Property docstring
236323
247334 * **y** ("int") – bar
248335
249336 * **z** ("Optional"["str"]) – baz
337
338 Return type:
339 "str"
340
341 locally_defined_callable_field() -> str
342
343 Wrapper
250344
251345 Return type:
252346 "str"
288382
289383 Parameters:
290384 **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
00 [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
33 skip_missing_interpreters = true
4 isolated_build = true
45
56 [testenv]
6 extras = test
7 extras = test, type_comments
78 commands = python -m pytest {posargs}
89
910 [testenv:flake8]
1011 deps = flake8
11 commands = flake8 sphinx_autodoc_typehints.py tests/test_sphinx_autodoc_typehints.py
12 commands = flake8 sphinx_autodoc_typehints.py tests
1213 skip_install = true