mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
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:
1
CHANGES
1
CHANGES
@@ -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 (-,
|
||||
|
@@ -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:
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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():
|
||||
|
Reference in New Issue
Block a user