summaryrefslogtreecommitdiff
path: root/sphinx/ext/autodoc.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/ext/autodoc.py')
-rw-r--r--sphinx/ext/autodoc.py124
1 files changed, 89 insertions, 35 deletions
diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py
index f78f9cbce..9e483854f 100644
--- a/sphinx/ext/autodoc.py
+++ b/sphinx/ext/autodoc.py
@@ -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 __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 __call__(self, *args, **kwargs):
- # type: (Any, Any) -> _MockModule
+ 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
- else:
- return _MockModule()
+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 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
+ 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
- if modname not in sys.modules:
- mod = _MockModule()
- sys.modules[modname] = mod # type: ignore
+ 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
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