Merge tag 'v3.1.1'

This commit is contained in:
Takeshi KOMIYA
2020-07-05 00:13:23 +09:00
14 changed files with 137 additions and 28 deletions

30
CHANGES
View File

@@ -36,6 +36,33 @@ Bugs fixed
Testing Testing
-------- --------
Release 3.1.1 (released Jun 14, 2020)
=====================================
Incompatible changes
--------------------
* #7808: napoleon: a type for attribute are represented as typed field
Features added
--------------
* #7807: autodoc: Show detailed warning when type_comment is mismatched with its
signature
Bugs fixed
----------
* #7808: autodoc: Warnings raised on variable and attribute type annotations
* #7802: autodoc: EOFError is raised on parallel build
* #7821: autodoc: TypeError is raised for overloaded C-ext function
* #7805: autodoc: an object which descriptors returns is unexpectedly documented
* #7807: autodoc: wrong signature is shown for the function using contextmanager
* #7812: autosummary: generates broken stub files if the target code contains
an attribute and module that are same name
* #7808: napoleon: Warnings raised on variable and attribute type annotations
* #7811: sphinx.util.inspect causes circular import problem
Release 3.1.0 (released Jun 08, 2020) Release 3.1.0 (released Jun 08, 2020)
===================================== =====================================
@@ -194,9 +221,6 @@ Bugs fixed
* #7763: C and C++, don't crash during display stringification of unary * #7763: C and C++, don't crash during display stringification of unary
expressions and fold expressions. expressions and fold expressions.
Release 3.0.5 (in development)
==============================
Release 3.0.4 (released May 27, 2020) Release 3.0.4 (released May 27, 2020)
===================================== =====================================

View File

@@ -592,7 +592,8 @@ class PyVariable(PyObject):
typ = self.options.get('type') typ = self.options.get('type')
if typ: if typ:
signode += addnodes.desc_annotation(typ, '', nodes.Text(': '), type_to_xref(typ)) annotations = _parse_annotation(typ)
signode += addnodes.desc_annotation(typ, '', nodes.Text(': '), *annotations)
value = self.options.get('value') value = self.options.get('value')
if value: if value:
@@ -752,7 +753,8 @@ class PyAttribute(PyObject):
typ = self.options.get('type') typ = self.options.get('type')
if typ: if typ:
signode += addnodes.desc_annotation(typ, '', nodes.Text(': '), type_to_xref(typ)) annotations = _parse_annotation(typ)
signode += addnodes.desc_annotation(typ, '', nodes.Text(': '), *annotations)
value = self.options.get('value') value = self.options.get('value')
if value: if value:

View File

