199 lines
5.8 KiB
Python
199 lines
5.8 KiB
Python
|
"""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
|