Merge branch '3.4.x' into 3.x

This commit is contained in:
Takeshi KOMIYA 2020-12-29 18:33:36 +09:00
commit 3f7bf48715
8 changed files with 80 additions and 3 deletions

View File

@ -53,6 +53,8 @@ Features added
Bugs fixed Bugs fixed
---------- ----------
* #8164: autodoc: Classes that inherit mocked class are not documented
Testing Testing
-------- --------

View File

@ -27,7 +27,7 @@ from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warni
from sphinx.environment import BuildEnvironment from sphinx.environment import BuildEnvironment
from sphinx.ext.autodoc.importer import (ClassAttribute, get_class_members, get_object_members, from sphinx.ext.autodoc.importer import (ClassAttribute, get_class_members, get_object_members,
import_module, import_object) import_module, import_object)
from sphinx.ext.autodoc.mock import mock from sphinx.ext.autodoc.mock import ismock, mock
from sphinx.locale import _, __ from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import inspect, logging from sphinx.util import inspect, logging
@ -739,7 +739,7 @@ class Documenter:
isprivate = membername.startswith('_') isprivate = membername.startswith('_')
keep = False keep = False
if safe_getattr(member, '__sphinx_mock__', None) is not None: if ismock(member):
# mocked module or object # mocked module or object
pass pass
elif self.options.exclude_members and membername in self.options.exclude_members: elif self.options.exclude_members and membername in self.options.exclude_members:

View File

@ -17,6 +17,7 @@ from types import FunctionType, MethodType, ModuleType
from typing import Any, Generator, Iterator, List, Sequence, Tuple, Union from typing import Any, Generator, Iterator, List, Sequence, Tuple, Union
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.inspect import safe_getattr
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -147,3 +148,24 @@ def mock(modnames: List[str]) -> Generator[None, None, None]:
finally: finally:
sys.meta_path.remove(finder) sys.meta_path.remove(finder)
finder.invalidate_caches() finder.invalidate_caches()
def ismock(subject: Any) -> bool:
"""Check if the object is mocked."""
# check the object has '__sphinx_mock__' attribute
if not hasattr(subject, '__sphinx_mock__'):
return False
# check the object is mocked module
if isinstance(subject, _MockModule):
return True
try:
# check the object is mocked object
__mro__ = safe_getattr(type(subject), '__mro__', [])
if len(__mro__) > 2 and __mro__[1] is _MockObject:
return True
except AttributeError:
pass
return False

View File

@ -250,3 +250,15 @@ def tempdir(tmpdir: str) -> "util.path":
this fixture is for compat with old test implementation. this fixture is for compat with old test implementation.
""" """
return util.path(tmpdir) return util.path(tmpdir)
@pytest.fixture
def rollback_sysmodules():
"""Rollback sys.modules to before testing to unload modules during tests."""
try:
sysmodules = list(sys.modules)
yield
finally:
for modname in list(sys.modules):
if modname not in sysmodules:
sys.modules.pop(modname)

View File

@ -28,4 +28,9 @@ class TestAutodoc(object):
return None return None
class Inherited(missing_module.Class):
"""docstring"""
pass
sphinx.missing_module4.missing_function(len(missing_name2)) sphinx.missing_module4.missing_function(len(missing_name2))

View File

@ -9,6 +9,8 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import sys
import pytest import pytest
from .test_ext_autodoc import do_autodoc from .test_ext_autodoc import do_autodoc
@ -25,3 +27,18 @@ def test_empty_all(app):
'docsting of empty_all module.', 'docsting of empty_all module.',
'', '',
] ]
@pytest.mark.sphinx('html', testroot='ext-autodoc',
confoverrides={'autodoc_mock_imports': ['missing_module',
'missing_package1',
'missing_package2',
'missing_package3',
'sphinx.missing_module4']})
@pytest.mark.usefixtures("rollback_sysmodules")
def test_subclass_of_mocked_object(app):
sys.modules.pop('target', None) # unload target module to clear the module cache
options = {'members': True}
actual = do_autodoc(app, 'module', 'target.need_mocks', options)
assert '.. py:class:: Inherited(*args: Any, **kwargs: Any)' in actual

View File

@ -429,7 +429,10 @@ def test_autoclass_content_and_docstring_signature_both(app):
@pytest.mark.sphinx('html', testroot='ext-autodoc') @pytest.mark.sphinx('html', testroot='ext-autodoc')
@pytest.mark.usefixtures("rollback_sysmodules")
def test_mocked_module_imports(app, warning): def test_mocked_module_imports(app, warning):
sys.modules.pop('target', None) # unload target module to clear the module cache
# no autodoc_mock_imports # no autodoc_mock_imports
options = {"members": 'TestAutodoc,decoratedFunction,func'} options = {"members": 'TestAutodoc,decoratedFunction,func'}
actual = do_autodoc(app, 'module', 'target.need_mocks', options) actual = do_autodoc(app, 'module', 'target.need_mocks', options)

View File

@ -15,7 +15,7 @@ from typing import TypeVar
import pytest import pytest
from sphinx.ext.autodoc.mock import _MockModule, _MockObject, mock from sphinx.ext.autodoc.mock import _MockModule, _MockObject, ismock, mock
def test_MockModule(): def test_MockModule():
@ -129,3 +129,19 @@ def test_mock_decorator():
assert func.__doc__ == "docstring" assert func.__doc__ == "docstring"
assert Foo.meth.__doc__ == "docstring" assert Foo.meth.__doc__ == "docstring"
assert Bar.__doc__ == "docstring" assert Bar.__doc__ == "docstring"
def test_ismock():
with mock(['sphinx.unknown']):
mod1 = import_module('sphinx.unknown')
mod2 = import_module('sphinx.application')
class Inherited(mod1.Class):
pass
assert ismock(mod1) is True
assert ismock(mod1.Class) is True
assert ismock(Inherited) is False
assert ismock(mod2) is False
assert ismock(mod2.Sphinx) is False