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>` * `#265 <https://github.com/readthedocs/sphinx-autoapi/issues/265>`
Can resolve the qualified paths of parameters to generics. 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 Trivial/Internal Changes
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
* Fixed `DeprecationWarning` for invalid escape sequence `\s` in tests. * Fixed ``DeprecationWarning`` for invalid escape sequence ``\s`` in tests.
* Fixed `FutureWarning` for `Node.traverse()` becoming an iterator instead of list. * Fixed ``FutureWarning`` for ``Node.traverse()`` becoming an iterator instead of list.
* New example implementation of `autoapi-skip-member` Sphinx event. * New example implementation of ``autoapi-skip-member`` Sphinx event.
* Can run tests with tox 4. * Can run tests with tox 4.

View File

@ -1,5 +1,4 @@
import builtins import builtins
import collections
import itertools import itertools
import re import re
import sys import sys
@ -114,7 +113,7 @@ def get_full_basenames(node):
:returns: The full names. :returns: The full names.
:rtype: iterable(str) :rtype: iterable(str)
""" """
for base, basename in zip(node.bases, node.basenames): for base in node.bases:
yield _resolve_annotation(base) yield _resolve_annotation(base)
@ -415,7 +414,9 @@ def _resolve_annotation(annotation):
elif isinstance(annotation, astroid.Subscript): elif isinstance(annotation, astroid.Subscript):
value = _resolve_annotation(annotation.value) value = _resolve_annotation(annotation.value)
if isinstance(annotation.slice, astroid.Tuple): 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: else:
slice_ = _resolve_annotation(annotation.slice) slice_ = _resolve_annotation(annotation.slice)
resolved = f"{value}[{slice_}]" resolved = f"{value}[{slice_}]"
@ -471,7 +472,7 @@ def _iter_args(args, annotations, defaults):
yield (name, format_annotation(annotation, arg.parent), default) 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 = [] result = []
positional_only_defaults = [] positional_only_defaults = []
positional_or_keyword_defaults = args_node.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 return result
def format_args(args_node): def get_return_annotation(node):
result = [] """Get the return annotation of a node.
:type node: astroid.nodes.FunctionDef
"""
return_annotation = None
args_info = _get_args_info(args_node) if node.returns:
for prefix, name, annotation, default in args_info: return_annotation = format_annotation(node.returns, node)
formatted = "{}{}{}{}".format( elif node.type_comment_returns:
prefix or "", return_annotation = format_annotation(node.type_comment_returns, node)
name or "",
": {}".format(annotation) if annotation else "",
(" = {}" if annotation else "={}").format(default) if default else "",
)
result.append(formatted)
return ", ".join(result) return return_annotation
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):

View File

@ -213,7 +213,7 @@ def _link_objs(value):
else: else:
result += "\\ " result += "\\ "
elif sub_target: elif sub_target:
result += f":py:obj:`{sub_target}`\ " result += f":py:obj:`{sub_target}`\\ "
# Strip off the extra "\ " # Strip off the extra "\ "
return result[:-2] return result[:-2]
@ -367,7 +367,7 @@ class PythonSphinxMapper(SphinxMapperBase):
options=self.app.config.autoapi_options, options=self.app.config.autoapi_options,
jinja_env=self.jinja_env, jinja_env=self.jinja_env,
app=self.app, app=self.app,
**kwargs **kwargs,
) )
obj.url_root = self.url_root obj.url_root = self.url_root
@ -395,5 +395,14 @@ class PythonSphinxMapper(SphinxMapperBase):
def _record_typehints(self, obj): def _record_typehints(self, obj):
if isinstance(obj, (PythonClass, PythonFunction, PythonMethod)): 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 = 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__) 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): class PythonPythonMapper(PythonMapperBase):
"""A base class for all types of representations of Python objects. """A base class for all types of representations of Python objects.
@ -31,7 +46,6 @@ class PythonPythonMapper(PythonMapperBase):
# Optional # Optional
self.children = [] self.children = []
self.args = obj.get("args")
self.docstring = obj["doc"] self.docstring = obj["doc"]
self.imported = "original_path" in obj self.imported = "original_path" in obj
self.inherited = obj.get("inherited", False) self.inherited = obj.get("inherited", False)
@ -45,21 +59,6 @@ class PythonPythonMapper(PythonMapperBase):
self._display_cache = None # type: Optional[bool] 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 @property
def docstring(self): def docstring(self):
"""The docstring for this object. """The docstring for this object.
@ -170,7 +169,11 @@ class PythonFunction(PythonPythonMapper):
def __init__(self, obj, **kwargs): def __init__(self, obj, **kwargs):
super(PythonFunction, self).__init__(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. """The type annotation for the return type of this function.
This will be ``None`` if an annotation This will be ``None`` if an annotation
@ -185,12 +188,31 @@ class PythonFunction(PythonPythonMapper):
:type: list(str) :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. """The list of overloaded signatures ``[(args, return_annotation), ...]`` of this function.
:type: list(tuple(str, str)) :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): class PythonMethod(PythonFunction):
"""The representation of a method.""" """The representation of a method."""
@ -241,7 +263,7 @@ class PythonData(PythonPythonMapper):
:type: str or None :type: str or None
""" """
self.annotation = obj.get("annotation", obj.get("return_annotation")) self.annotation = obj.get("annotation")
"""The type annotation of this attribute. """The type annotation of this attribute.
This will be ``None`` if an annotation This will be ``None`` if an annotation
@ -323,6 +345,8 @@ class PythonClass(PythonPythonMapper):
def __init__(self, obj, **kwargs): def __init__(self, obj, **kwargs):
super(PythonClass, self).__init__(obj, **kwargs) super(PythonClass, self).__init__(obj, **kwargs)
self.args = obj["args"]
self.bases = obj["bases"] self.bases = obj["bases"]
"""The fully qualified names of all base classes. """The fully qualified names of all base classes.
@ -331,6 +355,10 @@ class PythonClass(PythonPythonMapper):
@property @property
def args(self): def args(self):
"""The arguments to this object, formatted as a string.
:type: str
"""
args = self._args args = self._args
constructor = self.constructor constructor = self.constructor

