Repository: tox-dev/sphinx-autodoc-typehints (python)

Issue:
TypeVar broke with 1.25
 @hoodmane for some reason #397  broke tox documentation on TypeVar

<img width="416" alt="image" src="https://github.com/tox-dev/sphinx-autodoc-typehints/assets/690238/1e77d6a8-1825-4162-bc73-be835adad963">

https://github.com/tox-dev/tox/actions/runs/6822355429/job/18554265718

Any idea why is that?

_Originally posted by @gaborbernat in https://github.com/tox-dev/sphinx-autodoc-typehints/issues/397#issuecomment-1806076131_
            

Pull request description:
FIX Put a slash space between xref and type args
Fixes #398

Patch:
diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py
index 67a7261..aa81b1f 100644
--- a/src/sphinx_autodoc_typehints/__init__.py
+++ b/src/sphinx_autodoc_typehints/__init__.py
@@ -211,7 +211,7 @@ def format_annotation(annotation: Any, config: Config) -> str:  # noqa: C901, PL
     fully_qualified: bool = getattr(config, "typehints_fully_qualified", False)
     prefix = "" if fully_qualified or full_name == class_name else "~"
     role = "data" if module == "typing" and class_name in _PYDATA_ANNOTATIONS else "class"
-    args_format = "\\ \\[{}]"
+    args_format = "\\[{}]"
     formatted_args: str | None = ""
 
     # Some types require special handling
@@ -244,9 +244,9 @@ def format_annotation(annotation: Any, config: Config) -> str:  # noqa: C901, PL
                 args = tuple(x for x in args if x is not type(None))
     elif full_name in ("typing.Callable", "collections.abc.Callable") and args and args[0] is not ...:
         fmt = [format_annotation(arg, config) for arg in args]
-        formatted_args = f"\\ \\[\\[{', '.join(fmt[:-1])}], {fmt[-1]}]"
+        formatted_args = f"\\[\\[{', '.join(fmt[:-1])}], {fmt[-1]}]"
     elif full_name == "typing.Literal":
-        formatted_args = f"\\ \\[{', '.join(f'``{arg!r}``' for arg in args)}]"
+        formatted_args = f"\\[{', '.join(f'``{arg!r}``' for arg in args)}]"
     elif full_name == "types.UnionType":
         return " | ".join([format_annotation(arg, config) for arg in args])
 
@@ -258,6 +258,8 @@ def format_annotation(annotation: Any, config: Config) -> str:  # noqa: C901, PL
         else:
             fmt = [format_annotation(arg, config) for arg in args]
         formatted_args = args_format.format(", ".join(fmt))
+    if formatted_args:
+        formatted_args = "\\ " + formatted_args
 
     return f":py:{role}:`{prefix}{full_name}`{formatted_args}"
 


Test patch:
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 13f21a1..d1038c8 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -6,7 +6,7 @@ from dataclasses import dataclass
 from inspect import isclass
 from pathlib import Path
 from textwrap import dedent, indent
-from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union, overload  # no type comments
+from typing import TYPE_CHECKING, Any, Callable, NewType, Optional, TypeVar, Union, overload  # no type comments
 
 import pytest
 
@@ -18,6 +18,7 @@ if TYPE_CHECKING:
     from sphinx.testing.util import SphinxTestApp
 
 T = TypeVar("T")
+W = NewType("W", str)
 
 
 def expected(expected: str) -> Callable[[T], T]:
@@ -1174,6 +1175,52 @@ def docstring_with_definition_list_after_params_no_blank_line(param: int) -> Non
     """
 
 
+@expected(
+    """
+    mod.has_typevar(param)
+
+       Do something.
+
+       Parameters:
+          **param** ("TypeVar"("T")) -- A parameter.
+
+       Return type:
+          "TypeVar"("T")
+
+    """,
+)
+def has_typevar(param: T) -> T:
+    """Do something.
+
+    Args:
+        param: A parameter.
+    """
+    return param
+
+
+@expected(
+    """
+    mod.has_newtype(param)
+
+       Do something.
+
+       Parameters:
+          **param** ("NewType"("W", "str")) -- A parameter.
+
+       Return type:
+          "NewType"("W", "str")
+
+    """,
+)
+def has_newtype(param: W) -> W:
+    """Do something.
+
+    Args:
+        param: A parameter.
+    """
+    return param
+
+
 AUTO_FUNCTION = ".. autofunction:: mod.{}"
 AUTO_CLASS = """\
 .. autoclass:: mod.{}
