Merge tag 'v3.1.1'

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

30
CHANGES
View File

@ -36,6 +36,33 @@ Bugs fixed
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)
=====================================
@ -194,9 +221,6 @@ Bugs fixed
* #7763: C and C++, don't crash during display stringification of unary
expressions and fold expressions.
Release 3.0.5 (in development)
==============================
Release 3.0.4 (released May 27, 2020)
=====================================

View File

@ -592,7 +592,8 @@ class PyVariable(PyObject):
typ = self.options.get('type')
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')
if value:
@ -752,7 +753,8 @@ class PyAttribute(PyObject):
typ = self.options.get('type')
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')
if value:

View File

@ -421,9 +421,9 @@ class Documenter:
if matched:
args = matched.group(1)
retann = matched.group(2)
except Exception:
logger.warning(__('error while formatting arguments for %s:') %
self.fullname, type='autodoc', exc_info=True)
except Exception as exc:
logger.warning(__('error while formatting arguments for %s: %s'),
self.fullname, exc, type='autodoc')
args = None
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
# be cached anyway)
self.analyzer.find_attr_docs()
except PycodeError:
logger.debug('[autodoc] module analyzer failed:', exc_info=True)
except PycodeError as exc:
logger.debug('[autodoc] module analyzer failed: %s', exc)
# no source file -- e.g. for builtin and C modules
self.analyzer = None
# at least add the module.__file__ as a dependency
@ -1223,7 +1223,11 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
params = list(sig.parameters.values())
if params[0].annotation is Parameter.empty:
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):
@ -1815,7 +1819,11 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
params = list(sig.parameters.values())
if params[1].annotation is Parameter.empty:
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):
@ -1903,6 +1911,17 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
else:
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:
if not self._datadescriptor:
# 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__:
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()
logger.warning(__("Failed to parse type_comment for %r: %s"), obj, exc)

View File

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

View File

@ -584,12 +584,13 @@ class GoogleDocstring:
lines.append('.. attribute:: ' + _name)
if self._opt and 'noindex' in self._opt:
lines.append(' :noindex:')
if _type:
lines.extend(self._indent([':type: %s' % _type], 3))
lines.append('')
fields = self._format_field('', '', _desc)
lines.extend(self._indent(fields, 3))
if _type:
lines.append('')
lines.extend(self._indent([':type: %s' % _type], 3))
lines.append('')
if self._config.napoleon_use_ivar:
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 parse, unparse
from sphinx.util.inspect import signature_from_ast
comment_re = re.compile('^\\s*#: ?(.*)\r?\n?$')
@ -265,6 +264,8 @@ class VariableCommentPicker(ast.NodeVisitor):
self.finals.append(".".join(qualname))
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)
if 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
calling this function.
"""
from sphinx.util import inspect
parts = name.split('.')
for i, part in enumerate(parts, 1):
try:
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:
return ".".join(parts[:i - 1]), ".".join(parts[i - 1:])
except IndexError:
pass
return name, ""

View File

@ -9,6 +9,7 @@
"""
import builtins
import contextlib
import enum
import inspect
import re
@ -18,7 +19,7 @@ import typing
import warnings
from functools import partial, partialmethod
from inspect import ( # NOQA
Parameter, isclass, ismethod, ismethoddescriptor
Parameter, isclass, ismethod, ismethoddescriptor, ismodule
)
from io import StringIO
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
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
) -> inspect.Signature:
"""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:
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:
# follow built-in wrappers up (ex. functools.lru_cache)
signature = inspect.signature(subject)

View File

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

View File

@ -1,8 +1,15 @@
# for py32 or above
from contextlib import contextmanager
from functools import lru_cache
from typing import Generator
@lru_cache(maxsize=None)
def slow_function(message, timeout):
"""This function is slow."""
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"
"\n"
" .. py:attribute:: attr\n"
" :type: str\n"
" :type: Optional[str]\n"
" :value: ''\n")
domain = app.env.get_domain('py')
doctree = restructuredtext.parse(app, text)
@ -694,7 +694,10 @@ def test_pyattribute(app):
entries=[('single', 'attr (Class attribute)', 'Class.attr', '', None)])
assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "attr"],
[desc_annotation, (": ",
[pending_xref, "str"])],
[pending_xref, "Optional"],
[desc_sig_punctuation, "["],
[pending_xref, "str"],
[desc_sig_punctuation, "]"])],
[desc_annotation, " = ''"])],
[desc_content, ()]))
assert 'Class.attr' in domain.objects

View File

@ -146,3 +146,16 @@ def test_wrapped_function(app):
' 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
.. attribute:: attr1
:type: Arbitrary type
Quick description of attr1
:type: Arbitrary type
.. attribute:: attr2
:type: Another arbitrary type
Quick description of attr2
:type: Another arbitrary type
.. attribute:: attr3
:type: Type
Adds a newline after the type
:type: Type
"""
self.assertEqual(expected, actual)
@ -409,9 +412,10 @@ Attributes:
actual = str(GoogleDocstring(docstring))
expected = """\
.. attribute:: in_attr
:type: :class:`numpy.ndarray`
super-dooper attribute
:type: :class:`numpy.ndarray`
"""
self.assertEqual(expected, actual)
@ -423,9 +427,10 @@ Attributes:
actual = str(GoogleDocstring(docstring))
expected = """\
.. attribute:: in_attr
:type: numpy.ndarray
super-dooper attribute
:type: numpy.ndarray
"""
self.assertEqual(expected, actual)