diff --git a/CHANGELOG.md b/CHANGELOG.md index 33bddefe..b1b2e7d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.18.1 + +- Fix mocked module import not working when used as guarded import + ## 1.18.0 - Support and require `nptyping>=2` diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py index cde07203..1a9952f5 100644 --- a/src/sphinx_autodoc_typehints/__init__.py +++ b/src/sphinx_autodoc_typehints/__init__.py @@ -12,6 +12,7 @@ from sphinx.config import Config from sphinx.environment import BuildEnvironment from sphinx.ext.autodoc import Options +from sphinx.ext.autodoc.mock import mock from sphinx.util import logging from sphinx.util.inspect import signature as sphinx_signature from sphinx.util.inspect import stringify_signature @@ -305,8 +306,8 @@ def _future_annotations_imported(obj: Any) -> bool: return bool(_annotations.compiler_flag == future_annotations) -def get_all_type_hints(obj: Any, name: str) -> dict[str, Any]: - result = _get_type_hint(name, obj) +def get_all_type_hints(autodoc_mock_imports: list[str], obj: Any, name: str) -> dict[str, Any]: + result = _get_type_hint(autodoc_mock_imports, name, obj) if not result: result = backfill_type_hints(obj, name) try: @@ -314,7 +315,7 @@ def get_all_type_hints(obj: Any, name: str) -> dict[str, Any]: except (AttributeError, TypeError): pass else: - result = _get_type_hint(name, obj) + result = _get_type_hint(autodoc_mock_imports, name, obj) return result @@ -322,7 +323,7 @@ def get_all_type_hints(obj: Any, name: str) -> dict[str, Any]: _TYPE_GUARD_IMPORTS_RESOLVED = set() -def _resolve_type_guarded_imports(obj: Any) -> None: +def _resolve_type_guarded_imports(autodoc_mock_imports: list[str], obj: Any) -> None: if hasattr(obj, "__module__") and obj.__module__ not in _TYPE_GUARD_IMPORTS_RESOLVED: _TYPE_GUARD_IMPORTS_RESOLVED.add(obj.__module__) if obj.__module__ not in sys.builtin_module_names: @@ -336,13 +337,14 @@ def _resolve_type_guarded_imports(obj: Any) -> None: for (_, part) in _TYPE_GUARD_IMPORT_RE.findall(module_code): guarded_code = textwrap.dedent(part) try: - exec(guarded_code, obj.__globals__) + with mock(autodoc_mock_imports): + exec(guarded_code, obj.__globals__) except Exception as exc: _LOGGER.warning(f"Failed guarded type import with {exc!r}") -def _get_type_hint(name: str, obj: Any) -> dict[str, Any]: - _resolve_type_guarded_imports(obj) +def _get_type_hint(autodoc_mock_imports: list[str], name: str, obj: Any) -> dict[str, Any]: + _resolve_type_guarded_imports(autodoc_mock_imports, obj) try: result = get_type_hints(obj) except (AttributeError, TypeError, RecursionError) as exc: @@ -497,7 +499,7 @@ def process_docstring( signature = sphinx_signature(obj) except (ValueError, TypeError): signature = None - type_hints = get_all_type_hints(obj, name) + type_hints = get_all_type_hints(app.config.autodoc_mock_imports, obj, name) app.config._annotation_globals = getattr(obj, "__globals__", {}) # type: ignore # config has no such attribute try: _inject_types_to_docstring(type_hints, signature, original_obj, app, what, name, lines) diff --git a/tests/roots/test-resolve-typing-guard/demo_typing_guard.py b/tests/roots/test-resolve-typing-guard/demo_typing_guard.py index 1399bf44..fe120ad7 100644 --- a/tests/roots/test-resolve-typing-guard/demo_typing_guard.py +++ b/tests/roots/test-resolve-typing-guard/demo_typing_guard.py @@ -10,6 +10,8 @@ from decimal import Decimal from typing import Sequence + from demo_typing_guard_dummy import AnotherClass # module contains mocked import # noqa: F401 + if typing.TYPE_CHECKING: from typing import AnyStr diff --git a/tests/roots/test-resolve-typing-guard/demo_typing_guard_dummy.py b/tests/roots/test-resolve-typing-guard/demo_typing_guard_dummy.py new file mode 100644 index 00000000..43964e24 --- /dev/null +++ b/tests/roots/test-resolve-typing-guard/demo_typing_guard_dummy.py @@ -0,0 +1,5 @@ +from viktor import AI # module part of autodoc_mock_imports # noqa: F401,SC100,SC200 + + +class AnotherClass: + ... diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index b5f0a009..0e169820 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -353,7 +353,11 @@ def test_format_annotation_both_libs(library: ModuleType, annotation: str, param def test_process_docstring_slot_wrapper() -> None: lines: list[str] = [] config = create_autospec( - Config, typehints_fully_qualified=False, simplify_optional_unions=False, typehints_formatter=None + Config, + typehints_fully_qualified=False, + simplify_optional_unions=False, + typehints_formatter=None, + autodoc_mock_imports=[], ) app: Sphinx = create_autospec(Sphinx, config=config) process_docstring(app, "class", "SlotWrapper", Slotted, None, lines) @@ -870,7 +874,11 @@ def __init__(bound_args): # noqa: N805 @pytest.mark.parametrize("obj", [cmp_to_key, 1]) def test_default_no_signature(obj: Any) -> None: config = create_autospec( - Config, typehints_fully_qualified=False, simplify_optional_unions=False, typehints_formatter=None + Config, + typehints_fully_qualified=False, + simplify_optional_unions=False, + typehints_formatter=None, + autodoc_mock_imports=[], ) app: Sphinx = create_autospec(Sphinx, config=config) lines: list[str] = [] @@ -888,6 +896,7 @@ def test_bound_class_method(method: FunctionType) -> None: always_document_param_types=True, typehints_defaults=True, typehints_formatter=None, + autodoc_mock_imports=[], ) app: Sphinx = create_autospec(Sphinx, config=config) process_docstring(app, "class", method.__qualname__, method, None, []) @@ -905,17 +914,20 @@ def test_syntax_error_backfill() -> None: @pytest.mark.sphinx("text", testroot="resolve-typing-guard") def test_resolve_typing_guard_imports(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: set_python_path() + app.config.autodoc_mock_imports = ["viktor"] # type: ignore # create flag app.build() assert "build succeeded" in status.getvalue() - pat = r'WARNING: Failed guarded type import with ImportError\("cannot import name \'missing\' from \'functools\'' err = warning.getvalue() + r = re.compile("WARNING: Failed guarded type import") + assert len(r.findall(err)) == 1 + pat = r'WARNING: Failed guarded type import with ImportError\("cannot import name \'missing\' from \'functools\'' assert re.search(pat, err) def test_no_source_code_type_guard() -> None: from csv import Error - _resolve_type_guarded_imports(Error) + _resolve_type_guarded_imports([], Error) @pytest.mark.sphinx("text", testroot="dummy")