Skip to content

Commit

Permalink
Merge branch '2.4.2' into 7161_autodoc.typhints_parallel_build
Browse files Browse the repository at this point in the history
  • Loading branch information
tk0miya committed Feb 16, 2020
2 parents 68e248f + 9f23744 commit 39a192b
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 15 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ Bugs fixed
* #7138: autodoc: ``autodoc.typehints`` crashed when variable has unbound object
as a value
* #7156: autodoc: separator for keyword only arguments is not shown
* #7146: autodoc: IndexError is raised on suppressed type_comment found
* #7161: autodoc: typehints extension does not support parallel build
* #7151: crashed when extension assigns a value to ``env.indexentries``

Testing
--------
Expand Down
8 changes: 8 additions & 0 deletions sphinx/environment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,14 @@ def indexentries(self) -> Dict[str, List[Tuple[str, str, str, str, str]]]:
domain = cast(IndexDomain, self.get_domain('index'))
return domain.entries

@indexentries.setter
def indexentries(self, entries: Dict[str, List[Tuple[str, str, str, str, str]]]) -> None:
warnings.warn('env.indexentries() is deprecated. Please use IndexDomain instead.',
RemovedInSphinx40Warning, stacklevel=2)
from sphinx.domains.index import IndexDomain
domain = cast(IndexDomain, self.get_domain('index'))
domain.data['entries'] = entries


from sphinx.errors import NoUri # NOQA

