Merge pull request #2961 from rjarry/mock_imports

enhance autodoc_mock_imports
This commit is contained in:
Takayuki SHIMIZUKAWA 2017-04-19 16:20:22 +09:00 committed by GitHub
commit 1b6ac8b22d
2 changed files with 99 additions and 38 deletions

View File

@ -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

View File

@ -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