diff options
-rw-r--r-- | .travis.yml | 2 | ||||
-rw-r--r-- | CHANGES | 6 | ||||
-rw-r--r-- | doc/conf.py | 2 | ||||
-rw-r--r-- | sphinx/environment/__init__.py | 8 | ||||
-rw-r--r-- | sphinx/ext/autodoc/type_comment.py | 93 | ||||
-rw-r--r-- | sphinx/ext/autodoc/typehints.py | 2 | ||||
-rw-r--r-- | sphinx/util/inspect.py | 6 | ||||
-rw-r--r-- | tests/roots/test-ext-autodoc/target/pep570.py | 10 | ||||
-rw-r--r-- | tests/roots/test-ext-autodoc/target/typehints.py | 20 | ||||
-rw-r--r-- | tests/test_ext_autodoc_configs.py | 24 | ||||
-rw-r--r-- | tests/test_util_inspect.py | 13 |
11 files changed, 160 insertions, 26 deletions
diff --git a/.travis.yml b/.travis.yml index 046efc35a..008f4e442 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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' @@ -85,6 +85,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 -------- diff --git a/doc/conf.py b/doc/conf.py index aa513edf8..9951aed2d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -14,7 +14,7 @@ templates_path = ['_templates'] exclude_patterns = ['_build'] project = 'Sphinx' -copyright = '2007-2019, Georg Brandl and the Sphinx team' +copyright = '2007-2020, Georg Brandl and the Sphinx team' version = sphinx.__display_version__ release = version show_authors = True diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 341f22a27..d7c74a2fe 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -644,3 +644,11 @@ class BuildEnvironment: from sphinx.domains.index import IndexDomain 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 diff --git a/sphinx/ext/autodoc/type_comment.py b/sphinx/ext/autodoc/type_comment.py index c94020bf0..81391bc5c 100644 --- a/sphinx/ext/autodoc/type_comment.py +++ b/sphinx/ext/autodoc/type_comment.py @@ -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) diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index d7b4ee96b..77934dae5 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -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 diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 60daf3754..4a5b5d028 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -389,9 +389,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('*') diff --git a/tests/roots/test-ext-autodoc/target/pep570.py b/tests/roots/test-ext-autodoc/target/pep570.py index 904692eeb..1a77eae93 100644 --- a/tests/roots/test-ext-autodoc/target/pep570.py +++ b/tests/roots/test-ext-autodoc/target/pep570.py @@ -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 diff --git a/tests/roots/test-ext-autodoc/target/typehints.py b/tests/roots/test-ext-autodoc/target/typehints.py index 842530c13..ab5bfb624 100644 --- a/tests/roots/test-ext-autodoc/target/typehints.py +++ b/tests/roots/test-ext-autodoc/target/typehints.py @@ -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 "") + diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index a9af8a272..b90772f6e 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -482,9 +482,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', @@ -497,6 +505,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', '' ] @@ -521,9 +533,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', @@ -535,6 +555,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', '' ] diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 34844c9bf..c6b2c9149 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -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 in the middle + # case: separator at head sig = inspect.signature(foo) + assert stringify_signature(sig) == '(*, a, b)' + + # case: separator in the middle + 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, /)' |