@@ -421,9 +421,9 @@ class Documenter:
if matched: if matched:
args = matched.group(1) args = matched.group(1)
retann = matched.group(2) retann = matched.group(2)
except Exception: except Exception as exc:
logger.warning(__('error while formatting arguments for %s:') % logger.warning(__('error while formatting arguments for %s: %s'),
self.fullname, type='autodoc', exc_info=True) self.fullname, exc, type='autodoc')
args = None args = None
result = self.env.events.emit_firstresult('autodoc-process-signature', result = self.env.events.emit_firstresult('autodoc-process-signature',
@@ -790,8 +790,8 @@ class Documenter:
# parse right now, to get PycodeErrors on parsing (results will # parse right now, to get PycodeErrors on parsing (results will
# be cached anyway) # be cached anyway)
self.analyzer.find_attr_docs() self.analyzer.find_attr_docs()
except PycodeError: except PycodeError as exc:
logger.debug('[autodoc] module analyzer failed:', exc_info=True) logger.debug('[autodoc] module analyzer failed: %s', exc)
# no source file -- e.g. for builtin and C modules # no source file -- e.g. for builtin and C modules
self.analyzer = None self.analyzer = None
# at least add the module.__file__ as a dependency # at least add the module.__file__ as a dependency
@@ -1223,7 +1223,11 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
params = list(sig.parameters.values()) params = list(sig.parameters.values())
if params[0].annotation is Parameter.empty: if params[0].annotation is Parameter.empty:
params[0] = params[0].replace(annotation=typ) params[0] = params[0].replace(annotation=typ)
func.__signature__ = sig.replace(parameters=params) # type: ignore try:
func.__signature__ = sig.replace(parameters=params) # type: ignore
except TypeError:
# failed to update signature (ex. built-in or extension types)
return
class SingledispatchFunctionDocumenter(FunctionDocumenter): class SingledispatchFunctionDocumenter(FunctionDocumenter):
@@ -1815,7 +1819,11 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
params = list(sig.parameters.values()) params = list(sig.parameters.values())
if params[1].annotation is Parameter.empty: if params[1].annotation is Parameter.empty:
params[1] = params[1].replace(annotation=typ) params[1] = params[1].replace(annotation=typ)
func.__signature__ = sig.replace(parameters=params) # type: ignore try:
func.__signature__ = sig.replace(parameters=params) # type: ignore
except TypeError:
# failed to update signature (ex. built-in or extension types)
return
class SingledispatchMethodDocumenter(MethodDocumenter): class SingledispatchMethodDocumenter(MethodDocumenter):
@@ -1903,6 +1911,17 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
else: else:
self.add_line(' :annotation: %s' % self.options.annotation, sourcename) self.add_line(' :annotation: %s' % self.options.annotation, sourcename)
def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
try:
# Disable `autodoc_inherit_docstring` temporarily to avoid to obtain
# a docstring from the value which descriptor returns unexpectedly.
# ref: https://github.com/sphinx-doc/sphinx/issues/7805
orig = self.env.config.autodoc_inherit_docstrings
self.env.config.autodoc_inherit_docstrings = False # type: ignore
return super().get_doc(encoding, ignore)
finally:
self.env.config.autodoc_inherit_docstrings = orig # type: ignore
def add_content(self, more_content: Any, no_docstring: bool = False) -> None: def add_content(self, more_content: Any, no_docstring: bool = False) -> None:
if not self._datadescriptor: if not self._datadescriptor:
# if it's not a data descriptor, its docstring is very probably the # if it's not a data descriptor, its docstring is very probably the

View File

@@ -128,6 +128,9 @@ def update_annotations_using_type_comments(app: Sphinx, obj: Any, bound_method:
if 'return' not in obj.__annotations__: if 'return' not in obj.__annotations__:
obj.__annotations__['return'] = type_sig.return_annotation obj.__annotations__['return'] = type_sig.return_annotation
except KeyError as exc:
logger.warning(__("Failed to update signature for %r: parameter not found: %s"),
obj, exc)
except NotImplementedError as exc: # failed to ast.unparse() except NotImplementedError as exc: # failed to ast.unparse()
logger.warning(__("Failed to parse type_comment for %r: %s"), obj, exc) logger.warning(__("Failed to parse type_comment for %r: %s"), obj, exc)

View File

@@ -168,10 +168,11 @@ class Config:
**If False**:: **If False**::
.. attribute:: attr1 .. attribute:: attr1
:type: int
Description of `attr1` Description of `attr1`
:type: int
napoleon_use_param : :obj:`bool` (Defaults to True) napoleon_use_param : :obj:`bool` (Defaults to True)
True to use a ``:param:`` role for each function parameter. False to True to use a ``:param:`` role for each function parameter. False to
use a single ``:parameters:`` role for all the parameters. use a single ``:parameters:`` role for all the parameters.

View File

@@ -584,12 +584,13 @@ class GoogleDocstring:
lines.append('.. attribute:: ' + _name) lines.append('.. attribute:: ' + _name)
if self._opt and 'noindex' in self._opt: if self._opt and 'noindex' in self._opt:
lines.append(' :noindex:') lines.append(' :noindex:')
if _type:
lines.extend(self._indent([':type: %s' % _type], 3))
lines.append('') lines.append('')
fields = self._format_field('', '', _desc) fields = self._format_field('', '', _desc)
lines.extend(self._indent(fields, 3)) lines.extend(self._indent(fields, 3))
if _type:
lines.append('')
lines.extend(self._indent([':type: %s' % _type], 3))
lines.append('') lines.append('')
if self._config.napoleon_use_ivar: if self._config.napoleon_use_ivar:
lines.append('') lines.append('')

View File

@@ -19,7 +19,6 @@ from typing import Any, Dict, List, Optional, Tuple
from sphinx.pycode.ast import ast # for py37 or older from sphinx.pycode.ast import ast # for py37 or older
from sphinx.pycode.ast import parse, unparse from sphinx.pycode.ast import parse, unparse
from sphinx.util.inspect import signature_from_ast
comment_re = re.compile('^\\s*#: ?(.*)\r?\n?$') comment_re = re.compile('^\\s*#: ?(.*)\r?\n?$')
@@ -265,6 +264,8 @@ class VariableCommentPicker(ast.NodeVisitor):
self.finals.append(".".join(qualname)) self.finals.append(".".join(qualname))
def add_overload_entry(self, func: ast.FunctionDef) -> None: def add_overload_entry(self, func: ast.FunctionDef) -> None:
# avoid circular import problem
from sphinx.util.inspect import signature_from_ast
qualname = self.get_qualname_for(func.name) qualname = self.get_qualname_for(func.name)
if qualname: if qualname:
overloads = self.overloads.setdefault(".".join(qualname), []) overloads = self.overloads.setdefault(".".join(qualname), [])

View File

@@ -425,13 +425,28 @@ def split_full_qualified_name(name: str) -> Tuple[str, str]:
Therefore you need to mock 3rd party modules if needed before Therefore you need to mock 3rd party modules if needed before
calling this function. calling this function.
""" """
from sphinx.util import inspect
parts = name.split('.') parts = name.split('.')
for i, part in enumerate(parts, 1): for i, part in enumerate(parts, 1):
try: try:
modname = ".".join(parts[:i]) modname = ".".join(parts[:i])
import_module(modname) module = import_module(modname)
# check the module has a member named as attrname
#
# Note: This is needed to detect the attribute having the same name
# as the module.
# ref: https://github.com/sphinx-doc/sphinx/issues/7812
attrname = parts[i]
if hasattr(module, attrname):
value = inspect.safe_getattr(module, attrname)
if not inspect.ismodule(value):
return ".".join(parts[:i]), ".".join(parts[i:])
except ImportError: except ImportError:
return ".".join(parts[:i - 1]), ".".join(parts[i - 1:]) return ".".join(parts[:i - 1]), ".".join(parts[i - 1:])
except IndexError:
pass
return name, "" return name, ""

View File

@@ -9,6 +9,7 @@
""" """
import builtins import builtins
import contextlib
import enum import enum
import inspect import inspect
import re import re
@@ -18,7 +19,7 @@ import typing
import warnings import warnings
from functools import partial, partialmethod from functools import partial, partialmethod
from inspect import ( # NOQA from inspect import ( # NOQA
Parameter, isclass, ismethod, ismethoddescriptor Parameter, isclass, ismethod, ismethoddescriptor, ismodule
) )
from io import StringIO from io import StringIO
from typing import Any, Callable from typing import Any, Callable
@@ -404,6 +405,17 @@ def is_builtin_class_method(obj: Any, attr_name: str) -> bool:
return getattr(builtins, name, None) is cls return getattr(builtins, name, None) is cls
def _should_unwrap(subject: Callable) -> bool:
"""Check the function should be unwrapped on getting signature."""
if (safe_getattr(subject, '__globals__', None) and
subject.__globals__.get('__name__') == 'contextlib' and # type: ignore
subject.__globals__.get('__file__') == contextlib.__file__): # type: ignore
# contextmanger should be unwrapped
return True
return False
def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = False def signature(subject: Callable, bound_method: bool = False, follow_wrapped: bool = False
) -> inspect.Signature: ) -> inspect.Signature:
"""Return a Signature object for the given *subject*. """Return a Signature object for the given *subject*.
@@ -414,7 +426,10 @@ def signature(subject: Callable, bound_method: bool = False, follow_wrapped: boo
""" """
try: try:
try: try:
signature = inspect.signature(subject, follow_wrapped=follow_wrapped) if _should_unwrap(subject):
signature = inspect.signature(subject)
else:
signature = inspect.signature(subject, follow_wrapped=follow_wrapped)
except ValueError: except ValueError:
# follow built-in wrappers up (ex. functools.lru_cache) # follow built-in wrappers up (ex. functools.lru_cache)
signature = inspect.signature(subject) signature = inspect.signature(subject)

View File

@@ -27,7 +27,7 @@ if TYPE_CHECKING:
from sphinx.builders import Builder from sphinx.builders import Builder
from sphinx.domain import IndexEntry from sphinx.domain import IndexEntry
from sphinx.environment import BuildEnvironment from sphinx.environment import BuildEnvironment
from sphinx.utils.tags import Tags from sphinx.util.tags import Tags
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -1,8 +1,15 @@
# for py32 or above from contextlib import contextmanager
from functools import lru_cache from functools import lru_cache
from typing import Generator
@lru_cache(maxsize=None) @lru_cache(maxsize=None)
def slow_function(message, timeout): def slow_function(message, timeout):
"""This function is slow.""" """This function is slow."""
print(message) print(message)
@contextmanager
def feeling_good(x: int, y: int) -> Generator:
"""You'll feel better in this context!"""
yield

View File

@@ -681,7 +681,7 @@ def test_pyattribute(app):
text = (".. py:class:: Class\n" text = (".. py:class:: Class\n"
"\n" "\n"
" .. py:attribute:: attr\n" " .. py:attribute:: attr\n"
" :type: str\n" " :type: Optional[str]\n"
" :value: ''\n") " :value: ''\n")
domain = app.env.get_domain('py') domain = app.env.get_domain('py')
doctree = restructuredtext.parse(app, text) doctree = restructuredtext.parse(app, text)
@@ -694,7 +694,10 @@ def test_pyattribute(app):
entries=[('single', 'attr (Class attribute)', 'Class.attr', '', None)]) entries=[('single', 'attr (Class attribute)', 'Class.attr', '', None)])
assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "attr"], assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "attr"],
[desc_annotation, (": ", [desc_annotation, (": ",
[pending_xref, "str"])], [pending_xref, "Optional"],
[desc_sig_punctuation, "["],
[pending_xref, "str"],
[desc_sig_punctuation, "]"])],
[desc_annotation, " = ''"])], [desc_annotation, " = ''"])],
[desc_content, ()])) [desc_content, ()]))
assert 'Class.attr' in domain.objects assert 'Class.attr' in domain.objects

