Merge branch '2.4.2' into 2.0

This commit is contained in:
Takeshi KOMIYA
2020-02-16 21:41:47 +09:00
10 changed files with 159 additions and 25 deletions

View File

@@ -24,7 +24,7 @@ matrix:
env:
- TOXENV=du15
- PYTEST_ADDOPTS="--cov ./ --cov-append --cov-config setup.cfg"
- python: '3.8'
- python: 'nightly'
env:
- TOXENV=du16
- python: '3.6'

View File

@@ -16,6 +16,12 @@ Features added
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
* #7151: crashed when extension assigns a value to ``env.indexentries``
Testing
--------

View File

@@ -755,6 +755,14 @@ class BuildEnvironment:
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

View File

@@ -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
@@ -23,11 +23,73 @@ from sphinx.util import logging
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)
@@ -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
@@ -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)

View File

@@ -46,7 +46,7 @@ def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any,
annotation[param.name] = typing.stringify(param.annotation)
if sig.return_annotation is not sig.empty:
annotation['return'] = typing.stringify(sig.return_annotation)
except TypeError:
except (TypeError, ValueError):
pass

View File

@@ -387,9 +387,9 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True,
if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY:
# PEP-570: Separator for Positional Only Parameter: /
args.append('/')
elif param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD,
param.POSITIONAL_ONLY,
None):
if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD,
param.POSITIONAL_ONLY,
None):
# PEP-3102: Separator for Keyword Only Parameter: *
args.append('*')

View File

@@ -1,5 +1,11 @@
def foo(a, b, /, c, d):
def foo(*, a, b):
pass
def bar(a, b, /):
def bar(a, b, /, c, d):
pass
def baz(a, /, *, b):
pass
def qux(a, b, /):
pass

View File

@@ -18,7 +18,27 @@ class Math:
# 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 "")

View File

@@ -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',
@@ -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',
''
]
@@ -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',
@@ -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',
''
]

View File

@@ -298,14 +298,21 @@ def test_signature_annotations():
@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.')
@pytest.mark.sphinx(testroot='ext-autodoc')
def test_signature_annotations_py38(app):
from target.pep570 import foo, bar
from target.pep570 import foo, bar, baz, qux
# case: separator at head
sig = inspect.signature(foo)
assert stringify_signature(sig) == '(*, a, b)'
# case: separator in the middle
sig = inspect.signature(foo)
sig = inspect.signature(bar)
assert stringify_signature(sig) == '(a, b, /, c, d)'
sig = inspect.signature(baz)
assert stringify_signature(sig) == '(a, /, *, b)'
# case: separator at tail
sig = inspect.signature(bar)
sig = inspect.signature(qux)
assert stringify_signature(sig) == '(a, b, /)'