Skip to content

Resolve type guard imports #201

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## dev

- Resolve type guard imports before evaluating annotations for objects
- Fix crash when the `inspect` module returns an invalid python syntax source

## 1.14.1
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ other =
*\sphinx-autodoc-typehints

[coverage:report]
fail_under = 55
fail_under = 78

[coverage:html]
show_contexts = true
Expand Down
65 changes: 35 additions & 30 deletions src/sphinx_autodoc_typehints/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import inspect
import re
import sys
import textwrap
import typing
Expand Down Expand Up @@ -252,43 +253,48 @@ def _future_annotations_imported(obj: Any) -> bool:


def get_all_type_hints(obj: Any, name: str) -> dict[str, Any]:
rv = {}

result = _get_type_hint(name, obj)
if result:
return result
result = backfill_type_hints(obj, name)
try:
rv = get_type_hints(obj)
except (AttributeError, TypeError, RecursionError) as exc:
# Introspecting a slot wrapper will raise TypeError, and and some recursive type
# definitions will cause a RecursionError (https://github.com/python/typing/issues/574).
obj.__annotations__ = result
except (AttributeError, TypeError):
return result
return _get_type_hint(name, obj)

# If one is using PEP563 annotations, Python will raise a (e.g.,)
# TypeError("TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'")
# on 'str | None', therefore we accept TypeErrors with that error message
# if 'annotations' is imported from '__future__'.
if isinstance(exc, TypeError) and _future_annotations_imported(obj) and "unsupported operand type" in str(exc):
rv = obj.__annotations__
except NameError as exc:
_LOGGER.warning('Cannot resolve forward reference in type annotations of "%s": %s', name, exc)
rv = obj.__annotations__

if rv:
return rv
_TYPE_GUARD_IMPORT_RE = re.compile(r"if (typing.)?TYPE_CHECKING:([\s\S]*?)(?=\n\S)")
_TYPE_GUARD_IMPORTS_RESOLVED = set()

rv = backfill_type_hints(obj, name)

try:
obj.__annotations__ = rv
except (AttributeError, TypeError):
return rv
def _resolve_type_guarded_imports(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:
module = inspect.getmodule(obj)
if module:
module_code = inspect.getsource(module)
for (_, part) in _TYPE_GUARD_IMPORT_RE.findall(module_code):
module_code = textwrap.dedent(part)
exec(module_code, obj.__globals__)


def _get_type_hint(name: str, obj: Any) -> dict[str, Any]:
_resolve_type_guarded_imports(obj)
try:
rv = get_type_hints(obj)
except (AttributeError, TypeError):
pass
result = get_type_hints(obj)
except (AttributeError, TypeError, RecursionError) as exc:
# TypeError - slot wrapper, PEP-563 when part of new syntax not supported
# RecursionError - some recursive type definitions https://github.com/python/typing/issues/574
if isinstance(exc, TypeError) and _future_annotations_imported(obj) and "unsupported operand type" in str(exc):
result = obj.__annotations__
else:
result = {}
except NameError as exc:
_LOGGER.warning('Cannot resolve forward reference in type annotations of "%s": %s', name, exc)
rv = obj.__annotations__

return rv
result = obj.__annotations__
return result


def backfill_type_hints(obj: Any, name: str) -> dict[str, Any]:
Expand All @@ -305,11 +311,9 @@ def backfill_type_hints(obj: Any, name: str) -> dict[str, Any]:

def _one_child(module: Module) -> stmt | None:
children = module.body # use the body to ignore type comments

if len(children) != 1:
_LOGGER.warning('Did not get exactly one node from AST for "%s", got %s', name, len(children))
return None

return children[0]

try:
Expand Down Expand Up @@ -526,4 +530,5 @@ def setup(app: Sphinx) -> dict[str, bool]:
"normalize_source_lines",
"process_docstring",
"process_signature",
"backfill_type_hints",
]
9 changes: 9 additions & 0 deletions tests/roots/test-resolve-typing-guard/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import pathlib
import sys

master_doc = "index"
sys.path.insert(0, str(pathlib.Path(__file__).parent))
extensions = [
"sphinx.ext.autodoc",
"sphinx_autodoc_typehints",
]
32 changes: 32 additions & 0 deletions tests/roots/test-resolve-typing-guard/demo_typing_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Module demonstrating imports that are type guarded"""
from __future__ import annotations

import typing
from builtins import ValueError # handle does not have __module__
from functools import cmp_to_key # has __module__ but cannot get module as is builtin
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from decimal import Decimal
from typing import Sequence

if typing.TYPE_CHECKING:
from typing import AnyStr


def a(f: Decimal, s: AnyStr) -> Sequence[AnyStr | Decimal]:
"""
Do.

:param f: first
:param s: second
:return: result
"""
return [f, s]


__all__ = [
"a",
"ValueError",
"cmp_to_key",
]
2 changes: 2 additions & 0 deletions tests/roots/test-resolve-typing-guard/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.. automodule:: demo_typing_guard
:members:
8 changes: 8 additions & 0 deletions tests/test_sphinx_autodoc_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,3 +761,11 @@ def test_syntax_error_backfill() -> None:
lambda x: x)
# fmt: on
backfill_type_hints(func, "func")


@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.build()
assert "build succeeded" in status.getvalue()
assert not warning.getvalue()
2 changes: 1 addition & 1 deletion whitelist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ formatter
func
getmodule
getsource
globals
idx
inited
inv
Expand All @@ -29,7 +30,6 @@ param
parametrized
params
pathlib
pep563
pos
prepend
py310
Expand Down