Repository: tox-dev/sphinx-autodoc-typehints
Pull request #463 (2024-06-21 01:40:14)

Fix issue #462 and another alias processing bug
This PR fixes the bugs introduced in v2.2:

* Error during checking for annotation alias (#462)
* Incorrect alias name look up (looking up alias name instead of the true type name)
* Aliased type ignored for the return type

The new alias look up mechanism follows closely as done in `sphinx.util.inspect.signature()`

hoodmane: Thanks @tikuma-lsuhsc!

Patch:
diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py
index 0c02ad9..837598c 100644
--- a/src/sphinx_autodoc_typehints/__init__.py
+++ b/src/sphinx_autodoc_typehints/__init__.py
@@ -17,8 +17,8 @@ from docutils.frontend import OptionParser
 from sphinx.ext.autodoc.mock import mock
 from sphinx.parsers import RSTParser
 from sphinx.util import logging, rst
+from sphinx.util.inspect import TypeAliasForwardRef, TypeAliasNamespace, stringify_signature
 from sphinx.util.inspect import signature as sphinx_signature
-from sphinx.util.inspect import stringify_signature
 
 from .parser import parse
 from .patches import install_patches
@@ -194,6 +194,9 @@ def format_annotation(annotation: Any, config: Config) -> str:  # noqa: C901, PL
     if isinstance(annotation, tuple):
         return format_internal_tuple(annotation, config)
 
+    if isinstance(annotation, TypeAliasForwardRef):
+        return str(annotation)
+
     try:
         module = get_annotation_module(annotation)
         class_name = get_annotation_class_name(annotation, module)
@@ -404,8 +407,10 @@ def _future_annotations_imported(obj: Any) -> bool:
     return bool(_annotations.compiler_flag == future_annotations)
 
 
-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)
+def get_all_type_hints(
+    autodoc_mock_imports: list[str], obj: Any, name: str, localns: TypeAliasNamespace
+) -> dict[str, Any]:
+    result = _get_type_hint(autodoc_mock_imports, name, obj, localns)
     if not result:
         result = backfill_type_hints(obj, name)
         try:
@@ -413,7 +418,7 @@ def get_all_type_hints(autodoc_mock_imports: list[str], obj: Any, name: str) ->
         except (AttributeError, TypeError):
             pass
         else:
-            result = _get_type_hint(autodoc_mock_imports, name, obj)
+            result = _get_type_hint(autodoc_mock_imports, name, obj, localns)
     return result
 
 
@@ -474,10 +479,10 @@ def _resolve_type_guarded_imports(autodoc_mock_imports: list[str], obj: Any) ->
             _execute_guarded_code(autodoc_mock_imports, obj, module_code)
 
 
-def _get_type_hint(autodoc_mock_imports: list[str], name: str, obj: Any) -> dict[str, Any]:
+def _get_type_hint(autodoc_mock_imports: list[str], name: str, obj: Any, localns: TypeAliasNamespace) -> dict[str, Any]:
     _resolve_type_guarded_imports(autodoc_mock_imports, obj)
     try:
-        result = get_type_hints(obj)
+        result = get_type_hints(obj, None, localns)
     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
@@ -645,7 +650,9 @@ def process_docstring(  # noqa: PLR0913, PLR0917
         signature = sphinx_signature(obj, type_aliases=app.config["autodoc_type_aliases"])
     except (ValueError, TypeError):
         signature = None
-    type_hints = get_all_type_hints(app.config.autodoc_mock_imports, obj, name)
+
+    localns = TypeAliasNamespace(app.config["autodoc_type_aliases"])
+    type_hints = get_all_type_hints(app.config.autodoc_mock_imports, obj, name, localns)
     app.config._annotation_globals = getattr(obj, "__globals__", {})  # type: ignore[attr-defined]  # noqa: SLF001
     try:
         _inject_types_to_docstring(type_hints, signature, original_obj, app, what, name, lines)
@@ -715,10 +722,8 @@ def _inject_signature(  # noqa: C901
     app: Sphinx,
     lines: list[str],
 ) -> None:
-    type_aliases = app.config["autodoc_type_aliases"]
-
-    for arg_name, arg_type in signature.parameters.items():
-        annotation = arg_type.annotation if arg_type.annotation in type_aliases else type_hints.get(arg_name)
+    for arg_name in signature.parameters:
+        annotation = type_hints.get(arg_name)
 
         default = signature.parameters[arg_name].default
 


Test patch:
diff --git a/tests/test_integration_autodoc_type_aliases.py b/tests/test_integration_autodoc_type_aliases.py
index 8e88507..7d21971 100644
--- a/tests/test_integration_autodoc_type_aliases.py
+++ b/tests/test_integration_autodoc_type_aliases.py
@@ -37,27 +37,89 @@ def warns(pattern: str) -> Callable[[T], T]:
 ArrayLike = Literal["test"]
 
 
+class _SchemaMeta(type):  # noqa: PLW1641
+    def __eq__(cls, other: object) -> bool:
+        return True
+
+
+class Schema(metaclass=_SchemaMeta):
+    pass
+
+
+@expected(
+    """
+mod.f(s)
+
+   Do something.
+
+   Parameters:
+      **s** ("Schema") -- Some schema.
+
+   Return type:
+      "Schema"
+"""
+)
+def f(s: Schema) -> Schema:
+    """
+    Do something.
+
+    Args:
+        s: Some schema.
+    """
+    return s
+
+
+class AliasedClass: ...
+
+
+@expected(
+    """
+mod.g(s)
+
+   Do something.
+
+   Parameters:
+      **s** ("Class Alias") -- Some schema.
+
+   Return type:
+      "Class Alias"
+"""
+)
+def g(s: AliasedClass) -> AliasedClass:
+    """
+    Do something.
+
+    Args:
+        s: Some schema.
+    """
+    return s
+
+
 @expected(
     """\
-mod.function(x)
+mod.function(x, y)
 
    Function docstring.
 
    Parameters:
-      **x** (ArrayLike) -- foo
+      * **x** (Array) -- foo
+
+      * **y** ("Schema") -- boo
 
    Returns:
       something
 
    Return type:
       bytes
+
 """,
 )
-def function(x: ArrayLike) -> str:  # noqa: ARG001
+def function(x: ArrayLike, y: Schema) -> str:  # noqa: ARG001
     """
     Function docstring.
 
     :param x: foo
+    :param y: boo
     :return: something
     :rtype: bytes
     """
@@ -65,13 +127,7 @@ def function(x: ArrayLike) -> str:  # noqa: ARG001
 
 # Config settings for each test run.
 # Config Name: Sphinx Options as Dict.
-configs = {
-    "default_conf": {
-        "autodoc_type_aliases": {
-            "ArrayLike": "ArrayLike",
-        }
-    }
-}
+configs = {"default_conf": {"autodoc_type_aliases": {"ArrayLike": "Array", "AliasedClass": '"Class Alias"'}}}
 
 
 @pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")])
@@ -94,7 +150,7 @@ def test_integration(
     if regexp:
         msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
         assert re.search(regexp, value), msg
-    else:
+    elif not re.search("WARNING: Inline strong start-string without end-string.", value):
         assert not value
 
     result = (Path(app.srcdir) / "_build/text/index.txt").read_text()