mirror of
https://github.com/readthedocs/sphinx-autoapi
synced 2024-11-10 01:10:27 +00:00
Fixed type hints still showing when setting autodoc_typehints
Closes #273
This commit is contained in:
parent
565c43d99b
commit
bf8f50dc97
@ -14,12 +14,21 @@ Features
|
||||
* `#265 <https://github.com/readthedocs/sphinx-autoapi/issues/265>`
|
||||
Can resolve the qualified paths of parameters to generics.
|
||||
|
||||
Bug Fixes
|
||||
^^^^^^^^^
|
||||
|
||||
* `#273 <https://github.com/readthedocs/sphinx-autoapi/issues/273>`
|
||||
Fixed setting ``autodoc_typehints`` to ``none`` or ``description``
|
||||
not turning off signature type hints.
|
||||
``autodoc_typehints`` integration is consisidered experimental until
|
||||
the extension properly supports overload functions.
|
||||
|
||||
Trivial/Internal Changes
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
* Fixed `DeprecationWarning` for invalid escape sequence `\s` in tests.
|
||||
* Fixed `FutureWarning` for `Node.traverse()` becoming an iterator instead of list.
|
||||
* New example implementation of `autoapi-skip-member` Sphinx event.
|
||||
* Fixed ``DeprecationWarning`` for invalid escape sequence ``\s`` in tests.
|
||||
* Fixed ``FutureWarning`` for ``Node.traverse()`` becoming an iterator instead of list.
|
||||
* New example implementation of ``autoapi-skip-member`` Sphinx event.
|
||||
* Can run tests with tox 4.
|
||||
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import builtins
|
||||
import collections
|
||||
import itertools
|
||||
import re
|
||||
import sys
|
||||
@ -114,7 +113,7 @@ def get_full_basenames(node):
|
||||
:returns: The full names.
|
||||
:rtype: iterable(str)
|
||||
"""
|
||||
for base, basename in zip(node.bases, node.basenames):
|
||||
for base in node.bases:
|
||||
yield _resolve_annotation(base)
|
||||
|
||||
|
||||
@ -415,7 +414,9 @@ def _resolve_annotation(annotation):
|
||||
elif isinstance(annotation, astroid.Subscript):
|
||||
value = _resolve_annotation(annotation.value)
|
||||
if isinstance(annotation.slice, astroid.Tuple):
|
||||
slice_ = ", ".join(_resolve_annotation(elt) for elt in annotation.slice.elts)
|
||||
slice_ = ", ".join(
|
||||
_resolve_annotation(elt) for elt in annotation.slice.elts
|
||||
)
|
||||
else:
|
||||
slice_ = _resolve_annotation(annotation.slice)
|
||||
resolved = f"{value}[{slice_}]"
|
||||
@ -471,7 +472,7 @@ def _iter_args(args, annotations, defaults):
|
||||
yield (name, format_annotation(annotation, arg.parent), default)
|
||||
|
||||
|
||||
def _get_args_info(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 = []
|
||||
positional_only_defaults = []
|
||||
positional_or_keyword_defaults = args_node.defaults
|
||||
@ -570,31 +571,18 @@ def _get_args_info(args_node): # pylint: disable=too-many-branches,too-many-sta
|
||||
return result
|
||||
|
||||
|
||||
def format_args(args_node):
|
||||
result = []
|
||||
def get_return_annotation(node):
|
||||
"""Get the return annotation of a node.
|
||||
:type node: astroid.nodes.FunctionDef
|
||||
"""
|
||||
return_annotation = None
|
||||
|
||||
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)
|
||||
if node.returns:
|
||||
return_annotation = format_annotation(node.returns, node)
|
||||
elif node.type_comment_returns:
|
||||
return_annotation = format_annotation(node.type_comment_returns, node)
|
||||
|
||||
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
|
||||
return return_annotation
|
||||
|
||||
|
||||
def get_func_docstring(node):
|
||||
|
@ -213,7 +213,7 @@ def _link_objs(value):
|
||||
else:
|
||||
result += "\\ "
|
||||
elif sub_target:
|
||||
result += f":py:obj:`{sub_target}`\ "
|
||||
result += f":py:obj:`{sub_target}`\\ "
|
||||
|
||||
# Strip off the extra "\ "
|
||||
return result[:-2]
|
||||
@ -367,7 +367,7 @@ class PythonSphinxMapper(SphinxMapperBase):
|
||||
options=self.app.config.autoapi_options,
|
||||
jinja_env=self.jinja_env,
|
||||
app=self.app,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
)
|
||||
obj.url_root = self.url_root
|
||||
|
||||
@ -395,5 +395,14 @@ class PythonSphinxMapper(SphinxMapperBase):
|
||||
|
||||
def _record_typehints(self, obj):
|
||||
if isinstance(obj, (PythonClass, PythonFunction, PythonMethod)):
|
||||
obj_annotations = {}
|
||||
for _, name, annotation, _ in obj.obj["args"]:
|
||||
if name and annotation:
|
||||
obj_annotations[name] = annotation
|
||||
|
||||
return_annotation = obj.obj.get("return_annotation")
|
||||
if return_annotation:
|
||||
obj_annotations["return"] = return_annotation
|
||||
|
||||
annotations = self.app.env.temp_data.setdefault("annotations", {})
|
||||
annotations[obj.id] = obj.obj["annotations"]
|
||||
annotations[obj.id] = obj_annotations
|
||||
|
@ -8,6 +8,21 @@ from ..base import PythonMapperBase
|
||||
LOGGER = sphinx.util.logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _format_args(args_info, include_annotations=True):
|
||||
result = []
|
||||
|
||||
for prefix, name, annotation, default in args_info:
|
||||
formatted = "{}{}{}{}".format(
|
||||
prefix or "",
|
||||
name or "",
|
||||
": {}".format(annotation) if annotation and include_annotations else "",
|
||||
(" = {}" if annotation else "={}").format(default) if default else "",
|
||||
)
|
||||
result.append(formatted)
|
||||
|
||||
return ", ".join(result)
|
||||
|
||||
|
||||
class PythonPythonMapper(PythonMapperBase):
|
||||
"""A base class for all types of representations of Python objects.
|
||||
|
||||
@ -31,7 +46,6 @@ class PythonPythonMapper(PythonMapperBase):
|
||||
|
||||
# Optional
|
||||
self.children = []
|
||||
self.args = obj.get("args")
|
||||
self.docstring = obj["doc"]
|
||||
self.imported = "original_path" in obj
|
||||
self.inherited = obj.get("inherited", False)
|
||||
@ -45,21 +59,6 @@ class PythonPythonMapper(PythonMapperBase):
|
||||
|
||||
self._display_cache = None # type: Optional[bool]
|
||||
|
||||
@property
|
||||
def args(self):
|
||||
"""The arguments to this object, formatted as a string.
|
||||
|
||||
This will only be set for a function, method, or class.
|
||||
For classes, this does not include ``self``.
|
||||
|
||||
:type: str or None
|
||||
"""
|
||||
return self._args
|
||||
|
||||
@args.setter
|
||||
def args(self, value):
|
||||
self._args = value
|
||||
|
||||
@property
|
||||
def docstring(self):
|
||||
"""The docstring for this object.
|
||||
@ -170,7 +169,11 @@ class PythonFunction(PythonPythonMapper):
|
||||
def __init__(self, obj, **kwargs):
|
||||
super(PythonFunction, self).__init__(obj, **kwargs)
|
||||
|
||||
self.return_annotation = obj["return_annotation"]
|
||||
autodoc_typehints = getattr(self.app.config, "autodoc_typehints", "signature")
|
||||
show_annotations = autodoc_typehints not in ("none", "description")
|
||||
self.args = _format_args(obj["args"], show_annotations)
|
||||
|
||||
self.return_annotation = obj["return_annotation"] if show_annotations else None
|
||||
"""The type annotation for the return type of this function.
|
||||
|
||||
This will be ``None`` if an annotation
|
||||
@ -185,12 +188,31 @@ class PythonFunction(PythonPythonMapper):
|
||||
|
||||
:type: list(str)
|
||||
"""
|
||||
self.overloads = obj["overloads"]
|
||||
self.overloads = (
|
||||
[
|
||||
(_format_args(args), return_annotation)
|
||||
for args, return_annotation in obj["overloads"]
|
||||
]
|
||||
if show_annotations
|
||||
else []
|
||||
)
|
||||
"""The list of overloaded signatures ``[(args, return_annotation), ...]`` of this function.
|
||||
|
||||
:type: list(tuple(str, str))
|
||||
"""
|
||||
|
||||
@property
|
||||
def args(self):
|
||||
"""The arguments to this object, formatted as a string.
|
||||
|
||||
:type: str
|
||||
"""
|
||||
return self._args
|
||||
|
||||
@args.setter
|
||||
def args(self, value):
|
||||
self._args = value
|
||||
|
||||
|
||||
class PythonMethod(PythonFunction):
|
||||
"""The representation of a method."""
|
||||
@ -241,7 +263,7 @@ class PythonData(PythonPythonMapper):
|
||||
|
||||
:type: str or None
|
||||
"""
|
||||
self.annotation = obj.get("annotation", obj.get("return_annotation"))
|
||||
self.annotation = obj.get("annotation")
|
||||
"""The type annotation of this attribute.
|
||||
|
||||
This will be ``None`` if an annotation
|
||||
@ -323,6 +345,8 @@ class PythonClass(PythonPythonMapper):
|
||||
def __init__(self, obj, **kwargs):
|
||||
super(PythonClass, self).__init__(obj, **kwargs)
|
||||
|
||||
self.args = obj["args"]
|
||||
|
||||
self.bases = obj["bases"]
|
||||
"""The fully qualified names of all base classes.
|
||||
|
||||
@ -331,6 +355,10 @@ class PythonClass(PythonPythonMapper):
|
||||
|
||||
@property
|
||||
def args(self):
|
||||
"""The arguments to this object, formatted as a string.
|
||||
|
||||
:type: str
|
||||
"""
|
||||
args = self._args
|
||||
|
||||
constructor = self.constructor
|
||||
|
@ -86,16 +86,14 @@ class Parser(object):
|
||||
if astroid_utils.is_exception(node):
|
||||
type_ = "exception"
|
||||
|
||||
args = ""
|
||||
annotations = {}
|
||||
args = []
|
||||
try:
|
||||
constructor = node.lookup("__init__")[1]
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
if isinstance(constructor, astroid.nodes.FunctionDef):
|
||||
args = astroid_utils.format_args(constructor.args)
|
||||
annotations = astroid_utils.get_annotations_dict(constructor.args)
|
||||
args = astroid_utils.get_args_info(constructor.args)
|
||||
|
||||
basenames = list(astroid_utils.get_full_basenames(node))
|
||||
|
||||
@ -104,7 +102,6 @@ class Parser(object):
|
||||
"name": node.name,
|
||||
"full_name": self._get_full_name(node.name),
|
||||
"args": args,
|
||||
"annotations": annotations,
|
||||
"bases": basenames,
|
||||
"doc": astroid_utils.get_class_docstring(node),
|
||||
"from_line_no": node.fromlineno,
|
||||
@ -170,29 +167,15 @@ class Parser(object):
|
||||
if isinstance(node, astroid.AsyncFunctionDef):
|
||||
properties.append("async")
|
||||
|
||||
annotations = astroid_utils.get_annotations_dict(node.args)
|
||||
return_annotation = None
|
||||
if node.returns:
|
||||
return_annotation = astroid_utils.format_annotation(node.returns, node)
|
||||
annotations["return"] = return_annotation
|
||||
elif node.type_comment_returns:
|
||||
return_annotation = astroid_utils.format_annotation(
|
||||
node.type_comment_returns, node
|
||||
)
|
||||
annotations["return"] = return_annotation
|
||||
|
||||
arg_string = astroid_utils.format_args(node.args)
|
||||
|
||||
data = {
|
||||
"type": type_,
|
||||
"name": node.name,
|
||||
"full_name": self._get_full_name(node.name),
|
||||
"args": arg_string,
|
||||
"annotations": annotations,
|
||||
"args": astroid_utils.get_args_info(node.args),
|
||||
"doc": astroid_utils.get_func_docstring(node),
|
||||
"from_line_no": node.fromlineno,
|
||||
"to_line_no": node.tolineno,
|
||||
"return_annotation": return_annotation,
|
||||
"return_annotation": astroid_utils.get_return_annotation(node),
|
||||
"properties": properties,
|
||||
"is_overload": astroid_utils.is_decorated_with_overload(node),
|
||||
"overloads": [],
|
||||
|
@ -100,24 +100,30 @@ you can disable AutoAPI altogether from your project.
|
||||
How to Include Type Annotations as Types in Rendered Docstrings
|
||||
---------------------------------------------------------------
|
||||
|
||||
.. warning::
|
||||
|
||||
This feature is experimental and may change or be removed in future versions.
|
||||
|
||||
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:
|
||||
For example the following function:
|
||||
|
||||
.. code-block::
|
||||
|
||||
.. py:function:: _func(a: int, b: Optional[str]) -> bool
|
||||
def _func(a: int, b: Optional[str]) -> bool
|
||||
"""My function.
|
||||
|
||||
: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
|
||||
.. py:function:: _func(a, b)
|
||||
:noindex:
|
||||
|
||||
:param int a: The first arg.
|
||||
@ -134,3 +140,12 @@ and set :confval:`autodoc_typehints` to ``description`` as normal::
|
||||
|
||||
extensions = ['sphinx.ext.autodoc', 'autoapi.extension']
|
||||
autodoc_typehints = 'description'
|
||||
|
||||
.. note::
|
||||
|
||||
The :mod:`sphinx.ext.autodoc.typehints` extension does not support overload functions.
|
||||
Overloads will not be output when :confval:`autodoc_typehints` is set to
|
||||
anything other than ``signature``.
|
||||
When a documented parameter names a parameter that is specified in only an overload,
|
||||
not the final function definition, the type will not be included in the description
|
||||
when :confval:`autodoc_typehints` is set to ``description``.
|
||||
|
@ -159,8 +159,10 @@ class C:
|
||||
class D(C):
|
||||
class Da:
|
||||
...
|
||||
|
||||
class DB(Da):
|
||||
...
|
||||
|
||||
...
|
||||
|
||||
|
||||
|
@ -74,7 +74,17 @@ class PythonParserTests(unittest.TestCase):
|
||||
" return True\n"
|
||||
)
|
||||
data = self.parse(source)[0]
|
||||
self.assertEqual(data["args"], "self, bar, baz=42, foo=True, *args, **kwargs")
|
||||
self.assertEqual(
|
||||
data["args"],
|
||||
[
|
||||
(None, "self", None, None),
|
||||
(None, "bar", None, None),
|
||||
(None, "baz", None, "42"),
|
||||
(None, "foo", None, "True"),
|
||||
("*", "args", None, None),
|
||||
("**", "kwargs", None, None),
|
||||
],
|
||||
)
|
||||
|
||||
def test_advanced_arguments(self):
|
||||
"""Advanced argument parsing"""
|
||||
@ -88,23 +98,21 @@ class PythonParserTests(unittest.TestCase):
|
||||
data = self.parse(source)[0]
|
||||
self.assertEqual(
|
||||
data["args"],
|
||||
", ".join(
|
||||
[
|
||||
"self",
|
||||
"a",
|
||||
"b",
|
||||
"c=42",
|
||||
"d='string'",
|
||||
"e=(1, 2)",
|
||||
"f={'a': True}",
|
||||
"g=None",
|
||||
"h=[1, 2, 3, 4]",
|
||||
"i=dict(a=True)",
|
||||
"j=False",
|
||||
"*args",
|
||||
"**kwargs",
|
||||
]
|
||||
),
|
||||
[
|
||||
(None, "self", None, None),
|
||||
(None, "a", None, None),
|
||||
(None, "b", None, None),
|
||||
(None, "c", None, "42"),
|
||||
(None, "d", None, "'string'"),
|
||||
(None, "e", None, "(1, 2)"),
|
||||
(None, "f", None, "{'a': True}"),
|
||||
(None, "g", None, "None"),
|
||||
(None, "h", None, "[1, 2, 3, 4]"),
|
||||
(None, "i", None, "dict(a=True)"),
|
||||
(None, "j", None, "False"),
|
||||
("*", "args", None, None),
|
||||
("**", "kwargs", None, None),
|
||||
],
|
||||
)
|
||||
|
||||
def test_dict_key_assignment(self):
|
||||
|
@ -1,7 +1,7 @@
|
||||
import sys
|
||||
|
||||
import astroid
|
||||
from autoapi.mappers.python import astroid_utils
|
||||
from autoapi.mappers.python import astroid_utils, objects
|
||||
import pytest
|
||||
|
||||
|
||||
@ -105,24 +105,45 @@ class TestAstroidUtils(object):
|
||||
@pytest.mark.parametrize(
|
||||
"signature,expected",
|
||||
[
|
||||
("a: bool, b: int = 5", {"a": "bool", "b": "int"}),
|
||||
(
|
||||
"a: bool, b: int = 5",
|
||||
[(None, "a", "bool", None), (None, "b", "int", "5")],
|
||||
),
|
||||
pytest.param(
|
||||
"a: bool, /, b: int, *, c: str",
|
||||
{"a": "bool", "b": "int", "c": "str"},
|
||||
[
|
||||
(None, "a", "bool", None),
|
||||
("/", None, None, None),
|
||||
(None, "b", "int", None),
|
||||
("*", None, None, None),
|
||||
(None, "c", "str", None),
|
||||
],
|
||||
marks=pytest.mark.skipif(
|
||||
sys.version_info[:2] < (3, 8), reason="Uses Python 3.8+ syntax"
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
"a: bool, /, b: int, *args, c: str, **kwargs",
|
||||
{"a": "bool", "b": "int", "c": "str"},
|
||||
[
|
||||
(None, "a", "bool", None),
|
||||
("/", None, None, None),
|
||||
(None, "b", "int", None),
|
||||
("*", "args", None, None),
|
||||
(None, "c", "str", None),
|
||||
("**", "kwargs", None, None),
|
||||
],
|
||||
marks=pytest.mark.skipif(
|
||||
sys.version_info[:2] < (3, 8), reason="Uses Python 3.8+ syntax"
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
"a: int, *args, b: str, **kwargs",
|
||||
{"a": "int", "b": "str"},
|
||||
[
|
||||
(None, "a", "int", None),
|
||||
("*", "args", None, None),
|
||||
(None, "b", "str", None),
|
||||
("**", "kwargs", None, None),
|
||||
],
|
||||
marks=pytest.mark.skipif(
|
||||
sys.version_info[:2] < (3, 8), reason="Uses Python 3.8+ syntax"
|
||||
),
|
||||
@ -139,7 +160,7 @@ class TestAstroidUtils(object):
|
||||
)
|
||||
)
|
||||
|
||||
annotations = astroid_utils.get_annotations_dict(node.args)
|
||||
annotations = astroid_utils.get_args_info(node.args)
|
||||
assert annotations == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -174,5 +195,6 @@ class TestAstroidUtils(object):
|
||||
)
|
||||
)
|
||||
|
||||
formatted = astroid_utils.format_args(node.args)
|
||||
args_info = astroid_utils.get_args_info(node.args)
|
||||
formatted = objects._format_args(args_info)
|
||||
assert formatted == expected
|
||||
|
Loading…
Reference in New Issue
Block a user