Expand Down
93 changes: 78 additions & 15 deletions sphinx/ext/autodoc/type_comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
:license: BSD, see LICENSE for details.
"""

import ast
from inspect import getsource
from typing import Any, Dict
from inspect import Parameter, Signature, getsource
from typing import Any, Dict, List
from typing import cast

import sphinx
from sphinx.application import Sphinx
from sphinx.pycode.ast import ast
from sphinx.pycode.ast import parse as ast_parse
from sphinx.pycode.ast import unparse as ast_unparse
from sphinx.util import inspect
Expand All @@ -23,11 +23,73 @@
logger = logging.getLogger(__name__)


def get_type_comment(obj: Any) -> ast.FunctionDef:
def not_suppressed(argtypes: List[ast.AST] = []) -> bool:
"""Check given *argtypes* is suppressed type_comment or not."""
if len(argtypes) == 0: # no argtypees
return False
elif len(argtypes) == 1 and ast_unparse(argtypes[0]) == "...": # suppressed
# Note: To support multiple versions of python, this uses ``ast_unparse()`` for
# comparison with Ellipsis. Since 3.8, ast.Constant has been used to represent
# Ellipsis node instead of ast.Ellipsis.
return False
else: # not suppressed
return True


def signature_from_ast(node: ast.FunctionDef, bound_method: bool,
type_comment: ast.FunctionDef) -> Signature:
"""Return a Signature object for the given *node*.
:param bound_method: Specify *node* is a bound method or not
"""
params = []
if hasattr(node.args, "posonlyargs"): # for py38+
for arg in node.args.posonlyargs: # type: ignore
param = Parameter(arg.arg, Parameter.POSITIONAL_ONLY, annotation=arg.type_comment)
params.append(param)

for arg in node.args.args:
param = Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD,
annotation=arg.type_comment or Parameter.empty)
params.append(param)

if node.args.vararg:
param = Parameter(node.args.vararg.arg, Parameter.VAR_POSITIONAL,
annotation=arg.type_comment or Parameter.empty)
params.append(param)

for arg in node.args.kwonlyargs:
param = Parameter(arg.arg, Parameter.KEYWORD_ONLY,
annotation=arg.type_comment or Parameter.empty)
params.append(param)

if node.args.kwarg:
param = Parameter(node.args.kwarg.arg, Parameter.VAR_KEYWORD,
annotation=arg.type_comment or Parameter.empty)
params.append(param)

# Remove first parameter when *obj* is bound_method
if bound_method and params:
params.pop(0)

# merge type_comment into signature
if not_suppressed(type_comment.argtypes): # type: ignore
for i, param in enumerate(params):
params[i] = param.replace(annotation=type_comment.argtypes[i]) # type: ignore

if node.returns:
return Signature(params, return_annotation=node.returns)
elif type_comment.returns:
return Signature(params, return_annotation=ast_unparse(type_comment.returns))
else:
return Signature(params)


def get_type_comment(obj: Any, bound_method: bool = False) -> Signature:
"""Get type_comment'ed FunctionDef object from living object.
This tries to parse original code for living object and returns
AST node for given *obj*. It requires py38+ or typed_ast module.
Signature for given *obj*. It requires py38+ or typed_ast module.
"""
try:
source = getsource(obj)
Expand All @@ -41,7 +103,8 @@ def get_type_comment(obj: Any) -> ast.FunctionDef:
subject = cast(ast.FunctionDef, module.body[0]) # type: ignore

if getattr(subject, "type_comment", None):
return ast_parse(subject.type_comment, mode='func_type') # type: ignore
function = ast_parse(subject.type_comment, mode='func_type')
return signature_from_ast(subject, bound_method, function) # type: ignore
else:
return None
except (OSError, TypeError): # failed to load source code
Expand All @@ -53,17 +116,17 @@ def get_type_comment(obj: Any) -> ast.FunctionDef:
def update_annotations_using_type_comments(app: Sphinx, obj: Any, bound_method: bool) -> None:
"""Update annotations info of *obj* using type_comments."""
try:
function = get_type_comment(obj)
if function and hasattr(function, 'argtypes'):
if function.argtypes != [ast.Ellipsis]: # type: ignore
sig = inspect.signature(obj, bound_method)
for i, param in enumerate(sig.parameters.values()):
if param.name not in obj.__annotations__:
annotation = ast_unparse(function.argtypes[i]) # type: ignore
obj.__annotations__[param.name] = annotation
type_sig = get_type_comment(obj, bound_method)
if type_sig:
sig = inspect.signature(obj, bound_method)
for param in sig.parameters.values():
if param.name not in obj.__annotations__:
annotation = type_sig.parameters[param.name].annotation
if annotation is not Parameter.empty:
obj.__annotations__[param.name] = ast_unparse(annotation)

if 'return' not in obj.__annotations__:
obj.__annotations__['return'] = ast_unparse(function.returns) # type: ignore
obj.__annotations__['return'] = type_sig.return_annotation
except NotImplementedError as exc: # failed to ast.unparse()
logger.warning("Failed to parse type_comment for %r: %s", obj, exc)

Expand Down
20 changes: 20 additions & 0 deletions tests/roots/test-ext-autodoc/target/typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,27 @@ def decr(self, a, b = 1):
# type: (int, int) -> int
return a - b

def nothing(self):
# type: () -> None
pass

def horse(self,
a, # type: str
b, # type: int
):
# type: (...) -> None
return


def complex_func(arg1, arg2, arg3=None, *args, **kwargs):
# type: (str, List[int], Tuple[int, Union[str, Unknown]], *str, **str) -> None
pass


def missing_attr(c,
a, # type: str
b=None # type: Optional[str]
):
# type: (...) -> str
return a + (b or "")

24 changes: 24 additions & 0 deletions tests/test_ext_autodoc_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,9 +483,17 @@ def test_autodoc_typehints_signature(app):
' :module: target.typehints',
' ',
' ',
' .. py:method:: Math.horse(a: str, b: int) -> None',
' :module: target.typehints',
' ',
' ',
' .. py:method:: Math.incr(a: int, b: int = 1) -> int',
' :module: target.typehints',
' ',
' ',
' .. py:method:: Math.nothing() -> None',
' :module: target.typehints',
' ',
'',
'.. py:function:: complex_func(arg1: str, arg2: List[int], arg3: Tuple[int, '
'Union[str, Unknown]] = None, *args: str, **kwargs: str) -> None',
Expand All @@ -498,6 +506,10 @@ def test_autodoc_typehints_signature(app):
'',
'.. py:function:: incr(a: int, b: int = 1) -> int',
' :module: target.typehints',
'',
'',
'.. py:function:: missing_attr(c, a: str, b: Optional[str] = None) -> str',
' :module: target.typehints',
''
]

Expand All @@ -522,9 +534,17 @@ def test_autodoc_typehints_none(app):
' :module: target.typehints',
' ',
' ',
' .. py:method:: Math.horse(a, b)',
' :module: target.typehints',
' ',
' ',
' .. py:method:: Math.incr(a, b=1)',
' :module: target.typehints',
' ',
' ',
' .. py:method:: Math.nothing()',
' :module: target.typehints',
' ',
'',
'.. py:function:: complex_func(arg1, arg2, arg3=None, *args, **kwargs)',
' :module: target.typehints',
Expand All @@ -536,6 +556,10 @@ def test_autodoc_typehints_none(app):
'',
'.. py:function:: incr(a, b=1)',
' :module: target.typehints',
'',
'',
'.. py:function:: missing_attr(c, a, b=None)',
' :module: target.typehints',
''
]

Expand Down

0 comments on commit 39a192b

Please sign in to comment.