"""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
from sphinx.util import logging
from sphinx.util.inspect import isboundmethod, safe_getattr
if TYPE_CHECKING:
from collections.abc import Iterator, Sequence
from typing import Any
from typing_extensions import TypeIs
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[Any]:
return iter(())
def __mro_entries__(self, bases: tuple[Any, ...]) -> tuple[type, ...]:
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, ...] = ()) -> 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]) -> Iterator[None]:
"""Insert mock modules during context::
with mock(['target.module.name']):
# mock modules are enabled here
...
"""
finder = MockFinder(modnames)
try:
sys.meta_path.insert(0, finder)
yield
finally:
sys.meta_path.remove(finder)
finder.invalidate_caches()
def ismockmodule(subject: Any) -> TypeIs[_MockModule]:
"""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