From 1a50d34520a54b0f2055dd05ff537e2a84ea2e1f Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 4 Dec 2018 22:49:51 +0900 Subject: [PATCH] refactor ``mock()`` to based on PEP-451 implementation --- CHANGES | 2 + doc/extdev/index.rst | 10 +++++ sphinx/ext/autodoc/importer.py | 77 ++++++++++++++++++++++++++++++---- 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/CHANGES b/CHANGES index d3d93568b..b095a84e7 100644 --- a/CHANGES +++ b/CHANGES @@ -39,12 +39,14 @@ Deprecated ``autodoc.DocstringSignatureMixin.get_doc()``, ``autodoc.DocstringSignatureMixin._find_signature()``, and ``autodoc.ClassDocumenter.get_doc()`` are deprecated. +* The ``importer`` argument of ``sphinx.ext.autodoc.importer._MockModule`` * The ``nodetype`` argument of ``sphinx.search.WordCollector. is_meta_keywords()`` * The ``suffix`` argument of ``env.doc2path()`` is deprecated. * The string style ``base`` argument of ``env.doc2path()`` is deprecated. * ``sphinx.application.Sphinx._setting_up_extension`` * ``sphinx.ext.config.check_unicode()`` +* ``sphinx.ext.autodoc.importer._MockImporter`` * ``sphinx.ext.doctest.doctest_encode()`` * ``sphinx.testing.util.remove_unicode_literal()`` * ``sphinx.util.force_decode()`` diff --git a/doc/extdev/index.rst b/doc/extdev/index.rst index 48cbe90e4..2c1765a83 100644 --- a/doc/extdev/index.rst +++ b/doc/extdev/index.rst @@ -187,6 +187,16 @@ The following is a list of deprecated interfaces. - 3.0 - N/A + * - The ``importer`` argument of ``sphinx.ext.autodoc.importer._MockModule`` + - 2.0 + - 3.0 + - N/A + + * - ``sphinx.ext.autodoc.importer._MockImporter`` + - 2.0 + - 3.0 + - N/A + * - ``sphinx.writers.latex.LaTeXTranslator._make_visit_admonition()`` - 2.0 - 3.0 diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 47f2df915..0b921e93f 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -14,14 +14,17 @@ 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.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, Tuple # NOQA + from typing import Any, Callable, Dict, Generator, Iterator, List, Optional, Sequence, Tuple, Union # NOQA from sphinx.util.typing import unicode # NOQA logger = logging.getLogger(__name__) @@ -78,13 +81,16 @@ class _MockModule(ModuleType): """Used by autodoc_mock_imports.""" __file__ = '/dev/null' - def __init__(self, name, loader): + def __init__(self, name, loader=None): # type: (str, _MockImporter) -> None - self.__name__ = self.__package__ = name - self.__loader__ = loader + super(_MockModule, self).__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 o = _MockObject() @@ -100,6 +106,9 @@ class _MockImporter: # 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 @@ -131,14 +140,66 @@ class _MockImporter: return module +class MockLoader(Loader): + """A loader for mocking.""" + def __init__(self, finder): + # type: (MockFinder) -> None + super(MockLoader, self).__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(MockFinder, self).__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(names): - # type: (List[str]) -> Generator +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: - importer = _MockImporter(names) + finder = MockFinder(modnames) + sys.meta_path.insert(0, finder) yield finally: - importer.disable() + sys.meta_path.remove(finder) + finder.invalidate_caches() def import_module(modname, warningiserror=False):