View File

@ -86,16 +86,14 @@ class Parser(object):
if astroid_utils.is_exception(node): if astroid_utils.is_exception(node):
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 = astroid_utils.format_args(constructor.args) args = astroid_utils.get_args_info(constructor.args)
annotations = astroid_utils.get_annotations_dict(constructor.args)
basenames = list(astroid_utils.get_full_basenames(node)) basenames = list(astroid_utils.get_full_basenames(node))
@ -104,7 +102,6 @@ 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,
@ -170,29 +167,15 @@ 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
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 = { data = {
"type": type_, "type": type_,
"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": astroid_utils.get_args_info(node.args),
"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,
"return_annotation": return_annotation, "return_annotation": astroid_utils.get_return_annotation(node),
"properties": properties, "properties": properties,
"is_overload": astroid_utils.is_decorated_with_overload(node), "is_overload": astroid_utils.is_decorated_with_overload(node),
"overloads": [], "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 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` Since v3.0, :mod:`sphinx` has included an :mod:`sphinx.ext.autodoc.typehints`
extension that is capable of rendering type annotations as extension that is capable of rendering type annotations as
parameter types and return types. parameter types and return types.
For example the following ReST: For example the following function:
.. code-block:: .. 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 a: The first arg.
:param b: The second arg. :param b: The second arg.
:returns: Something. :returns: Something.
"""
would be rendered as: would be rendered as:
.. py:function:: _func(a: int, b: Optional[str]) -> bool .. py:function:: _func(a, b)
:noindex: :noindex:
:param int a: The first arg. :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'] extensions = ['sphinx.ext.autodoc', 'autoapi.extension']
autodoc_typehints = 'description' 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 D(C):
class Da: class Da:
... ...
class DB(Da): class DB(Da):
... ...
... ...

View File

@ -74,7 +74,17 @@ class PythonParserTests(unittest.TestCase):
" return True\n" " return True\n"
) )
data = self.parse(source)[0] 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): def test_advanced_arguments(self):
"""Advanced argument parsing""" """Advanced argument parsing"""
@ -88,23 +98,21 @@ class PythonParserTests(unittest.TestCase):
data = self.parse(source)[0] data = self.parse(source)[0]
self.assertEqual( self.assertEqual(
data["args"], data["args"],
", ".join(
[ [
"self", (None, "self", None, None),
"a", (None, "a", None, None),
"b", (None, "b", None, None),
"c=42", (None, "c", None, "42"),
"d='string'", (None, "d", None, "'string'"),
"e=(1, 2)", (None, "e", None, "(1, 2)"),
"f={'a': True}", (None, "f", None, "{'a': True}"),
"g=None", (None, "g", None, "None"),
"h=[1, 2, 3, 4]", (None, "h", None, "[1, 2, 3, 4]"),
"i=dict(a=True)", (None, "i", None, "dict(a=True)"),
"j=False", (None, "j", None, "False"),
"*args", ("*", "args", None, None),
"**kwargs", ("**", "kwargs", None, None),
] ],
),
) )
def test_dict_key_assignment(self): def test_dict_key_assignment(self):

View File

@ -1,7 +1,7 @@
import sys import sys
import astroid import astroid
from autoapi.mappers.python import astroid_utils from autoapi.mappers.python import astroid_utils, objects
import pytest import pytest
@ -105,24 +105,45 @@ class TestAstroidUtils(object):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"signature,expected", "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( pytest.param(
"a: bool, /, b: int, *, c: str", "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( marks=pytest.mark.skipif(
sys.version_info[:2] < (3, 8), reason="Uses Python 3.8+ syntax" sys.version_info[:2] < (3, 8), reason="Uses Python 3.8+ syntax"
), ),
), ),
pytest.param( pytest.param(
"a: bool, /, b: int, *args, c: str, **kwargs", "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( marks=pytest.mark.skipif(
sys.version_info[:2] < (3, 8), reason="Uses Python 3.8+ syntax" sys.version_info[:2] < (3, 8), reason="Uses Python 3.8+ syntax"
), ),
), ),
pytest.param( pytest.param(
"a: int, *args, b: str, **kwargs", "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( marks=pytest.mark.skipif(
sys.version_info[:2] < (3, 8), reason="Uses Python 3.8+ syntax" 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 assert annotations == expected
@pytest.mark.parametrize( @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 assert formatted == expected