@@ -1184,6 +1231,8 @@ AUTO_EXCEPTION = """\
    :members:
 """
 
+LT_PY310 = sys.version_info < (3, 10)
+
 
 @pytest.mark.parametrize("val", [x for x in globals().values() if hasattr(x, "EXPECTED")])
 @pytest.mark.sphinx("text", testroot="integration")
@@ -1217,6 +1266,8 @@ def test_integration(
     result = (Path(app.srcdir) / "_build/text/index.txt").read_text()
 
     expected = val.EXPECTED
+    if LT_PY310:
+        expected = expected.replace("NewType", "NewType()")
     try:
         assert result.strip() == dedent(expected).strip()
     except Exception:
diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py
index 31b71a7..10b22d3 100644
--- a/tests/test_sphinx_autodoc_typehints.py
+++ b/tests/test_sphinx_autodoc_typehints.py
@@ -201,122 +201,122 @@ _CASES = [
     (type, ":py:class:`type`"),
     (collections.abc.Callable, ":py:class:`~collections.abc.Callable`"),
     (Type, ":py:class:`~typing.Type`"),
-    (Type[A], ":py:class:`~typing.Type`\\ \\[:py:class:`~%s.A`]" % __name__),
+    (Type[A], r":py:class:`~typing.Type`\ \[:py:class:`~%s.A`]" % __name__),
     (Any, ":py:data:`~typing.Any`"),
     (AnyStr, ":py:data:`~typing.AnyStr`"),
-    (Generic[T], ":py:class:`~typing.Generic`\\ \\[:py:class:`~typing.TypeVar`\\(``T``)]"),
+    (Generic[T], r":py:class:`~typing.Generic`\ \[:py:class:`~typing.TypeVar`\ \(``T``)]"),
     (Mapping, ":py:class:`~typing.Mapping`"),
     (
         Mapping[T, int],  # type: ignore[valid-type]
-        ":py:class:`~typing.Mapping`\\ \\[:py:class:`~typing.TypeVar`\\(``T``), :py:class:`int`]",
+        r":py:class:`~typing.Mapping`\ \[:py:class:`~typing.TypeVar`\ \(``T``), :py:class:`int`]",
     ),
     (
         Mapping[str, V_contra],  # type: ignore[valid-type]
-        ":py:class:`~typing.Mapping`\\ \\[:py:class:`str`, :py:class:`~typing.TypeVar`\\("
+        r":py:class:`~typing.Mapping`\ \[:py:class:`str`, :py:class:`~typing.TypeVar`\ \("
         "``V_contra``, contravariant=True)]",
     ),
     (
         Mapping[T, U_co],  # type: ignore[valid-type]
-        ":py:class:`~typing.Mapping`\\ \\[:py:class:`~typing.TypeVar`\\(``T``), "
-        ":py:class:`~typing.TypeVar`\\(``U_co``, covariant=True)]",
+        r":py:class:`~typing.Mapping`\ \[:py:class:`~typing.TypeVar`\ \(``T``), "
+        r":py:class:`~typing.TypeVar`\ \(``U_co``, covariant=True)]",
     ),
-    (Mapping[str, bool], ":py:class:`~typing.Mapping`\\ \\[:py:class:`str`, :py:class:`bool`]"),
+    (Mapping[str, bool], r":py:class:`~typing.Mapping`\ \[:py:class:`str`, :py:class:`bool`]"),
     (Dict, ":py:class:`~typing.Dict`"),
     (
         Dict[T, int],  # type: ignore[valid-type]
-        ":py:class:`~typing.Dict`\\ \\[:py:class:`~typing.TypeVar`\\(``T``), :py:class:`int`]",
+        r":py:class:`~typing.Dict`\ \[:py:class:`~typing.TypeVar`\ \(``T``), :py:class:`int`]",
     ),
     (
         Dict[str, V_contra],  # type: ignore[valid-type]
-        ":py:class:`~typing.Dict`\\ \\[:py:class:`str`, :py:class:`~typing.TypeVar`\\(``V_contra``, contravariant=True)]",  # noqa: E501
+        r":py:class:`~typing.Dict`\ \[:py:class:`str`, :py:class:`~typing.TypeVar`\ \(``V_contra``, contravariant=True)]",  # noqa: E501
     ),
     (
         Dict[T, U_co],  # type: ignore[valid-type]
-        ":py:class:`~typing.Dict`\\ \\[:py:class:`~typing.TypeVar`\\(``T``),"
-        " :py:class:`~typing.TypeVar`\\(``U_co``, covariant=True)]",
+        r":py:class:`~typing.Dict`\ \[:py:class:`~typing.TypeVar`\ \(``T``),"
+        r" :py:class:`~typing.TypeVar`\ \(``U_co``, covariant=True)]",
     ),
-    (Dict[str, bool], ":py:class:`~typing.Dict`\\ \\[:py:class:`str`, :py:class:`bool`]"),
+    (Dict[str, bool], r":py:class:`~typing.Dict`\ \[:py:class:`str`, :py:class:`bool`]"),
     (Tuple, ":py:data:`~typing.Tuple`"),
-    (Tuple[str, bool], ":py:data:`~typing.Tuple`\\ \\[:py:class:`str`, :py:class:`bool`]"),
-    (Tuple[int, int, int], ":py:data:`~typing.Tuple`\\ \\[:py:class:`int`, :py:class:`int`, :py:class:`int`]"),
-    (Tuple[str, ...], ":py:data:`~typing.Tuple`\\ \\[:py:class:`str`, :py:data:`...<Ellipsis>`]"),
+    (Tuple[str, bool], r":py:data:`~typing.Tuple`\ \[:py:class:`str`, :py:class:`bool`]"),
+    (Tuple[int, int, int], r":py:data:`~typing.Tuple`\ \[:py:class:`int`, :py:class:`int`, :py:class:`int`]"),
+    (Tuple[str, ...], r":py:data:`~typing.Tuple`\ \[:py:class:`str`, :py:data:`...<Ellipsis>`]"),
     (Union, ":py:data:`~typing.Union`"),
-    (Union[str, bool], ":py:data:`~typing.Union`\\ \\[:py:class:`str`, :py:class:`bool`]"),
-    (Union[str, bool, None], ":py:data:`~typing.Union`\\ \\[:py:class:`str`, :py:class:`bool`, :py:obj:`None`]"),
-    pytest.param(Union[str, Any], ":py:data:`~typing.Union`\\ \\[:py:class:`str`, :py:data:`~typing.Any`]"),
-    (Optional[str], ":py:data:`~typing.Optional`\\ \\[:py:class:`str`]"),
-    (Union[str, None], ":py:data:`~typing.Optional`\\ \\[:py:class:`str`]"),
+    (Union[str, bool], r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`]"),
+    (Union[str, bool, None], r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`, :py:obj:`None`]"),
+    pytest.param(Union[str, Any], r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:data:`~typing.Any`]"),
+    (Optional[str], r":py:data:`~typing.Optional`\ \[:py:class:`str`]"),
+    (Union[str, None], r":py:data:`~typing.Optional`\ \[:py:class:`str`]"),
     (
         Optional[Union[str, bool]],
-        ":py:data:`~typing.Union`\\ \\[:py:class:`str`, :py:class:`bool`, :py:obj:`None`]",
+        r":py:data:`~typing.Union`\ \[:py:class:`str`, :py:class:`bool`, :py:obj:`None`]",
     ),
     (Callable, ":py:data:`~typing.Callable`"),
-    (Callable[..., int], ":py:data:`~typing.Callable`\\ \\[:py:data:`...<Ellipsis>`, :py:class:`int`]"),
-    (Callable[[int], int], ":py:data:`~typing.Callable`\\ \\[\\[:py:class:`int`], :py:class:`int`]"),
+    (Callable[..., int], r":py:data:`~typing.Callable`\ \[:py:data:`...<Ellipsis>`, :py:class:`int`]"),
+    (Callable[[int], int], r":py:data:`~typing.Callable`\ \[\[:py:class:`int`], :py:class:`int`]"),
     (
         Callable[[int, str], bool],
-        ":py:data:`~typing.Callable`\\ \\[\\[:py:class:`int`, :py:class:`str`], :py:class:`bool`]",
+        r":py:data:`~typing.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:class:`bool`]",
     ),
     (
         Callable[[int, str], None],
-        ":py:data:`~typing.Callable`\\ \\[\\[:py:class:`int`, :py:class:`str`], :py:obj:`None`]",
+        r":py:data:`~typing.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:obj:`None`]",
     ),
     (
         Callable[[T], T],
-        ":py:data:`~typing.Callable`\\ \\[\\[:py:class:`~typing.TypeVar`\\(``T``)],"
-        " :py:class:`~typing.TypeVar`\\(``T``)]",
+        r":py:data:`~typing.Callable`\ \[\[:py:class:`~typing.TypeVar`\ \(``T``)],"
+        r" :py:class:`~typing.TypeVar`\ \(``T``)]",
     ),
     (
         AbcCallable[[int, str], bool],  # type: ignore[valid-type,misc,type-arg]
-        ":py:class:`~collections.abc.Callable`\\ \\[\\[:py:class:`int`, :py:class:`str`], :py:class:`bool`]",
+        r":py:class:`~collections.abc.Callable`\ \[\[:py:class:`int`, :py:class:`str`], :py:class:`bool`]",
     ),
     (Pattern, ":py:class:`~typing.Pattern`"),
-    (Pattern[str], ":py:class:`~typing.Pattern`\\ \\[:py:class:`str`]"),
+    (Pattern[str], r":py:class:`~typing.Pattern`\ \[:py:class:`str`]"),
     (IO, ":py:class:`~typing.IO`"),
-    (IO[str], ":py:class:`~typing.IO`\\ \\[:py:class:`str`]"),
+    (IO[str], r":py:class:`~typing.IO`\ \[:py:class:`str`]"),
     (Metaclass, ":py:class:`~%s.Metaclass`" % __name__),
     (A, ":py:class:`~%s.A`" % __name__),
     (B, ":py:class:`~%s.B`" % __name__),
-    (B[int], ":py:class:`~%s.B`\\ \\[:py:class:`int`]" % __name__),
+    (B[int], r":py:class:`~%s.B`\ \[:py:class:`int`]" % __name__),
     (C, ":py:class:`~%s.C`" % __name__),
     (D, ":py:class:`~%s.D`" % __name__),
     (E, ":py:class:`~%s.E`" % __name__),
-    (E[int], ":py:class:`~%s.E`\\ \\[:py:class:`int`]" % __name__),
-    (W, f":py:{'class' if PY310_PLUS else 'func'}:`~typing.NewType`\\(``W``, :py:class:`str`)"),
-    (T, ":py:class:`~typing.TypeVar`\\(``T``)"),
-    (U_co, ":py:class:`~typing.TypeVar`\\(``U_co``, covariant=True)"),
-    (V_contra, ":py:class:`~typing.TypeVar`\\(``V_contra``, contravariant=True)"),
-    (X, ":py:class:`~typing.TypeVar`\\(``X``, :py:class:`str`, :py:class:`int`)"),
-    (Y, ":py:class:`~typing.TypeVar`\\(``Y``, bound= :py:class:`str`)"),
-    (Z, ":py:class:`~typing.TypeVar`\\(``Z``, bound= A)"),
-    (S, ":py:class:`~typing.TypeVar`\\(``S``, bound= miss)"),
+    (E[int], r":py:class:`~%s.E`\ \[:py:class:`int`]" % __name__),
+    (W, rf":py:{'class' if PY310_PLUS else 'func'}:`~typing.NewType`\ \(``W``, :py:class:`str`)"),
+    (T, r":py:class:`~typing.TypeVar`\ \(``T``)"),
+    (U_co, r":py:class:`~typing.TypeVar`\ \(``U_co``, covariant=True)"),
+    (V_contra, r":py:class:`~typing.TypeVar`\ \(``V_contra``, contravariant=True)"),
+    (X, r":py:class:`~typing.TypeVar`\ \(``X``, :py:class:`str`, :py:class:`int`)"),
+    (Y, r":py:class:`~typing.TypeVar`\ \(``Y``, bound= :py:class:`str`)"),
+    (Z, r":py:class:`~typing.TypeVar`\ \(``Z``, bound= A)"),
+    (S, r":py:class:`~typing.TypeVar`\ \(``S``, bound= miss)"),
     # ParamSpec should behave like TypeVar, except for missing constraints
-    (P, f":py:class:`~typing.ParamSpec`\\(``P``{', bound= :py:obj:`None`' if PY312_PLUS else ''})"),
+    (P, rf":py:class:`~typing.ParamSpec`\ \(``P``{', bound= :py:obj:`None`' if PY312_PLUS else ''})"),
     (
         P_co,
-        f":py:class:`~typing.ParamSpec`\\(``P_co``{', bound= :py:obj:`None`' if PY312_PLUS else ''}, covariant=True)",
+        rf":py:class:`~typing.ParamSpec`\ \(``P_co``{', bound= :py:obj:`None`' if PY312_PLUS else ''}, covariant=True)",
     ),
     (
         P_contra,
-        f":py:class:`~typing.ParamSpec`\\(``P_contra``{', bound= :py:obj:`None`' if PY312_PLUS else ''}"
+        rf":py:class:`~typing.ParamSpec`\ \(``P_contra``{', bound= :py:obj:`None`' if PY312_PLUS else ''}"
         ", contravariant=True)",
     ),
-    (P_bound, ":py:class:`~typing.ParamSpec`\\(``P_bound``, bound= :py:class:`str`)"),
+    (P_bound, r":py:class:`~typing.ParamSpec`\ \(``P_bound``, bound= :py:class:`str`)"),
     # ## These test for correct internal tuple rendering, even if not all are valid Tuple types
     # Zero-length tuple remains
     (Tuple[()], ":py:data:`~typing.Tuple`"),
     # Internal single tuple with simple types is flattened in the output
-    (Tuple[(int,)], ":py:data:`~typing.Tuple`\\ \\[:py:class:`int`]"),
-    (Tuple[(int, int)], ":py:data:`~typing.Tuple`\\ \\[:py:class:`int`, :py:class:`int`]"),
+    (Tuple[(int,)], r":py:data:`~typing.Tuple`\ \[:py:class:`int`]"),
+    (Tuple[(int, int)], r":py:data:`~typing.Tuple`\ \[:py:class:`int`, :py:class:`int`]"),
     # Ellipsis in single tuple also gets flattened
-    (Tuple[(int, ...)], ":py:data:`~typing.Tuple`\\ \\[:py:class:`int`, :py:data:`...<Ellipsis>`]"),
+    (Tuple[(int, ...)], r":py:data:`~typing.Tuple`\ \[:py:class:`int`, :py:data:`...<Ellipsis>`]"),
     (
         RecList,
-        ":py:data:`~typing.Union`\\ \\[:py:class:`int`, :py:class:`~typing.List`\\ \\[RecList]]",
+        r":py:data:`~typing.Union`\ \[:py:class:`int`, :py:class:`~typing.List`\ \[RecList]]",
     ),
     (
         MutualRecA,
-        ":py:data:`~typing.Union`\\ \\[:py:class:`bool`, :py:class:`~typing.List`\\ \\[MutualRecB]]",
+        r":py:data:`~typing.Union`\ \[:py:class:`bool`, :py:class:`~typing.List`\ \[MutualRecB]]",
     ),
 ]
 
@@ -425,7 +425,7 @@ def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str
         ("NoReturn", None, ":py:data:`~typing.NoReturn`"),
         ("Literal", ("a", 1), ":py:data:`~typing.Literal`\\ \\[``'a'``, ``1``]"),
         ("Type", None, ":py:class:`~typing.Type`"),
-        ("Type", (A,), f":py:class:`~typing.Type`\\ \\[:py:class:`~{__name__}.A`]"),
+        ("Type", (A,), rf":py:class:`~typing.Type`\ \[:py:class:`~{__name__}.A`]"),
     ],
 )
 def test_format_annotation_both_libs(library: ModuleType, annotation: str, params: Any, expected_result: str) -> None: