mirror of
https://github.com/readthedocs/sphinx-autoapi
synced 2024-11-16 00:12:55 +00:00
Can use type hints as parameter types and return types
This commit is contained in:
parent
8e4cd49e1a
commit
6f0299356a
@ -12,6 +12,13 @@ Breaking Changes
|
|||||||
* Dropped support for Python 2 and Sphinx 1.x/2.x.
|
* Dropped support for Python 2 and Sphinx 1.x/2.x.
|
||||||
Python 2 source code can still be parsed.
|
Python 2 source code can still be parsed.
|
||||||
|
|
||||||
|
Features
|
||||||
|
^^^^^^^^
|
||||||
|
|
||||||
|
* Added support for using type hints as parameter types and return types
|
||||||
|
via the ``sphinx.ext.autodoc.typehints`` extension.
|
||||||
|
|
||||||
|
|
||||||
V1.5.1 (2020-10-01)
|
V1.5.1 (2020-10-01)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import builtins
|
import builtins
|
||||||
|
import collections
|
||||||
import itertools
|
import itertools
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@ -402,41 +403,33 @@ def merge_annotations(annotations, comment_annotations):
|
|||||||
yield None
|
yield None
|
||||||
|
|
||||||
|
|
||||||
def _format_args(args, defaults=None, annotations=None):
|
def _format_annotation(annotation):
|
||||||
values = []
|
if annotation:
|
||||||
|
if isinstance(annotation, astroid.Const):
|
||||||
|
annotation = annotation.value
|
||||||
|
else:
|
||||||
|
annotation = annotation.as_string()
|
||||||
|
|
||||||
if args is None:
|
return annotation
|
||||||
return ""
|
|
||||||
|
|
||||||
if annotations is None:
|
|
||||||
annotations = []
|
|
||||||
|
|
||||||
if defaults is not None:
|
|
||||||
default_offset = len(args) - len(defaults)
|
|
||||||
|
|
||||||
|
def _iter_args(args, annotations, defaults):
|
||||||
|
default_offset = len(args) - len(defaults)
|
||||||
packed = itertools.zip_longest(args, annotations)
|
packed = itertools.zip_longest(args, annotations)
|
||||||
for i, (arg, annotation) in enumerate(packed):
|
for i, (arg, annotation) in enumerate(packed):
|
||||||
|
default = None
|
||||||
|
if defaults is not None and i >= default_offset:
|
||||||
|
if defaults[i - default_offset] is not None:
|
||||||
|
default = defaults[i - default_offset].as_string()
|
||||||
|
|
||||||
|
name = arg.name
|
||||||
if isinstance(arg, astroid.Tuple):
|
if isinstance(arg, astroid.Tuple):
|
||||||
values.append("({})".format(_format_args(arg.elts)))
|
name = "({})".format(", ".join(x.name for x in arg.elts))
|
||||||
else:
|
|
||||||
argname = arg.name
|
|
||||||
default_sep = "="
|
|
||||||
if annotation is not None:
|
|
||||||
ann_str = annotation.as_string()
|
|
||||||
if isinstance(annotation, astroid.Const):
|
|
||||||
ann_str = annotation.value
|
|
||||||
argname = "{}: {}".format(argname, ann_str)
|
|
||||||
default_sep = " = "
|
|
||||||
values.append(argname)
|
|
||||||
|
|
||||||
if defaults is not None and i >= default_offset:
|
yield (name, _format_annotation(annotation), default)
|
||||||
if defaults[i - default_offset] is not None:
|
|
||||||
values[-1] += default_sep + defaults[i - default_offset].as_string()
|
|
||||||
|
|
||||||
return ", ".join(values)
|
|
||||||
|
|
||||||
|
|
||||||
def format_args(args_node): # pylint: disable=too-many-branches,too-many-statements
|
def _get_args_info(args_node): # pylint: disable=too-many-branches,too-many-statements
|
||||||
result = []
|
result = []
|
||||||
positional_only_defaults = []
|
positional_only_defaults = []
|
||||||
positional_or_keyword_defaults = args_node.defaults
|
positional_or_keyword_defaults = args_node.defaults
|
||||||
@ -449,8 +442,8 @@ def format_args(args_node): # pylint: disable=too-many-branches,too-many-statem
|
|||||||
|
|
||||||
plain_annotations = args_node.annotations or ()
|
plain_annotations = args_node.annotations or ()
|
||||||
func_comment_annotations = args_node.parent.type_comment_args or ()
|
func_comment_annotations = args_node.parent.type_comment_args or ()
|
||||||
comment_annotations = args_node.type_comment_args or []
|
comment_annotations = args_node.type_comment_posonlyargs
|
||||||
comment_annotations = args_node.type_comment_posonlyargs + comment_annotations
|
comment_annotations += args_node.type_comment_args or []
|
||||||
comment_annotations += args_node.type_comment_kwonlyargs
|
comment_annotations += args_node.type_comment_kwonlyargs
|
||||||
annotations = list(
|
annotations = list(
|
||||||
merge_annotations(
|
merge_annotations(
|
||||||
@ -468,43 +461,39 @@ def format_args(args_node): # pylint: disable=too-many-branches,too-many-statem
|
|||||||
annotation_offset : annotation_offset + num_args
|
annotation_offset : annotation_offset + num_args
|
||||||
]
|
]
|
||||||
|
|
||||||
result.append(
|
for arg, annotation, default in _iter_args(
|
||||||
_format_args(
|
args_node.posonlyargs, posonlyargs_annotations, positional_only_defaults
|
||||||
args_node.posonlyargs, positional_only_defaults, posonlyargs_annotations
|
):
|
||||||
)
|
result.append((None, arg, annotation, default))
|
||||||
)
|
|
||||||
result.append("/")
|
result.append(("/", None, None, None))
|
||||||
|
|
||||||
if not any(args_node.posonlyargs_annotations):
|
if not any(args_node.posonlyargs_annotations):
|
||||||
annotation_offset += num_args
|
annotation_offset += num_args
|
||||||
|
|
||||||
if args_node.args:
|
if args_node.args:
|
||||||
num_args = len(args_node.args)
|
num_args = len(args_node.args)
|
||||||
result.append(
|
for arg, annotation, default in _iter_args(
|
||||||
_format_args(
|
args_node.args,
|
||||||
args_node.args,
|
annotations[annotation_offset : annotation_offset + num_args],
|
||||||
positional_or_keyword_defaults,
|
positional_or_keyword_defaults,
|
||||||
annotations[annotation_offset : annotation_offset + num_args],
|
):
|
||||||
)
|
result.append((None, arg, annotation, default))
|
||||||
)
|
|
||||||
annotation_offset += num_args
|
annotation_offset += num_args
|
||||||
|
|
||||||
if args_node.vararg:
|
if args_node.vararg:
|
||||||
vararg_result = "*{}".format(args_node.vararg)
|
annotation = None
|
||||||
if args_node.varargannotation:
|
if args_node.varargannotation:
|
||||||
vararg_result = "{}: {}".format(
|
annotation = _format_annotation(args_node.varargannotation)
|
||||||
vararg_result, args_node.varargannotation.as_string()
|
|
||||||
)
|
|
||||||
elif len(annotations) > annotation_offset and annotations[annotation_offset]:
|
elif len(annotations) > annotation_offset and annotations[annotation_offset]:
|
||||||
vararg_result = "{}: {}".format(
|
annotation = _format_annotation(annotations[annotation_offset])
|
||||||
vararg_result, annotations[annotation_offset].as_string()
|
|
||||||
)
|
|
||||||
annotation_offset += 1
|
annotation_offset += 1
|
||||||
result.append(vararg_result)
|
result.append(("*", args_node.vararg, annotation, None))
|
||||||
|
|
||||||
if args_node.kwonlyargs:
|
if args_node.kwonlyargs:
|
||||||
if not args_node.vararg:
|
if not args_node.vararg:
|
||||||
result.append("*")
|
result.append(("*", None, None, None))
|
||||||
|
|
||||||
kwonlyargs_annotations = args_node.kwonlyargs_annotations
|
kwonlyargs_annotations = args_node.kwonlyargs_annotations
|
||||||
if not any(args_node.kwonlyargs_annotations):
|
if not any(args_node.kwonlyargs_annotations):
|
||||||
@ -513,33 +502,55 @@ def format_args(args_node): # pylint: disable=too-many-branches,too-many-statem
|
|||||||
annotation_offset : annotation_offset + num_args
|
annotation_offset : annotation_offset + num_args
|
||||||
]
|
]
|
||||||
|
|
||||||
result.append(
|
for arg, annotation, default in _iter_args(
|
||||||
_format_args(
|
args_node.kwonlyargs,
|
||||||
args_node.kwonlyargs,
|
kwonlyargs_annotations,
|
||||||
args_node.kw_defaults,
|
args_node.kw_defaults,
|
||||||
kwonlyargs_annotations,
|
):
|
||||||
)
|
result.append((None, arg, annotation, default))
|
||||||
)
|
|
||||||
|
|
||||||
if not any(args_node.kwonlyargs_annotations):
|
if not any(args_node.kwonlyargs_annotations):
|
||||||
annotation_offset += num_args
|
annotation_offset += num_args
|
||||||
|
|
||||||
if args_node.kwarg:
|
if args_node.kwarg:
|
||||||
kwarg_result = "**{}".format(args_node.kwarg)
|
annotation = None
|
||||||
if args_node.kwargannotation:
|
if args_node.kwargannotation:
|
||||||
kwarg_result = "{}: {}".format(
|
annotation = _format_annotation(args_node.kwargannotation)
|
||||||
kwarg_result, args_node.kwargannotation.as_string()
|
|
||||||
)
|
|
||||||
elif len(annotations) > annotation_offset and annotations[annotation_offset]:
|
elif len(annotations) > annotation_offset and annotations[annotation_offset]:
|
||||||
kwarg_result = "{}: {}".format(
|
annotation = _format_annotation(annotations[annotation_offset])
|
||||||
kwarg_result, annotations[annotation_offset].as_string()
|
|
||||||
)
|
|
||||||
annotation_offset += 1
|
annotation_offset += 1
|
||||||
result.append(kwarg_result)
|
result.append(("**", args_node.kwarg, annotation, None))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def format_args(args_node):
|
||||||
|
result = []
|
||||||
|
|
||||||
|
args_info = _get_args_info(args_node)
|
||||||
|
for prefix, name, annotation, default in args_info:
|
||||||
|
formatted = "{}{}{}{}".format(
|
||||||
|
prefix or "",
|
||||||
|
name or "",
|
||||||
|
": {}".format(annotation) if annotation else "",
|
||||||
|
(" = {}" if annotation else "={}").format(default) if default else "",
|
||||||
|
)
|
||||||
|
result.append(formatted)
|
||||||
|
|
||||||
return ", ".join(result)
|
return ", ".join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def get_annotations_dict(args_node):
|
||||||
|
result = collections.OrderedDict()
|
||||||
|
|
||||||
|
args_info = _get_args_info(args_node)
|
||||||
|
for _, name, annotation, __ in args_info:
|
||||||
|
if name and annotation:
|
||||||
|
result[name] = annotation
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_func_docstring(node):
|
def get_func_docstring(node):
|
||||||
"""Get the docstring of a node, using a parent docstring if needed.
|
"""Get the docstring of a node, using a parent docstring if needed.
|
||||||
|
|
||||||
|
@ -269,7 +269,8 @@ class PythonSphinxMapper(SphinxMapperBase):
|
|||||||
parsed_data = Parser().parse_file(path)
|
parsed_data = Parser().parse_file(path)
|
||||||
return parsed_data
|
return parsed_data
|
||||||
except (IOError, TypeError, ImportError):
|
except (IOError, TypeError, ImportError):
|
||||||
LOGGER.warning("Error reading file: {0}".format(path))
|
LOGGER.warning("Unable to read file: {0}".format(path))
|
||||||
|
LOGGER.debug("Reason:", exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _resolve_placeholders(self):
|
def _resolve_placeholders(self):
|
||||||
@ -325,8 +326,9 @@ class PythonSphinxMapper(SphinxMapperBase):
|
|||||||
if lines and "autodoc-process-docstring" in self.app.events.events:
|
if lines and "autodoc-process-docstring" in self.app.events.events:
|
||||||
self.app.emit(
|
self.app.emit(
|
||||||
"autodoc-process-docstring", cls.type, obj.name, None, None, lines
|
"autodoc-process-docstring", cls.type, obj.name, None, None, lines
|
||||||
) # object # options
|
)
|
||||||
obj.docstring = "\n".join(lines)
|
obj.docstring = "\n".join(lines)
|
||||||
|
self._record_typehints(obj)
|
||||||
|
|
||||||
for child_data in data.get("children", []):
|
for child_data in data.get("children", []):
|
||||||
for child_obj in self.create_class(
|
for child_obj in self.create_class(
|
||||||
@ -341,3 +343,8 @@ class PythonSphinxMapper(SphinxMapperBase):
|
|||||||
obj.children.sort(key=lambda x: (x.member_order, x.name))
|
obj.children.sort(key=lambda x: (x.member_order, x.name))
|
||||||
|
|
||||||
yield obj
|
yield obj
|
||||||
|
|
||||||
|
def _record_typehints(self, obj):
|
||||||
|
if isinstance(obj, (PythonClass, PythonFunction, PythonMethod)):
|
||||||
|
annotations = self.app.env.temp_data.setdefault("annotations", {})
|
||||||
|
annotations[obj.id] = obj.obj["annotations"]
|
||||||
|
@ -87,13 +87,15 @@ class Parser(object):
|
|||||||
type_ = "exception"
|
type_ = "exception"
|
||||||
|
|
||||||
args = ""
|
args = ""
|
||||||
|
annotations = {}
|
||||||
try:
|
try:
|
||||||
constructor = node.lookup("__init__")[1]
|
constructor = node.lookup("__init__")[1]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if isinstance(constructor, astroid.nodes.FunctionDef):
|
if isinstance(constructor, astroid.nodes.FunctionDef):
|
||||||
args = constructor.args.as_string()
|
args = astroid_utils.format_args(constructor.args)
|
||||||
|
annotations = astroid_utils.get_annotations_dict(constructor.args)
|
||||||
|
|
||||||
basenames = list(astroid_utils.get_full_basenames(node.bases, node.basenames))
|
basenames = list(astroid_utils.get_full_basenames(node.bases, node.basenames))
|
||||||
|
|
||||||
@ -102,6 +104,7 @@ class Parser(object):
|
|||||||
"name": node.name,
|
"name": node.name,
|
||||||
"full_name": self._get_full_name(node.name),
|
"full_name": self._get_full_name(node.name),
|
||||||
"args": args,
|
"args": args,
|
||||||
|
"annotations": annotations,
|
||||||
"bases": basenames,
|
"bases": basenames,
|
||||||
"doc": astroid_utils.get_class_docstring(node),
|
"doc": astroid_utils.get_class_docstring(node),
|
||||||
"from_line_no": node.fromlineno,
|
"from_line_no": node.fromlineno,
|
||||||
@ -163,11 +166,14 @@ class Parser(object):
|
|||||||
if isinstance(node, astroid.AsyncFunctionDef):
|
if isinstance(node, astroid.AsyncFunctionDef):
|
||||||
properties.append("async")
|
properties.append("async")
|
||||||
|
|
||||||
|
annotations = astroid_utils.get_annotations_dict(node.args)
|
||||||
return_annotation = None
|
return_annotation = None
|
||||||
if node.returns:
|
if node.returns:
|
||||||
return_annotation = node.returns.as_string()
|
return_annotation = node.returns.as_string()
|
||||||
|
annotations["return"] = return_annotation
|
||||||
elif node.type_comment_returns:
|
elif node.type_comment_returns:
|
||||||
return_annotation = node.type_comment_returns.as_string()
|
return_annotation = node.type_comment_returns.as_string()
|
||||||
|
annotations["return"] = return_annotation
|
||||||
|
|
||||||
arg_string = astroid_utils.format_args(node.args)
|
arg_string = astroid_utils.format_args(node.args)
|
||||||
|
|
||||||
@ -176,6 +182,7 @@ class Parser(object):
|
|||||||
"name": node.name,
|
"name": node.name,
|
||||||
"full_name": self._get_full_name(node.name),
|
"full_name": self._get_full_name(node.name),
|
||||||
"args": arg_string,
|
"args": arg_string,
|
||||||
|
"annotations": annotations,
|
||||||
"doc": astroid_utils.get_func_docstring(node),
|
"doc": astroid_utils.get_func_docstring(node),
|
||||||
"from_line_no": node.fromlineno,
|
"from_line_no": node.fromlineno,
|
||||||
"to_line_no": node.tolineno,
|
"to_line_no": node.tolineno,
|
||||||
|
@ -95,3 +95,42 @@ using the :confval:`autoapi_keep_files` option::
|
|||||||
|
|
||||||
Once you have built your documentation with this option turned on,
|
Once you have built your documentation with this option turned on,
|
||||||
you can disable AutoAPI altogether from your project.
|
you can disable AutoAPI altogether from your project.
|
||||||
|
|
||||||
|
|
||||||
|
How to Include Type Annotations as Types in Rendered Docstrings
|
||||||
|
---------------------------------------------------------------
|
||||||
|
|
||||||
|
Since v3.0, :mod:`sphinx` has included an :mod:`sphinx.ext.autodoc.typehints`
|
||||||
|
extension that is capable of rendering type annotations as
|
||||||
|
parameter types and return types.
|
||||||
|
|
||||||
|
For example the following ReST:
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
.. py:function:: _func(a: int, b: Optional[str]) -> bool
|
||||||
|
|
||||||
|
:param a: The first arg.
|
||||||
|
:param b: The second arg.
|
||||||
|
|
||||||
|
:returns: Something.
|
||||||
|
|
||||||
|
would be rendered as:
|
||||||
|
|
||||||
|
.. py:function:: _func(a: int, b: Optional[str]) -> bool
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
:param int a: The first arg.
|
||||||
|
:param b: The second arg.
|
||||||
|
:type b: Optional[str]
|
||||||
|
|
||||||
|
:returns: Something.
|
||||||
|
:rtype: bool
|
||||||
|
|
||||||
|
AutoAPI is capable of the same thing.
|
||||||
|
To enable this behaviour, load the :mod:`sphinx.ext.autodoc.typehints`
|
||||||
|
(or :mod:`sphinx.ext.autodoc`) extension in Sphinx's ``conf.py`` file
|
||||||
|
and set :confval:`autodoc_typehints` to ``description`` as normal::
|
||||||
|
|
||||||
|
extensions = ['sphinx.ext.autodoc', 'autoapi.extension']
|
||||||
|
autodoc_typehints = 'description'
|
||||||
|
@ -119,7 +119,12 @@ class A:
|
|||||||
return a * 2
|
return a * 2
|
||||||
|
|
||||||
|
|
||||||
async def async_function(self, wait: bool) -> int:
|
async def async_function(wait: bool) -> int:
|
||||||
|
"""Blah.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wait: Blah
|
||||||
|
"""
|
||||||
if wait:
|
if wait:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
56
tests/python/test_astroid_utils.py
Normal file
56
tests/python/test_astroid_utils.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import astroid
|
||||||
|
from autoapi.mappers.python import astroid_utils
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"signature,expected",
|
||||||
|
[
|
||||||
|
("a: bool, b: int = 5", {"a": "bool", "b": "int"}),
|
||||||
|
("a: bool, /, b: int, *, c: str", {"a": "bool", "b": "int", "c": "str"}),
|
||||||
|
(
|
||||||
|
"a: bool, /, b: int, *args, c: str, **kwargs",
|
||||||
|
{"a": "bool", "b": "int", "c": "str"},
|
||||||
|
),
|
||||||
|
("a: int, *args, b: str, **kwargs", {"a": "int", "b": "str"}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parse_annotations(signature, expected):
|
||||||
|
node = astroid.extract_node(
|
||||||
|
"""
|
||||||
|
def func({}) -> str: #@
|
||||||
|
pass
|
||||||
|
""".format(
|
||||||
|
signature
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
annotations = astroid_utils.get_annotations_dict(node.args)
|
||||||
|
assert annotations == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"signature,expected",
|
||||||
|
[
|
||||||
|
("a: bool, b: int = 5, c='hi'", "a: bool, b: int = 5, c='hi'"),
|
||||||
|
("a: bool, /, b: int, *, c: str", "a: bool, /, b: int, *, c: str"),
|
||||||
|
(
|
||||||
|
"a: bool, /, b: int, *args, c: str, **kwargs",
|
||||||
|
"a: bool, /, b: int, *args, c: str, **kwargs",
|
||||||
|
),
|
||||||
|
("a: int, *args, b: str, **kwargs", "a: int, *args, b: str, **kwargs"),
|
||||||
|
("a: 'A'", "a: A"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_format_args(signature, expected):
|
||||||
|
node = astroid.extract_node(
|
||||||
|
"""
|
||||||
|
def func({}) -> str: #@
|
||||||
|
pass
|
||||||
|
""".format(
|
||||||
|
signature
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
formatted = astroid_utils.format_args(node.args)
|
||||||
|
assert formatted == expected
|
Loading…
Reference in New Issue
Block a user