Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #8164: autodoc: Classes that inherit mocked class are not documented #8607

Merged
merged 3 commits into from Dec 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES
Expand Up @@ -16,6 +16,8 @@ Features added
Bugs fixed
----------

* #8164: autodoc: Classes that inherit mocked class are not documented

Testing
--------

Expand Down
4 changes: 2 additions & 2 deletions sphinx/ext/autodoc/__init__.py
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
22 changes: 22 additions & 0 deletions sphinx/ext/autodoc/mock.py
Expand Up @@ -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__)

Expand Down Expand Up @@ -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
12 changes: 12 additions & 0 deletions sphinx/testing/fixtures.py
Expand Up @@ -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)
5 changes: 5 additions & 0 deletions tests/roots/test-ext-autodoc/target/need_mocks.py
Expand Up @@ -28,4 +28,9 @@ def decoratedMethod(self):
return None


class Inherited(missing_module.Class):
"""docstring"""
pass


sphinx.missing_module4.missing_function(len(missing_name2))
31 changes: 31 additions & 0 deletions 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
3 changes: 3 additions & 0 deletions tests/test_ext_autodoc_configs.py
Expand Up @@ -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)
Expand Down
18 changes: 17 additions & 1 deletion tests/test_ext_autodoc_mock.py
Expand Up @@ -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():
Expand Down Expand Up @@ -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