Repository: tox-dev/sphinx-autodoc-typehints
Pull request #459 (2024-06-16 21:41:49)

Support autodoc_type_aliases configuration
This PR addresses Issues #284.

Currently, `autodoc-typehints` does not honor the [`autodoc_type_aliases` configuration of `sphinx.ext.autodoc`](https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_type_aliases). For example,

```python
from numpy.typing import ArrayLike

def func(x: ArrayLike):...
```

renders

```rst
**func(x: _SupportsArray[dtype[Any]] |
_NestedSequence[_SupportsArray[dtype[Any]]] | bool | int | float |
complex | str | bytes | _NestedSequence[bool | int | float | complex |
str | bytes])
```

even if `autodoc_type_aliases = {'ArrayLike': 'ArrayLike'}` line in `conf.py`.

I modified `autodoc_typehints` to pass `app.config.autodoc_type_aliases` to `sphinx_signature()` calls to match `sphinx.ext.autodoc` and print the alias instead of the annotation as discovered.

`test_integration_autodoc_type_aliases.py` is a simple pytest script to check the conservation of `ArrayLike` annotation in a function argument.

tikuma-lsuhsc: @gaborbernat - Could you help me sort out the tox env type check errors?

I'm getting

```
tests/test_integration_autodoc_type_aliases.py:22: error: "T" has no attribute "EXPECTED"  [attr-defined]
tests/test_integration_autodoc_type_aliases.py:23: error: "T" has no attribute "OPTIONS"  [attr-defined]
tests/test_integration_autodoc_type_aliases.py:31: error: "T" has no attribute "WARNING"  [attr-defined]
tests/test_integration_autodoc_type_aliases.py:56: error: Missing return statement  [empty-body]
```

I got the last one, but not clear about the others. I created the test file by copying then stripping away existing tests of the `test_integration.py`. Thanks
gaborbernat: Nothing jumps out for me from the top of my head. 🤔
tikuma-lsuhsc: OK. I'll look into it when I get home tonight.
tikuma-lsuhsc: Resolved the issue (I think).  The test file `test_integration.py` was excluded from the `mypy` type check via `pyproject.toml`. If you try to merge again, it should work now. Thanks.

Patch:
diff --git a/pyproject.toml b/pyproject.toml
index 0665011..945866c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -145,7 +145,7 @@ run.plugins = [
 [tool.mypy]
 python_version = "3.10"
 strict = true
-exclude = "^(.*/roots/.*)|(tests/test_integration.py)$"
+exclude = "^(.*/roots/.*)|(tests/test_integration.*.py)$"
 overrides = [
   { module = [
     "sphobjinv.*",
diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py
index 7cf8edd..0c02ad9 100644
--- a/src/sphinx_autodoc_typehints/__init__.py
+++ b/src/sphinx_autodoc_typehints/__init__.py
@@ -345,7 +345,7 @@ def process_signature(  # noqa: C901, PLR0913, PLR0917
         return None
 
     obj = inspect.unwrap(obj)
-    sph_signature = sphinx_signature(obj)
+    sph_signature = sphinx_signature(obj, type_aliases=app.config["autodoc_type_aliases"])
 
     if app.config.typehints_use_signature:
         parameters = list(sph_signature.parameters.values())
@@ -642,7 +642,7 @@ def process_docstring(  # noqa: PLR0913, PLR0917
     obj = inspect.unwrap(obj)
 
     try:
-        signature = sphinx_signature(obj)
+        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)
@@ -715,8 +715,10 @@ def _inject_signature(  # noqa: C901
     app: Sphinx,
     lines: list[str],
 ) -> None:
-    for arg_name in signature.parameters:
-        annotation = type_hints.get(arg_name)
+    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)
 
         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
new file mode 100644
index 0000000..8e88507
--- /dev/null
+++ b/tests/test_integration_autodoc_type_aliases.py
@@ -0,0 +1,110 @@
+from __future__ import annotations
+
+import re
+import sys
+from pathlib import Path
+from textwrap import dedent, indent
+from typing import TYPE_CHECKING, Any, Callable, Literal, NewType, TypeVar  # no type comments
+
+import pytest
+
+if TYPE_CHECKING:
+    from io import StringIO
+
+    from sphinx.testing.util import SphinxTestApp
+
+T = TypeVar("T")
+W = NewType("W", str)
+
+
+def expected(expected: str, **options: dict[str, Any]) -> Callable[[T], T]:
+    def dec(val: T) -> T:
+        val.EXPECTED = expected
+        val.OPTIONS = options
+        return val
+
+    return dec
+
+
+def warns(pattern: str) -> Callable[[T], T]:
+    def dec(val: T) -> T:
+        val.WARNING = pattern
+        return val
+
+    return dec
+
+
+ArrayLike = Literal["test"]
+
+
+@expected(
+    """\
+mod.function(x)
+
+   Function docstring.
+
+   Parameters:
+      **x** (ArrayLike) -- foo
+
+   Returns:
+      something
+
+   Return type:
+      bytes
+""",
+)
+def function(x: ArrayLike) -> str:  # noqa: ARG001
+    """
+    Function docstring.
+
+    :param x: foo
+    :return: something
+    :rtype: bytes
+    """
+
+
+# Config settings for each test run.
+# Config Name: Sphinx Options as Dict.
+configs = {
+    "default_conf": {
+        "autodoc_type_aliases": {
+            "ArrayLike": "ArrayLike",
+        }
+    }
+}
+
+
+@pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")])
+@pytest.mark.parametrize("conf_run", list(configs.keys()))
+@pytest.mark.sphinx("text", testroot="integration")
+def test_integration(
+    app: SphinxTestApp, status: StringIO, warning: StringIO, monkeypatch: pytest.MonkeyPatch, val: Any, conf_run: str
+) -> None:
+    template = ".. autofunction:: mod.{}"
+
+    (Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__))
+    app.config.__dict__.update(configs[conf_run])
+    app.config.__dict__.update(val.OPTIONS)
+    monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__])
+    app.build()
+    assert "build succeeded" in status.getvalue()  # Build succeeded
+
+    regexp = getattr(val, "WARNING", None)
+    value = warning.getvalue().strip()
+    if regexp:
+        msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
+        assert re.search(regexp, value), msg
+    else:
+        assert not value
+
+    result = (Path(app.srcdir) / "_build/text/index.txt").read_text()
+
+    expected = val.EXPECTED
+    if sys.version_info < (3, 10):
+        expected = expected.replace("NewType", "NewType()")
+    try:
+        assert result.strip() == dedent(expected).strip()
+    except Exception:
+        indented = indent(f'"""\n{result}\n"""', " " * 4)
+        print(f"@expected(\n{indented}\n)\n")  # noqa: T201
+        raise