diff --git a/CHANGES b/CHANGES index 931c0e0dcdd..1985b74c434 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,7 @@ Deprecated Features added -------------- +* #10133: autodoc: Crashed when mocked module is used for type annotation * #9494, #9456: html search: Add a config variable :confval:`html_show_search_summary` to enable/disable the search summaries diff --git a/sphinx/ext/autodoc/mock.py b/sphinx/ext/autodoc/mock.py index 36b2836f3e2..7bd0b1ea017 100644 --- a/sphinx/ext/autodoc/mock.py +++ b/sphinx/ext/autodoc/mock.py @@ -154,6 +154,11 @@ def mock(modnames: List[str]) -> Generator[None, None, None]: 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 diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index d9b63e046e3..5cd0c230ef2 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -116,6 +116,7 @@ def restify(cls: Optional[Type], mode: str = 'fully-qualified-except-typing') -> 'smart' Show the name of the annotation. """ + from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading from sphinx.util import inspect # lazy loading if mode == 'smart': @@ -130,6 +131,10 @@ def restify(cls: Optional[Type], mode: str = 'fully-qualified-except-typing') -> return '...' elif isinstance(cls, str): return cls + elif ismockmodule(cls): + return ':py:class:`%s%s`' % (modprefix, cls.__name__) + elif ismock(cls): + return ':py:class:`%s%s.%s`' % (modprefix, cls.__module__, cls.__name__) elif cls in INVALID_BUILTIN_CLASSES: return ':py:class:`%s%s`' % (modprefix, INVALID_BUILTIN_CLASSES[cls]) elif inspect.isNewType(cls): @@ -335,6 +340,7 @@ def stringify(annotation: Any, mode: str = 'fully-qualified-except-typing') -> s 'fully-qualified' Show the module name and qualified name of the annotation. """ + from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading from sphinx.util import inspect # lazy loading if mode == 'smart': @@ -364,6 +370,10 @@ def stringify(annotation: Any, mode: str = 'fully-qualified-except-typing') -> s return repr(annotation) elif annotation is NoneType: return 'None' + elif ismockmodule(annotation): + return modprefix + annotation.__name__ + elif ismock(annotation): + return modprefix + '%s.%s' % (annotation.__module__, annotation.__name__) elif annotation in INVALID_BUILTIN_CLASSES: return modprefix + INVALID_BUILTIN_CLASSES[annotation] elif str(annotation).startswith('typing.Annotated'): # for py310+ diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index c061fa08518..7d81fee5d7b 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -213,6 +213,7 @@ def test_restify_broken_type_hints(): def test_restify_mock(): with mock(['unknown']): import unknown + assert restify(unknown) == ':py:class:`unknown`' assert restify(unknown.secret.Class) == ':py:class:`unknown.secret.Class`' assert restify(unknown.secret.Class, "smart") == ':py:class:`~unknown.secret.Class`' @@ -480,5 +481,6 @@ def test_stringify_broken_type_hints(): def test_stringify_mock(): with mock(['unknown']): import unknown + assert stringify(unknown) == 'unknown' assert stringify(unknown.secret.Class) == 'unknown.secret.Class' assert stringify(unknown.secret.Class, "smart") == 'unknown.secret.Class'