diff options
| -rw-r--r-- | CHANGES | 30 | ||||
| -rw-r--r-- | sphinx/domains/python.py | 6 | ||||
| -rw-r--r-- | sphinx/ext/autodoc/__init__.py | 33 | ||||
| -rw-r--r-- | sphinx/ext/autodoc/type_comment.py | 3 | ||||
| -rw-r--r-- | sphinx/ext/napoleon/__init__.py | 3 | ||||
| -rw-r--r-- | sphinx/ext/napoleon/docstring.py | 5 | ||||
| -rw-r--r-- | sphinx/pycode/parser.py | 3 | ||||
| -rw-r--r-- | sphinx/util/__init__.py | 17 | ||||
| -rw-r--r-- | sphinx/util/inspect.py | 19 | ||||
| -rw-r--r-- | sphinx/util/nodes.py | 2 | ||||
| -rw-r--r-- | tests/roots/test-ext-autodoc/target/wrappedfunction.py | 9 | ||||
| -rw-r--r-- | tests/test_domain_py.py | 7 | ||||
| -rw-r--r-- | tests/test_ext_autodoc_autofunction.py | 13 | ||||
| -rw-r--r-- | tests/test_ext_napoleon_docstring.py | 15 |
14 files changed, 137 insertions, 28 deletions
@@ -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) ===================================== diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index f348a6f3a..1d1b5f18c 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -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: diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index ae3bf1166..3df184467 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -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 diff --git a/sphinx/ext/autodoc/type_comment.py b/sphinx/ext/autodoc/type_comment.py index e6a77f24d..7f11e3d12 100644 --- a/sphinx/ext/autodoc/type_comment.py +++ b/sphinx/ext/autodoc/type_comment.py @@ -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) diff --git a/sphinx/ext/napoleon/__init__.py b/sphinx/ext/napoleon/__init__.py index 9b41152fc..10b1ff3a3 100644 --- a/sphinx/ext/napoleon/__init__.py +++ b/sphinx/ext/napoleon/__init__.py @@ -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. diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index ea26d76bb..0683a06ed 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -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('') diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index 7463249b5..a6f9b1643 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -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), []) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index f0794bed0..fd6410a36 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -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, "" diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index ddf72f1a4..036bbc364 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -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) diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 712db4ef4..e360ffb7f 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -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__) diff --git a/tests/roots/test-ext-autodoc/target/wrappedfunction.py b/tests/roots/test-ext-autodoc/target/wrappedfunction.py index ea872f086..0bd2d2069 100644 --- a/tests/roots/test-ext-autodoc/target/wrappedfunction.py +++ b/tests/roots/test-ext-autodoc/target/wrappedfunction.py @@ -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 diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 653ab1cd4..9219a0bec 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -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 diff --git a/tests/test_ext_autodoc_autofunction.py b/tests/test_ext_autodoc_autofunction.py index b4be85019..579ad9f48 100644 --- a/tests/test_ext_autodoc_autofunction.py +++ b/tests/test_ext_autodoc_autofunction.py @@ -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!", + '', + ] diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_ext_napoleon_docstring.py index 738fd6532..f9cd40104 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_ext_napoleon_docstring.py @@ -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) |
