Skip to content

Commit

Permalink
Fix mock imports on guarded imports (#225)
Browse files Browse the repository at this point in the history
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
khameeteman and pre-commit-ci[bot] authored Apr 13, 2022
1 parent 4d5867d commit 73aa9b6
Show file tree
Hide file tree
Showing 5 changed files with 37 additions and 12 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`
Expand Down
18 changes: 10 additions & 8 deletions src/sphinx_autodoc_typehints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -305,24 +306,24 @@ 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:
obj.__annotations__ = result
except (AttributeError, TypeError):
pass
else:
result = _get_type_hint(name, obj)
result = _get_type_hint(autodoc_mock_imports, name, obj)
return result


_TYPE_GUARD_IMPORT_RE = re.compile(r"\nif (typing.)?TYPE_CHECKING:[^\n]*([\s\S]*?)(?=\n\S)")
_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:
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions tests/roots/test-resolve-typing-guard/demo_typing_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from viktor import AI # module part of autodoc_mock_imports # noqa: F401,SC100,SC200


class AnotherClass:
...
20 changes: 16 additions & 4 deletions tests/test_sphinx_autodoc_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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] = []
Expand All @@ -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, [])
Expand All @@ -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")
Expand Down

0 comments on commit 73aa9b6

Please sign in to comment.