summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rw-r--r--CHANGES6
-rw-r--r--doc/conf.py2
-rw-r--r--sphinx/environment/__init__.py8
-rw-r--r--sphinx/ext/autodoc/type_comment.py93
-rw-r--r--sphinx/ext/autodoc/typehints.py2
-rw-r--r--sphinx/util/inspect.py6
-rw-r--r--tests/roots/test-ext-autodoc/target/pep570.py10
-rw-r--r--tests/roots/test-ext-autodoc/target/typehints.py20
-rw-r--r--tests/test_ext_autodoc_configs.py24
-rw-r--r--tests/test_util_inspect.py13
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'
diff --git a/CHANGES b/CHANGES
index 83b1963e2..ec2e75467 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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, /)'