| """mock for autodoc""" |
| |
| from __future__ import annotations |
| |
| import contextlib |
| import os |
| import sys |
| from importlib.abc import Loader, MetaPathFinder |
| from importlib.machinery import ModuleSpec |
| from types import MethodType, ModuleType |
| from typing import TYPE_CHECKING, Any |
| |
| from sphinx.util import logging |
| from sphinx.util.inspect import isboundmethod, safe_getattr |
| |
| if TYPE_CHECKING: |
| from collections.abc import Generator, Iterator, Sequence |
| |
| logger = logging.getLogger(__name__) |
| |
| |
| class _MockObject: |
| """Used by autodoc_mock_imports.""" |
| |
| __display_name__ = '_MockObject' |
| __name__ = '' |
| __sphinx_mock__ = True |
| __sphinx_decorator_args__: tuple[Any, ...] = () |
| |
| def __new__(cls, *args: Any, **kwargs: Any) -> Any: |
| 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]) |
| |
| return super().__new__(cls) |
| |
| def __init__(self, *args: Any, **kwargs: Any) -> None: |
| self.__qualname__ = self.__name__ |
| |
| def __len__(self) -> int: |
| return 0 |
| |
| def __contains__(self, key: str) -> bool: |
| return False |
| |
| def __iter__(self) -> Iterator: |
| return iter([]) |
| |
| def __mro_entries__(self, bases: tuple) -> tuple: |
| return (self.__class__,) |
| |
| def __getitem__(self, key: Any) -> _MockObject: |
| return _make_subclass(str(key), self.__display_name__, self.__class__)() |
| |
| def __getattr__(self, key: str) -> _MockObject: |
| return _make_subclass(key, self.__display_name__, self.__class__)() |
| |
| def __call__(self, *args: Any, **kwargs: Any) -> Any: |
| call = self.__class__() |
| call.__sphinx_decorator_args__ = args |
| return call |
| |
| def __repr__(self) -> str: |
| return self.__display_name__ |
| |
| |
| def _make_subclass(name: str, module: str, superclass: Any = _MockObject, |
| attributes: Any = None, decorator_args: tuple = ()) -> Any: |
| attrs = {'__module__': module, |
| '__display_name__': module + '.' + name, |
| '__name__': name, |
| '__sphinx_decorator_args__': decorator_args} |
| attrs.update(attributes or {}) |
| |
| return type(name, (superclass,), attrs) |
| |
| |
| class _MockModule(ModuleType): |
| """Used by autodoc_mock_imports.""" |
| __file__ = os.devnull |
| __sphinx_mock__ = True |
| |
| def __init__(self, name: str) -> None: |
| super().__init__(name) |
| self.__all__: list[str] = [] |
| self.__path__: list[str] = [] |
| |
| def __getattr__(self, name: str) -> _MockObject: |
| return _make_subclass(name, self.__name__)() |
| |
| def __repr__(self) -> str: |
| return self.__name__ |
| |
| |
| class MockLoader(Loader): |
| """A loader for mocking.""" |
| def __init__(self, finder: MockFinder) -> None: |
| super().__init__() |
| self.finder = finder |
| |
| def create_module(self, spec: 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: ModuleType) -> None: |
| pass # nothing to do |
| |
| |
| class MockFinder(MetaPathFinder): |
| """A finder for mocking.""" |
| |
| def __init__(self, modnames: list[str]) -> None: |
| super().__init__() |
| self.modnames = modnames |
| self.loader = MockLoader(self) |
| self.mocked_modules: list[str] = [] |
| |
| def find_spec(self, fullname: str, path: Sequence[bytes | str] | None, |
| target: ModuleType | None = None) -> ModuleSpec | None: |
| 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) -> None: |
| """Invalidate mocked modules on sys.modules.""" |
| for modname in self.mocked_modules: |
| sys.modules.pop(modname, None) |
| |
| |
| @contextlib.contextmanager |
| def mock(modnames: list[str]) -> Generator[None, None, None]: |
| """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() |
| |
| |
| def ismockmodule(subject: Any) -> bool: |
| """Check if the object is a mocked module.""" |
| return isinstance(subject, _MockModule) |
| |
| |
| def ismock(subject: Any) -> bool: |
| """Check if the object is mocked.""" |
| # check the object has '__sphinx_mock__' attribute |
| try: |
| if safe_getattr(subject, '__sphinx_mock__', None) is None: |
| return False |
| except AttributeError: |
| return False |
| |
| # check the object is mocked module |
| if isinstance(subject, _MockModule): |
| return True |
| |
| # check the object is bound method |
| if isinstance(subject, MethodType) and isboundmethod(subject): |
| tmp_subject = subject.__func__ |
| else: |
| tmp_subject = subject |
| |
| try: |
| # check the object is mocked object |
| __mro__ = safe_getattr(type(tmp_subject), '__mro__', []) |
| if len(__mro__) > 2 and __mro__[-2] is _MockObject: |
| # A mocked object has a MRO that ends with (..., _MockObject, object). |
| return True |
| except AttributeError: |
| pass |
| |
| return False |
| |
| |
| def undecorate(subject: _MockObject) -> Any: |
| """Unwrap mock if *subject* is decorated by mocked object. |
| |
| If not decorated, returns given *subject* itself. |
| """ |
| if ismock(subject) and subject.__sphinx_decorator_args__: |
| return subject.__sphinx_decorator_args__[0] |
| else: |
| return subject |