Merge pull request #6108 from tk0miya/refactor_autodoc_mock

refactor: Move implementation of mock to sphinx.ext.autodoc.mock
This commit is contained in:
Takeshi KOMIYA
2019-02-27 23:33:45 +09:00
committed by GitHub
7 changed files with 272 additions and 205 deletions

View File

@@ -13,6 +13,9 @@ Incompatible changes
Deprecated
----------
* ``sphinx.ext.autodoc.importer.MockFinder``
* ``sphinx.ext.autodoc.importer.MockLoader``
* ``sphinx.ext.autodoc.importer.mock()``
* ``sphinx.ext.autosummary.autolink_role()``
Features added

View File

@@ -234,6 +234,21 @@ The following is a list of deprecated interfaces.
- (will be) Removed
- Alternatives
* - ``sphinx.ext.autodoc.importer.MockFinder``
- 2.1
- 4.0
- ``sphinx.ext.autodoc.mock.MockFinder``
* - ``sphinx.ext.autodoc.importer.MockLoader``
- 2.1
- 4.0
- ``sphinx.ext.autodoc.mock.MockLoader``
* - ``sphinx.ext.autodoc.importer.mock()``
- 2.1
- 4.0
- ``sphinx.ext.autodoc.mock.mock()``
* - ``sphinx.ext.autosummary.autolink_role()``
- 2.1
- 4.0

View File

@@ -18,9 +18,11 @@ from typing import Any
from docutils.statemachine import StringList
import sphinx
from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning
from sphinx.ext.autodoc.importer import mock, import_object, get_object_members
from sphinx.ext.autodoc.importer import _MockImporter # to keep compatibility # NOQA
from sphinx.deprecation import (
RemovedInSphinx30Warning, RemovedInSphinx40Warning, deprecated_alias
)
from sphinx.ext.autodoc.importer import import_object, get_object_members
from sphinx.ext.autodoc.mock import mock
from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import logging
@@ -1472,6 +1474,15 @@ def merge_autodoc_default_flags(app, config):
)
from sphinx.ext.autodoc.mock import _MockImporter # NOQA
deprecated_alias('sphinx.ext.autodoc',
{
'_MockImporter': _MockImporter,
},
RemovedInSphinx40Warning)
def setup(app):
# type: (Sphinx) -> Dict[str, Any]
app.add_autodocumenter(ModuleDocumenter)

View File

