mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge pull request #2961 from rjarry/mock_imports
enhance autodoc_mock_imports
This commit is contained in:
commit
1b6ac8b22d
@ -371,7 +371,14 @@ There are also new config values that you can set:
|
||||
|
||||
This value contains a list of modules to be mocked up. This is useful when
|
||||
some external dependencies are not met at build time and break the building
|
||||
process.
|
||||
process. You may only specify the root package of the dependencies
|
||||
themselves and ommit the sub-modules:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
autodoc_mock_imports = ["django"]
|
||||
|
||||
Will mock all imports under the ``django`` package.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
|
@ -16,7 +16,7 @@ import sys
|
||||
import inspect
|
||||
import traceback
|
||||
import warnings
|
||||
from types import FunctionType, BuiltinFunctionType, MethodType
|
||||
from types import FunctionType, BuiltinFunctionType, MethodType, ModuleType
|
||||
|
||||
from six import PY2, iterkeys, iteritems, itervalues, text_type, class_types, \
|
||||
string_types, StringIO
|
||||
@ -41,7 +41,6 @@ from sphinx.util.docstrings import prepare_docstring
|
||||
if False:
|
||||
# For type annotation
|
||||
from typing import Any, Callable, Dict, Iterator, List, Sequence, Set, Tuple, Type, Union # NOQA
|
||||
from types import ModuleType # NOQA
|
||||
from docutils.utils import Reporter # NOQA
|
||||
from sphinx.application import Sphinx # NOQA
|
||||
|
||||
@ -107,49 +106,102 @@ class Options(dict):
|
||||
return None
|
||||
|
||||
|
||||
class _MockModule(object):
|
||||
class _MockObject(object):
|
||||
"""Used by autodoc_mock_imports."""
|
||||
__file__ = '/dev/null'
|
||||
__path__ = '/dev/null'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# type: (Any, Any) -> None
|
||||
self.__all__ = [] # type: List[str]
|
||||
pass
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
# type: (Any, Any) -> _MockModule
|
||||
def __len__(self):
|
||||
# type: () -> int
|
||||
return 0
|
||||
|
||||
def __contains__(self, key):
|
||||
# type: (str) -> bool
|
||||
return False
|
||||
|
||||
def __iter__(self):
|
||||
# type: () -> None
|
||||
pass
|
||||
|
||||
def __getitem__(self, key):
|
||||
# type: (str) -> _MockObject
|
||||
return self
|
||||
|
||||
def __getattr__(self, key):
|
||||
# type: (str) -> _MockObject
|
||||
return self
|
||||
|
||||
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 _MockModule()
|
||||
return self
|
||||
|
||||
def _append_submodule(self, submod):
|
||||
# type: (str) -> None
|
||||
self.__all__.append(submod)
|
||||
|
||||
@classmethod
|
||||
def __getattr__(cls, name):
|
||||
# type: (unicode) -> Any
|
||||
if name[0] == name[0].upper():
|
||||
# Not very good, we assume Uppercase names are classes...
|
||||
mocktype = type(name, (), {}) # type: ignore
|
||||
mocktype.__module__ = __name__
|
||||
return mocktype
|
||||
class _MockModule(ModuleType):
|
||||
"""Used by autodoc_mock_imports."""
|
||||
__file__ = '/dev/null'
|
||||
|
||||
def __init__(self, name, loader):
|
||||
# type: (str, _MockImporter) -> None
|
||||
self.__name__ = self.__package__ = name
|
||||
self.__loader__ = loader
|
||||
self.__all__ = [] # type: List[str]
|
||||
self.__path__ = [] # type: List[str]
|
||||
|
||||
def __getattr__(self, name):
|
||||
# type: (str) -> _MockObject
|
||||
o = _MockObject()
|
||||
o.__module__ = self.__name__
|
||||
return o
|
||||
|
||||
|
||||
class _MockImporter(object):
|
||||
|
||||
def __init__(self, names):
|
||||
# type: (List[str]) -> None
|
||||
self.base_packages = set() # type: Set[str]
|
||||
for n in names:
|
||||
# Convert module names:
|
||||
# ['a.b.c', 'd.e']
|
||||
# to a set of base packages:
|
||||
# set(['a', 'd'])
|
||||
self.base_packages.add(n.split('.')[0])
|
||||
self.mocked_modules = [] # type: List[str]
|
||||
self.orig_meta_path = sys.meta_path
|
||||
# enable hook by adding itself to meta_path
|
||||
sys.meta_path = sys.meta_path + [self]
|
||||
|
||||
def disable(self):
|
||||
# restore original meta_path to disable import hook
|
||||
sys.meta_path = self.orig_meta_path
|
||||
# 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, str) -> Any
|
||||
base_package = name.split('.')[0]
|
||||
if base_package in self.base_packages:
|
||||
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:
|
||||
return _MockModule()
|
||||
|
||||
|
||||
def mock_import(modname):
|
||||
# type: (str) -> None
|
||||
if '.' in modname:
|
||||
pkg, _n, mods = modname.rpartition('.')
|
||||
mock_import(pkg)
|
||||
if isinstance(sys.modules[pkg], _MockModule):
|
||||
sys.modules[pkg]._append_submodule(mods) # type: ignore
|
||||
|
||||
if modname not in sys.modules:
|
||||
mod = _MockModule()
|
||||
sys.modules[modname] = mod # type: ignore
|
||||
logger.debug('[autodoc] adding a mock module %s!', name)
|
||||
module = _MockModule(name, self)
|
||||
sys.modules[name] = module
|
||||
self.mocked_modules.append(name)
|
||||
return module
|
||||
|
||||
|
||||
ALL = object()
|
||||
@ -587,11 +639,11 @@ class Documenter(object):
|
||||
if self.objpath:
|
||||
logger.debug('[autodoc] from %s import %s',
|
||||
self.modname, '.'.join(self.objpath))
|
||||
# always enable mock import hook
|
||||
# it will do nothing if autodoc_mock_imports is empty
|
||||
import_hook = _MockImporter(self.env.config.autodoc_mock_imports)
|
||||
try:
|
||||
logger.debug('[autodoc] import %s', self.modname)
|
||||
for modname in self.env.config.autodoc_mock_imports:
|
||||
logger.debug('[autodoc] adding a mock module %s!', modname)
|
||||
mock_import(modname)
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", category=ImportWarning)
|
||||
__import__(self.modname)
|
||||
@ -628,6 +680,8 @@ class Documenter(object):
|
||||
self.directive.warn(errmsg)
|
||||
self.env.note_reread()
|
||||
return False
|
||||
finally:
|
||||
import_hook.disable()
|
||||
|
||||
def get_real_modname(self):
|
||||
# type: () -> str
|
||||
|
Loading…
Reference in New Issue
Block a user