diff --git a/CHANGES b/CHANGES index 48ae4db57..c96659a84 100644 --- a/CHANGES +++ b/CHANGES @@ -155,6 +155,7 @@ Features added * #4182: autodoc: Support :confval:`suppress_warnings` * #5533: autodoc: :confval:`autodoc_default_options` supports ``member-order`` +* #5394: autodoc: Display readable names in type annotations for mocked objects * #4018: htmlhelp: Add :confval:`htmlhelp_file_suffix` and :confval:`htmlhelp_link_suffix` * #5559: text: Support complex tables (colspan and rowspan) diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 55e4ce6cc..f19464b13 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -32,13 +32,18 @@ logger = logging.getLogger(__name__) class _MockObject: """Used by autodoc_mock_imports.""" + __display_name__ = '_MockObject' + def __new__(cls, *args, **kwargs): # type: (Any, Any) -> Any - if len(args) == 3 and isinstance(args[1], tuple) and args[1][-1].__class__ is cls: - # subclassing MockObject - return type(args[0], (_MockObject,), args[2], **kwargs) # type: ignore - else: - return super(_MockObject, cls).__new__(cls) + 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(_MockObject, cls).__new__(cls) def __init__(self, *args, **kwargs): # type: (Any, Any) -> None @@ -62,11 +67,11 @@ class _MockObject: def __getitem__(self, key): # type: (str) -> _MockObject - return self + return _make_subclass(key, self.__display_name__, self.__class__)() def __getattr__(self, key): # type: (str) -> _MockObject - return self + return _make_subclass(key, self.__display_name__, self.__class__)() def __call__(self, *args, **kw): # type: (Any, Any) -> Any @@ -75,6 +80,18 @@ class _MockObject: return args[0] return self + def __repr__(self): + # type: () -> str + return self.__display_name__ + + +def _make_subclass(name, module, superclass=_MockObject, attributes=None): + # type: (str, str, Any, dict) -> Any + attrs = {'__module__': module, '__display_name__': module + '.' + name} + attrs.update(attributes or {}) + + return type(name, (superclass,), attrs) + class _MockModule(ModuleType): """Used by autodoc_mock_imports.""" @@ -92,9 +109,11 @@ class _MockModule(ModuleType): def __getattr__(self, name): # type: (str) -> _MockObject - o = _MockObject() - o.__module__ = self.__name__ - return o + return _make_subclass(name, self.__name__)() + + def __repr__(self): + # type: () -> str + return self.__name__ class _MockImporter(MetaPathFinder): diff --git a/tests/roots/test-ext-autodoc/target/need_mocks.py b/tests/roots/test-ext-autodoc/target/need_mocks.py index 4f83579a4..b8a661581 100644 --- a/tests/roots/test-ext-autodoc/target/need_mocks.py +++ b/tests/roots/test-ext-autodoc/target/need_mocks.py @@ -15,6 +15,11 @@ def decoratedFunction(): return None +def func(arg: missing_module.Class): + """a function takes mocked object as an argument""" + pass + + class TestAutodoc(object): """TestAutodoc docstring.""" @missing_name diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index a25a13639..a56226b42 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1345,7 +1345,7 @@ def test_autofunction_for_callable(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_mocked_module_imports(app, warning): # no autodoc_mock_imports - options = {"members": 'TestAutodoc,decoratedFunction'} + options = {"members": 'TestAutodoc,decoratedFunction,func'} actual = do_autodoc(app, 'module', 'target.need_mocks', options) assert list(actual) == [] assert "autodoc: failed to import module 'need_mocks'" in warning.getvalue() @@ -1382,6 +1382,12 @@ def test_mocked_module_imports(app, warning): ' :module: target.need_mocks', '', ' decoratedFunction docstring', + ' ', + '', + '.. py:function:: func(arg: missing_module.Class)', + ' :module: target.need_mocks', + '', + ' a function takes mocked object as an argument', ' ' ] assert warning.getvalue() == '' diff --git a/tests/test_ext_autodoc_importer.py b/tests/test_ext_autodoc_importer.py index ea40632ef..08920cc7e 100644 --- a/tests/test_ext_autodoc_importer.py +++ b/tests/test_ext_autodoc_importer.py @@ -16,6 +16,21 @@ import pytest from sphinx.ext.autodoc.importer import _MockModule, _MockObject, mock +def test_MockModule(): + mock = _MockModule('mocked_module', None) + assert isinstance(mock.some_attr, _MockObject) + assert isinstance(mock.some_method, _MockObject) + assert isinstance(mock.attr1.attr2, _MockObject) + assert isinstance(mock.attr1.attr2.meth(), _MockObject) + + assert repr(mock.some_attr) == 'mocked_module.some_attr' + assert repr(mock.some_method) == 'mocked_module.some_method' + assert repr(mock.attr1.attr2) == 'mocked_module.attr1.attr2' + assert repr(mock.attr1.attr2.meth) == 'mocked_module.attr1.attr2.meth' + + assert repr(mock) == 'mocked_module' + + def test_MockObject(): mock = _MockObject() assert isinstance(mock.some_attr, _MockObject) @@ -25,6 +40,7 @@ def test_MockObject(): class SubClass(mock.SomeClass): """docstring of SubClass""" + def method(self): return "string"