@@ -8,218 +8,22 @@
:license: BSD, see LICENSE for details.
"""
import contextlib
import os
import sys
import traceback
import warnings
from collections import namedtuple
from importlib.abc import Loader, MetaPathFinder
from importlib.machinery import ModuleSpec
from types import FunctionType, MethodType, ModuleType
from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx.util import logging
from sphinx.util.inspect import isenumclass, safe_getattr
if False:
# For type annotation
from typing import Any, Callable, Dict, Generator, Iterator, List, Optional, Sequence, Tuple, Union # NOQA
from typing import Any, Callable, Dict, List # NOQA
logger = logging.getLogger(__name__)
class _MockObject:
"""Used by autodoc_mock_imports."""
__display_name__ = '_MockObject'
def __new__(cls, *args, **kwargs):
# type: (Any, Any) -> Any
if len(args) == 3 and isinstance(args[1], tuple):
superclass = args[1][-1].__class__
if superclass is cls:
# subclassing MockObject
return _make_subclass(args[0], superclass.__display_name__,
superclass=superclass, attributes=args[2])
return super(_MockObject, cls).__new__(cls)
def __init__(self, *args, **kwargs):
# type: (Any, Any) -> None
self.__qualname__ = ''
def __len__(self):
# type: () -> int
return 0
def __contains__(self, key):
# type: (str) -> bool
return False
def __iter__(self):
# type: () -> Iterator
return iter([])
def __mro_entries__(self, bases):
# type: (Tuple) -> Tuple
return (self.__class__,)
def __getitem__(self, key):
# type: (str) -> _MockObject
return _make_subclass(key, self.__display_name__, self.__class__)()
def __getattr__(self, key):
# type: (str) -> _MockObject
return _make_subclass(key, self.__display_name__, self.__class__)()
def __call__(self, *args, **kw):
# type: (Any, Any) -> Any
if args and type(args[0]) in [FunctionType, MethodType]:
# Appears to be a decorator, pass through unchanged
return args[0]
return self
def __repr__(self):
# type: () -> str
return self.__display_name__
def _make_subclass(name, module, superclass=_MockObject, attributes=None):
# type: (str, str, Any, dict) -> Any
attrs = {'__module__': module, '__display_name__': module + '.' + name}
attrs.update(attributes or {})
return type(name, (superclass,), attrs)
class _MockModule(ModuleType):
"""Used by autodoc_mock_imports."""
__file__ = os.devnull
def __init__(self, name, loader=None):
# type: (str, _MockImporter) -> None
super().__init__(name)
self.__all__ = [] # type: List[str]
self.__path__ = [] # type: List[str]
if loader is not None:
warnings.warn('The loader argument for _MockModule is deprecated.',
RemovedInSphinx30Warning)
def __getattr__(self, name):
# type: (str) -> _MockObject
return _make_subclass(name, self.__name__)()
def __repr__(self):
# type: () -> str
return self.__name__
class _MockImporter(MetaPathFinder):
def __init__(self, names):
# type: (List[str]) -> None
self.names = names
self.mocked_modules = [] # type: List[str]
# enable hook by adding itself to meta_path
sys.meta_path.insert(0, self)
warnings.warn('_MockImporter is now deprecated.',
RemovedInSphinx30Warning)
def disable(self):
# type: () -> None
# remove `self` from `sys.meta_path` to disable import hook
sys.meta_path = [i for i in sys.meta_path if i is not self]
# remove mocked modules from sys.modules to avoid side effects after
# running auto-documenter
for m in self.mocked_modules:
if m in sys.modules:
del sys.modules[m]
def find_module(self, name, path=None):
# type: (str, Sequence[Union[bytes, str]]) -> Any
# check if name is (or is a descendant of) one of our base_packages
for n in self.names:
if n == name or name.startswith(n + '.'):
return self
return None
def load_module(self, name):
# type: (str) -> ModuleType
if name in sys.modules:
# module has already been imported, return it
return sys.modules[name]
else:
logger.debug('[autodoc] adding a mock module %s!', name)
module = _MockModule(name, self)
sys.modules[name] = module
self.mocked_modules.append(name)
return module
class MockLoader(Loader):
"""A loader for mocking."""
def __init__(self, finder):
# type: (MockFinder) -> None
super().__init__()
self.finder = finder
def create_module(self, spec):
# type: (ModuleSpec) -> ModuleType
logger.debug('[autodoc] adding a mock module as %s!', spec.name)
self.finder.mocked_modules.append(spec.name)
return _MockModule(spec.name)
def exec_module(self, module):
# type: (ModuleType) -> None
pass # nothing to do
class MockFinder(MetaPathFinder):
"""A finder for mocking."""
def __init__(self, modnames):
# type: (List[str]) -> None
super().__init__()
self.modnames = modnames
self.loader = MockLoader(self)
self.mocked_modules = [] # type: List[str]
def find_spec(self, fullname, path, target=None):
# type: (str, Sequence[Union[bytes, str]], ModuleType) -> ModuleSpec
for modname in self.modnames:
# check if fullname is (or is a descendant of) one of our targets
if modname == fullname or fullname.startswith(modname + '.'):
return ModuleSpec(fullname, self.loader)
return None
def invalidate_caches(self):
# type: () -> None
"""Invalidate mocked modules on sys.modules."""
for modname in self.mocked_modules:
sys.modules.pop(modname, None)
@contextlib.contextmanager
def mock(modnames):
# type: (List[str]) -> Generator[None, None, None]
"""Insert mock modules during context::
with mock(['target.module.name']):
# mock modules are enabled here
...
"""
try:
finder = MockFinder(modnames)
sys.meta_path.insert(0, finder)
yield
finally:
sys.meta_path.remove(finder)
finder.invalidate_caches()
def import_module(modname, warningiserror=False):
# type: (str, bool) -> Any
"""
@@ -343,3 +147,19 @@ def get_object_members(subject, objpath, attrgetter, analyzer=None):
members[name] = Attribute(name, True, INSTANCEATTR)
return members
from sphinx.ext.autodoc.mock import ( # NOQA
_MockImporter, _MockModule, _MockObject, MockFinder, MockLoader, mock
)
deprecated_alias('sphinx.ext.autodoc.importer',
{
'_MockImporter': _MockImporter,
'_MockModule': _MockModule,
'_MockObject': _MockObject,
'MockFinder': MockFinder,
'MockLoader': MockLoader,
'mock': mock,
},
RemovedInSphinx40Warning)

