Fixed type hints still showing when setting autodoc_typehints

Closes #273
This commit is contained in:
Ashley Whetter 2021-04-02 19:24:18 -07:00
parent 565c43d99b
commit bf8f50dc97
9 changed files with 165 additions and 101 deletions

View File

@ -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.

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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": [],

View File

@ -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``.

View File

@ -159,8 +159,10 @@ class C:
class D(C):
class Da:
...
class DB(Da):
...
...

View File

@ -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):

View File

@ -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