Fix #8134: autodoc: crashes when mocked decorator takes arguments

autodoc crashed when a decorator in mocked module takes arguments
because mock system returns the first argument for the decorator as a
decorated object.

This changes the approach for mocking decorators that remembers
arguments for each decoration, and fetch the latest argument on
generating document.
This commit is contained in:
Takeshi KOMIYA
2021-01-24 02:08:19 +09:00
parent 3a0a6556c5
commit a78c6b799f
6 changed files with 43 additions and 16 deletions

View File

@@ -66,6 +66,7 @@ Bugs fixed
* #8652: autodoc: All variable comments in the module are ignored if the module
contains invalid type comments
* #8693: autodoc: Default values for overloaded functions are rendered as string
* #8134: autodoc: crashes when mocked decorator takes arguments
* #8306: autosummary: mocked modules are documented as empty page when using
:recursive: option
* #8618: html: kbd role produces incorrect HTML when compound-key separators (-,

View File

@@ -27,7 +27,7 @@ from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warni
from sphinx.environment import BuildEnvironment
from sphinx.ext.autodoc.importer import (get_class_members, get_object_members, import_module,
import_object)
from sphinx.ext.autodoc.mock import ismock, mock
from sphinx.ext.autodoc.mock import ismock, mock, undecorate
from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import inspect, logging
@@ -422,6 +422,8 @@ class Documenter:
attrgetter=self.get_attr,
warningiserror=self.config.autodoc_warningiserror)
self.module, self.parent, self.object_name, self.object = ret
if ismock(self.object):
self.object = undecorate(self.object)
return True
except ImportError as exc:
if raiseerror:
@@ -1054,6 +1056,8 @@ class ModuleDocumenter(Documenter):
for name in dir(self.object):
try:
value = safe_getattr(self.object, name, None)
if ismock(value):
value = undecorate(value)
docstring = attr_docs.get(('', name), [])
members[name] = ObjectMember(name, value, docstring="\n".join(docstring))
except AttributeError:

View File

@@ -15,6 +15,7 @@ from typing import Any, Callable, Dict, List, Mapping, NamedTuple, Optional, Tup
from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warning,
deprecated_alias)
from sphinx.ext.autodoc.mock import ismock, undecorate
from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import logging
from sphinx.util.inspect import (getannotations, getmro, getslots, isclass, isenumclass,
@@ -285,6 +286,9 @@ def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable
for name in dir(subject):
try:
value = attrgetter(subject, name)
if ismock(value):
value = undecorate(value)
unmangled = unmangle(subject, name)
if unmangled and unmangled not in members:
if name in obj_dict:

View File

@@ -13,7 +13,7 @@ import os
import sys
from importlib.abc import Loader, MetaPathFinder
from importlib.machinery import ModuleSpec
from types import FunctionType, MethodType, ModuleType
from types import ModuleType
from typing import Any, Generator, Iterator, List, Sequence, Tuple, Union
from sphinx.util import logging
@@ -27,6 +27,7 @@ class _MockObject:
__display_name__ = '_MockObject'
__sphinx_mock__ = True
__sphinx_decorator_args__ = () # type: Tuple[Any, ...]
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
if len(args) == 3 and isinstance(args[1], tuple):
@@ -60,18 +61,19 @@ class _MockObject:
return _make_subclass(key, self.__display_name__, self.__class__)()
def __call__(self, *args: Any, **kwargs: Any) -> Any:
if args and type(args[0]) in [type, FunctionType, MethodType]:
# Appears to be a decorator, pass through unchanged
return args[0]
return self
call = self.__class__()
call.__sphinx_decorator_args__ = args
return call
def __repr__(self) -> str:
return self.__display_name__
def _make_subclass(name: str, module: str, superclass: Any = _MockObject,
attributes: Any = None) -> Any:
attrs = {'__module__': module, '__display_name__': module + '.' + name}
attributes: Any = None, decorator_args: Tuple = ()) -> Any:
attrs = {'__module__': module,
'__display_name__': module + '.' + name,
'__sphinx_decorator_args__': decorator_args}
attrs.update(attributes or {})
return type(name, (superclass,), attrs)
@@ -172,3 +174,14 @@ def ismock(subject: Any) -> bool:
pass
return False
def undecorate(subject: _MockObject) -> Any:
"""Unwrap mock if *subject* is decorated by mocked object.
If not decorated, returns given *subject* itself.
"""
if ismock(subject) and subject.__sphinx_decorator_args__:
return subject.__sphinx_decorator_args__[0]
else:
return subject

View File

@@ -9,7 +9,7 @@ import sphinx.missing_module4 # NOQA
from sphinx.missing_module4 import missing_name2 # NOQA
@missing_name
@missing_name(int)
def decoratedFunction():
"""decoratedFunction docstring"""
return None

View File

@@ -15,7 +15,7 @@ from typing import TypeVar
import pytest
from sphinx.ext.autodoc.mock import _MockModule, _MockObject, ismock, mock
from sphinx.ext.autodoc.mock import _MockModule, _MockObject, ismock, mock, undecorate
def test_MockModule():
@@ -115,20 +115,25 @@ def test_mock_decorator():
@mock.function_deco
def func():
"""docstring"""
pass
class Foo:
@mock.method_deco
def meth(self):
"""docstring"""
pass
@mock.class_deco
class Bar:
"""docstring"""
pass
assert func.__doc__ == "docstring"
assert Foo.meth.__doc__ == "docstring"
assert Bar.__doc__ == "docstring"
@mock.funcion_deco(Foo)
class Baz:
pass
assert undecorate(func).__name__ == "func"
assert undecorate(Foo.meth).__name__ == "meth"
assert undecorate(Bar).__name__ == "Bar"
assert undecorate(Baz).__name__ == "Baz"
def test_ismock():