217
sphinx/ext/autodoc/mock.py Normal file
View File

@@ -0,0 +1,217 @@
"""
sphinx.ext.autodoc.mock
~~~~~~~~~~~~~~~~~~~~~~~
mock for autodoc
:copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import contextlib
import os
import sys
import warnings
from importlib.abc import Loader, MetaPathFinder
from importlib.machinery import ModuleSpec
from types import FunctionType, MethodType, ModuleType
from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.util import logging
if False:
# For type annotation
from typing import Any, Generator, Iterator, List, Sequence, Tuple, Union # NOQA
logger = logging.getLogger(__name__)
class _MockObject:
"""Used by autodoc_mock_imports."""
__display_name__ = '_MockObject'
def __new__(cls, *args, **kwargs):
# type: (Any, Any) -> Any
if len(args) == 3 and isinstance(args[1], tuple):
superclass = args[1][-1].__class__
if superclass is cls:
# subclassing MockObject
return _make_subclass(args[0], superclass.__display_name__,
superclass=superclass, attributes=args[2])
return super(_MockObject, cls).__new__(cls)
def __init__(self, *args, **kwargs):
# type: (Any, Any) -> None
self.__qualname__ = ''
def __len__(self):
# type: () -> int
return 0
def __contains__(self, key):
# type: (str) -> bool
return False
def __iter__(self):
# type: () -> Iterator
return iter([])
def __mro_entries__(self, bases):
# type: (Tuple) -> Tuple
return (self.__class__,)
def __getitem__(self, key):
# type: (str) -> _MockObject
return _make_subclass(key, self.__display_name__, self.__class__)()
def __getattr__(self, key):
# type: (str) -> _MockObject
return _make_subclass(key, self.__display_name__, self.__class__)()
def __call__(self, *args, **kw):
# type: (Any, Any) -> Any
if args and type(args[0]) in [FunctionType, MethodType]:
# Appears to be a decorator, pass through unchanged
return args[0]
return self
def __repr__(self):
# type: () -> str
return self.__display_name__
def _make_subclass(name, module, superclass=_MockObject, attributes=None):
# type: (str, str, Any, dict) -> Any
attrs = {'__module__': module, '__display_name__': module + '.' + name}
attrs.update(attributes or {})
return type(name, (superclass,), attrs)
class _MockModule(ModuleType):
"""Used by autodoc_mock_imports."""
__file__ = os.devnull
def __init__(self, name, loader=None):
# type: (str, _MockImporter) -> None
super().__init__(name)
self.__all__ = [] # type: List[str]
self.__path__ = [] # type: List[str]
if loader is not None:
warnings.warn('The loader argument for _MockModule is deprecated.',
RemovedInSphinx30Warning)
def __getattr__(self, name):
# type: (str) -> _MockObject
return _make_subclass(name, self.__name__)()
def __repr__(self):
# type: () -> str
return self.__name__
class _MockImporter(MetaPathFinder):
def __init__(self, names):
# type: (List[str]) -> None
self.names = names
self.mocked_modules = [] # type: List[str]
# enable hook by adding itself to meta_path
sys.meta_path.insert(0, self)
warnings.warn('_MockImporter is now deprecated.',
RemovedInSphinx30Warning)
def disable(self):
# type: () -> None
# remove `self` from `sys.meta_path` to disable import hook
sys.meta_path = [i for i in sys.meta_path if i is not self]
# remove mocked modules from sys.modules to avoid side effects after
# running auto-documenter
for m in self.mocked_modules:
if m in sys.modules:
del sys.modules[m]
def find_module(self, name, path=None):
# type: (str, Sequence[Union[bytes, str]]) -> Any
# check if name is (or is a descendant of) one of our base_packages
for n in self.names:
if n == name or name.startswith(n + '.'):
return self
return None
def load_module(self, name):
# type: (str) -> ModuleType
if name in sys.modules:
# module has already been imported, return it
return sys.modules[name]
else:
logger.debug('[autodoc] adding a mock module %s!', name)
module = _MockModule(name, self)
sys.modules[name] = module
self.mocked_modules.append(name)
return module
class MockLoader(Loader):
"""A loader for mocking."""
def __init__(self, finder):
# type: (MockFinder) -> None
super().__init__()
self.finder = finder
def create_module(self, spec):
# type: (ModuleSpec) -> ModuleType
logger.debug('[autodoc] adding a mock module as %s!', spec.name)
self.finder.mocked_modules.append(spec.name)
return _MockModule(spec.name)
def exec_module(self, module):
# type: (ModuleType) -> None
pass # nothing to do
class MockFinder(MetaPathFinder):
"""A finder for mocking."""
def __init__(self, modnames):
# type: (List[str]) -> None
super().__init__()
self.modnames = modnames
self.loader = MockLoader(self)
self.mocked_modules = [] # type: List[str]
def find_spec(self, fullname, path, target=None):
# type: (str, Sequence[Union[bytes, str]], ModuleType) -> ModuleSpec
for modname in self.modnames:
# check if fullname is (or is a descendant of) one of our targets
if modname == fullname or fullname.startswith(modname + '.'):
return ModuleSpec(fullname, self.loader)
return None
def invalidate_caches(self):
# type: () -> None
"""Invalidate mocked modules on sys.modules."""
for modname in self.mocked_modules:
sys.modules.pop(modname, None)
@contextlib.contextmanager
def mock(modnames):
# type: (List[str]) -> Generator[None, None, None]
"""Insert mock modules during context::
with mock(['target.module.name']):
# mock modules are enabled here
...
"""
try:
finder = MockFinder(modnames)
sys.meta_path.insert(0, finder)
yield
finally:
sys.meta_path.remove(finder)
finder.invalidate_caches()

View File

@@ -72,7 +72,8 @@ from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.environment.adapters.toctree import TocTree
from sphinx.ext.autodoc import get_documenters
from sphinx.ext.autodoc.directive import DocumenterBridge, Options
from sphinx.ext.autodoc.importer import import_module, mock
from sphinx.ext.autodoc.importer import import_module
from sphinx.ext.autodoc.mock import mock
from sphinx.locale import __
from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import import_object, rst, logging

View File

@@ -1,6 +1,6 @@
"""
test_ext_autodoc_importer
~~~~~~~~~~~~~~~~~~~~~~~~~
test_ext_autodoc_mock
~~~~~~~~~~~~~~~~~~~~~
Test the autodoc extension.
@@ -13,7 +13,7 @@ import sys
import pytest
from sphinx.ext.autodoc.importer import _MockModule, _MockObject, mock
from sphinx.ext.autodoc.mock import _MockModule, _MockObject, mock
def test_MockModule():