Skip to content

Commit

Permalink
FIX Put a slash space between xref and type args (#399)
Browse files Browse the repository at this point in the history
  • Loading branch information
hoodmane authored Nov 10, 2023
1 parent 9735dc6 commit 92a9bb6
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 54 deletions.
8 changes: 5 additions & 3 deletions src/sphinx_autodoc_typehints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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])

Expand All @@ -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}"

Expand Down
53 changes: 52 additions & 1 deletion tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
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

Expand All @@ -18,6 +18,7 @@
from sphinx.testing.util import SphinxTestApp

T = TypeVar("T")
W = NewType("W", str)


def expected(expected: str) -> Callable[[T], T]:
Expand Down Expand Up @@ -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.{}
Expand All @@ -1184,6 +1231,8 @@ def docstring_with_definition_list_after_params_no_blank_line(param: int) -> Non
: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")
Expand Down Expand Up @@ -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:
Expand Down
100 changes: 50 additions & 50 deletions tests/test_sphinx_autodoc_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,122 +201,122 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t
(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]]",
),
]

Expand Down Expand Up @@ -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:
Expand Down

0 comments on commit 92a9bb6

Please sign in to comment.