2019-02-26 23:49:07 -06:00
|
|
|
"""
|
|
|
|
sphinx.ext.autodoc.mock
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
mock for autodoc
|
|
|
|
|
2019-12-31 20:15:42 -06:00
|
|
|
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
|
2019-02-26 23:49:07 -06:00
|
|
|
: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
|
2019-06-30 01:02:35 -05:00
|
|
|
from typing import Any, Generator, Iterator, List, Sequence, Tuple, Union
|
2019-02-26 23:49:07 -06:00
|
|
|
|
|
|
|
from sphinx.deprecation import RemovedInSphinx30Warning
|
|
|
|
from sphinx.util import logging
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
class _MockObject:
|
|
|
|
"""Used by autodoc_mock_imports."""
|
|
|
|
|
|
|
|
__display_name__ = '_MockObject'
|
|
|
|
|
2019-12-29 08:56:30 -06:00
|
|
|
def __new__(cls, *args: Any, **kwargs: Any) -> Any:
|
2019-02-26 23:49:07 -06:00
|
|
|
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])
|
|
|
|
|
2019-03-17 14:49:36 -05:00
|
|
|
return super().__new__(cls)
|
2019-02-26 23:49:07 -06:00
|
|
|
|
2019-12-29 08:56:30 -06:00
|
|
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
2019-02-26 23:49:07 -06:00
|
|
|
self.__qualname__ = ''
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def __len__(self) -> int:
|
2019-02-26 23:49:07 -06:00
|
|
|
return 0
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def __contains__(self, key: str) -> bool:
|
2019-02-26 23:49:07 -06:00
|
|
|
return False
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def __iter__(self) -> Iterator:
|
2019-02-26 23:49:07 -06:00
|
|
|
return iter([])
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def __mro_entries__(self, bases: Tuple) -> Tuple:
|
2019-02-26 23:49:07 -06:00
|
|
|
return (self.__class__,)
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def __getitem__(self, key: str) -> "_MockObject":
|
2019-02-26 23:49:07 -06:00
|
|
|
return _make_subclass(key, self.__display_name__, self.__class__)()
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def __getattr__(self, key: str) -> "_MockObject":
|
2019-02-26 23:49:07 -06:00
|
|
|
return _make_subclass(key, self.__display_name__, self.__class__)()
|
|
|
|
|
2019-12-30 02:41:00 -06:00
|
|
|
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
2019-10-06 09:31:07 -05:00
|
|
|
if args and type(args[0]) in [type, FunctionType, MethodType]:
|
2019-02-26 23:49:07 -06:00
|
|
|
# Appears to be a decorator, pass through unchanged
|
|
|
|
return args[0]
|
|
|
|
return self
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def __repr__(self) -> str:
|
2019-02-26 23:49:07 -06:00
|
|
|
return self.__display_name__
|
|
|
|
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def _make_subclass(name: str, module: str, superclass: Any = _MockObject,
|
|
|
|
attributes: Any = None) -> Any:
|
2019-02-26 23:49:07 -06:00
|
|
|
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
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def __init__(self, name: str, loader: "_MockImporter" = None) -> None:
|
2019-02-26 23:49:07 -06:00
|
|
|
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)
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def __getattr__(self, name: str) -> _MockObject:
|
2019-02-26 23:49:07 -06:00
|
|
|
return _make_subclass(name, self.__name__)()
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def __repr__(self) -> str:
|
2019-02-26 23:49:07 -06:00
|
|
|
return self.__name__
|
|
|
|
|
|
|
|
|
|
|
|
class _MockImporter(MetaPathFinder):
|
2019-06-30 01:02:35 -05:00
|
|
|
def __init__(self, names: List[str]) -> None:
|
2019-02-26 23:49:07 -06:00
|
|
|
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)
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def disable(self) -> None:
|
2019-02-26 23:49:07 -06:00
|
|
|
# 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]
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def find_module(self, name: str, path: Sequence[Union[bytes, str]] = None) -> Any:
|
2019-02-26 23:49:07 -06:00
|
|
|
# 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
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def load_module(self, name: str) -> ModuleType:
|
2019-02-26 23:49:07 -06:00
|
|
|
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."""
|
2019-06-30 01:02:35 -05:00
|
|
|
def __init__(self, finder: "MockFinder") -> None:
|
2019-02-26 23:49:07 -06:00
|
|
|
super().__init__()
|
|
|
|
self.finder = finder
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def create_module(self, spec: ModuleSpec) -> ModuleType:
|
2019-02-26 23:49:07 -06:00
|
|
|
logger.debug('[autodoc] adding a mock module as %s!', spec.name)
|
|
|
|
self.finder.mocked_modules.append(spec.name)
|
|
|
|
return _MockModule(spec.name)
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def exec_module(self, module: ModuleType) -> None:
|
2019-02-26 23:49:07 -06:00
|
|
|
pass # nothing to do
|
|
|
|
|
|
|
|
|
|
|
|
class MockFinder(MetaPathFinder):
|
|
|
|
"""A finder for mocking."""
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def __init__(self, modnames: List[str]) -> None:
|
2019-02-26 23:49:07 -06:00
|
|
|
super().__init__()
|
|
|
|
self.modnames = modnames
|
|
|
|
self.loader = MockLoader(self)
|
|
|
|
self.mocked_modules = [] # type: List[str]
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def find_spec(self, fullname: str, path: Sequence[Union[bytes, str]],
|
|
|
|
target: ModuleType = None) -> ModuleSpec:
|
2019-02-26 23:49:07 -06:00
|
|
|
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
|
|
|
|
|
2019-06-30 01:02:35 -05:00
|
|
|
def invalidate_caches(self) -> None:
|
2019-02-26 23:49:07 -06:00
|
|
|
"""Invalidate mocked modules on sys.modules."""
|
|
|
|
for modname in self.mocked_modules:
|
|
|
|
sys.modules.pop(modname, None)
|
|
|
|
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
2019-06-30 01:02:35 -05:00
|
|
|
def mock(modnames: List[str]) -> Generator[None, None, None]:
|
2019-02-26 23:49:07 -06:00
|
|
|
"""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()
|