View File

@@ -146,3 +146,16 @@ def test_wrapped_function(app):
' This function is slow.', ' This function is slow.',
'', '',
] ]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_wrapped_function_contextmanager(app):
actual = do_autodoc(app, 'function', 'target.wrappedfunction.feeling_good')
assert list(actual) == [
'',
'.. py:function:: feeling_good(x: int, y: int) -> Generator',
' :module: target.wrappedfunction',
'',
" You'll feel better in this context!",
'',
]

View File

@@ -53,19 +53,22 @@ class NamedtupleSubclassTest(BaseDocstringTest):
Sample namedtuple subclass Sample namedtuple subclass
.. attribute:: attr1 .. attribute:: attr1
:type: Arbitrary type
Quick description of attr1 Quick description of attr1
:type: Arbitrary type
.. attribute:: attr2 .. attribute:: attr2
:type: Another arbitrary type
Quick description of attr2 Quick description of attr2
:type: Another arbitrary type
.. attribute:: attr3 .. attribute:: attr3
:type: Type
Adds a newline after the type Adds a newline after the type
:type: Type
""" """
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
@@ -409,9 +412,10 @@ Attributes:
actual = str(GoogleDocstring(docstring)) actual = str(GoogleDocstring(docstring))
expected = """\ expected = """\
.. attribute:: in_attr .. attribute:: in_attr
:type: :class:`numpy.ndarray`
super-dooper attribute super-dooper attribute
:type: :class:`numpy.ndarray`
""" """
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
@@ -423,9 +427,10 @@ Attributes:
actual = str(GoogleDocstring(docstring)) actual = str(GoogleDocstring(docstring))
expected = """\ expected = """\
.. attribute:: in_attr .. attribute:: in_attr
:type: numpy.ndarray
super-dooper attribute super-dooper attribute
:type: numpy.ndarray
""" """
self.assertEqual(expected, actual) self.assertEqual(expected, actual)