From 476169284d2c1ae36d60fd139d5631afa2eade4c Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 28 Dec 2020 21:26:22 +0900 Subject: [PATCH 1/3] autodoc: Add a helper that checks the object is mocked; ismock() --- sphinx/ext/autodoc/mock.py | 22 ++++++++++++++++++++++ tests/test_ext_autodoc_mock.py | 18 +++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/sphinx/ext/autodoc/mock.py b/sphinx/ext/autodoc/mock.py index 40258a13577..3b49918562a 100644 --- a/sphinx/ext/autodoc/mock.py +++ b/sphinx/ext/autodoc/mock.py @@ -17,6 +17,7 @@ from typing import Any, Generator, Iterator, List, Sequence, Tuple, Union from sphinx.util import logging +from sphinx.util.inspect import safe_getattr logger = logging.getLogger(__name__) @@ -147,3 +148,24 @@ def mock(modnames: List[str]) -> Generator[None, None, None]: finally: sys.meta_path.remove(finder) 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 diff --git a/tests/test_ext_autodoc_mock.py b/tests/test_ext_autodoc_mock.py index a29170f752d..aa2da86c89c 100644 --- a/tests/test_ext_autodoc_mock.py +++ b/tests/test_ext_autodoc_mock.py @@ -15,7 +15,7 @@ import pytest -from sphinx.ext.autodoc.mock import _MockModule, _MockObject, mock +from sphinx.ext.autodoc.mock import _MockModule, _MockObject, ismock, mock def test_MockModule(): @@ -129,3 +129,19 @@ class Bar: assert func.__doc__ == "docstring" assert Foo.meth.__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 From 1a659c6ca7e7697eaee67bf97cb56bb3c471d24d Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 28 Dec 2020 23:21:14 +0900 Subject: [PATCH 2/3] testing: Add rollback_sysmodules fixture to unload modules after tests --- sphinx/testing/fixtures.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sphinx/testing/fixtures.py b/sphinx/testing/fixtures.py index e6c3edd186f..b9ee30d8ac4 100644 --- a/sphinx/testing/fixtures.py +++ b/sphinx/testing/fixtures.py @@ -250,3 +250,15 @@ def tempdir(tmpdir: str) -> "util.path": this fixture is for compat with old test implementation. """ 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) From 85567748d968cbd95587997788ba46cb29245b36 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 28 Dec 2020 21:46:57 +0900 Subject: [PATCH 3/3] Fix #8164: autodoc: Classes that inherit mocked class are not documented Use ismock() to check a module member is a mocked or not. It allows not to skip subclasses of mocked class. --- CHANGES | 2 ++ sphinx/ext/autodoc/__init__.py | 4 +-- .../test-ext-autodoc/target/need_mocks.py | 5 +++ tests/test_ext_autodoc_automodule.py | 31 +++++++++++++++++++ tests/test_ext_autodoc_configs.py | 3 ++ 5 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 tests/test_ext_autodoc_automodule.py diff --git a/CHANGES b/CHANGES index 895293b7fb3..b237781769a 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,8 @@ Features added Bugs fixed ---------- +* #8164: autodoc: Classes that inherit mocked class are not documented + Testing -------- diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 85f9f6de255..a1c642703db 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -28,7 +28,7 @@ from sphinx.environment import BuildEnvironment from sphinx.ext.autodoc.importer import (get_class_members, get_module_members, get_object_members, import_object) -from sphinx.ext.autodoc.mock import mock +from sphinx.ext.autodoc.mock import ismock, mock from sphinx.locale import _, __ from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.util import inspect, logging @@ -731,7 +731,7 @@ def is_filtered_inherited_member(name: str) -> bool: isprivate = membername.startswith('_') keep = False - if safe_getattr(member, '__sphinx_mock__', None) is not None: + if ismock(member): # mocked module or object pass elif self.options.exclude_members and membername in self.options.exclude_members: diff --git a/tests/roots/test-ext-autodoc/target/need_mocks.py b/tests/roots/test-ext-autodoc/target/need_mocks.py index 275ce8d5f2d..bc227f24682 100644 --- a/tests/roots/test-ext-autodoc/target/need_mocks.py +++ b/tests/roots/test-ext-autodoc/target/need_mocks.py @@ -28,4 +28,9 @@ def decoratedMethod(self): return None +class Inherited(missing_module.Class): + """docstring""" + pass + + sphinx.missing_module4.missing_function(len(missing_name2)) diff --git a/tests/test_ext_autodoc_automodule.py b/tests/test_ext_autodoc_automodule.py new file mode 100644 index 00000000000..2fd20a611ea --- /dev/null +++ b/tests/test_ext_autodoc_automodule.py @@ -0,0 +1,31 @@ +""" + test_ext_autodoc_autocmodule + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Test the autodoc extension. This tests mainly the Documenters; the auto + directives are tested in a test source file translated by test_build. + + :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import sys + +import pytest + +from .test_ext_autodoc import do_autodoc + + +@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 diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index ac0a2c11c1b..979c66d4732 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -429,7 +429,10 @@ def test_autoclass_content_and_docstring_signature_both(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') +@pytest.mark.usefixtures("rollback_sysmodules") def test_mocked_module_imports(app, warning): + sys.modules.pop('target', None) # unload target module to clear the module cache + # no autodoc_mock_imports options = {"members": 'TestAutodoc,decoratedFunction,func'} actual = do_autodoc(app, 'module', 'target.need_mocks', options)