Skip to content

Commit

Permalink
Add handling of tuples in type subscriptions (#212)
Browse files Browse the repository at this point in the history
  • Loading branch information
bskinn authored Jan 22, 2022
1 parent 7b8c357 commit f75d19b
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 2 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.16.0

- Add support for type subscriptions with multiple elements, where one or more elements
are tuples; e.g., `nptyping.NDArray[(Any, ...), nptyping.Float]`
- Fix bug for arbitrary types accepting singleton subscriptions; e.g., `nptyping.Float[64]`

## 1.15.3

- Prevents reaching inner blocks that contains `if TYPE_CHECKING`
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ testing =
covdefaults>=2
coverage>=6
diff-cover>=6.4
nptyping>=1
pytest>=6
pytest-cov>=3
sphobjinv>=2
Expand Down
25 changes: 24 additions & 1 deletion src/sphinx_autodoc_typehints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[
return getattr(annotation, "__args__", ())


def format_internal_tuple(t: tuple[Any, ...], config: Config) -> str:
# An annotation can be a tuple, e.g., for nptyping:
# NDArray[(typing.Any, ...), Float]
# In this case, format_annotation receives:
# (typing.Any, Ellipsis)
# This solution should hopefully be general for *any* type that allows tuples in annotations
fmt = [format_annotation(a, config) for a in t]
if len(fmt) == 0:
return "()"
elif len(fmt) == 1:
return f"({fmt[0]}, )"
else:
return f"({', '.join(fmt)})"


def format_annotation(annotation: Any, config: Config) -> str:
typehints_formatter: Callable[..., str] | None = getattr(config, "typehints_formatter", None)
if typehints_formatter is not None:
Expand All @@ -102,6 +117,9 @@ def format_annotation(annotation: Any, config: Config) -> str:
elif annotation is Ellipsis:
return "..."

if isinstance(annotation, tuple):
return format_internal_tuple(annotation, config)

# Type variables are also handled specially
try:
if isinstance(annotation, TypeVar) and annotation is not AnyStr:
Expand Down Expand Up @@ -150,7 +168,12 @@ def format_annotation(annotation: Any, config: Config) -> str:
formatted_args = f"\\[{', '.join(repr(arg) for arg in args)}]"

if args and not formatted_args:
fmt = [format_annotation(arg, config) for arg in args]
try:
iter(args)
except TypeError:
fmt = [format_annotation(args, config)]
else:
fmt = [format_annotation(arg, config) for arg in args]
formatted_args = args_format.format(", ".join(fmt))

return f":py:{role}:`{prefix}{full_name}`{formatted_args}"
Expand Down
53 changes: 52 additions & 1 deletion tests/test_sphinx_autodoc_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
)
from unittest.mock import create_autospec, patch

import nptyping # type: ignore
import pytest
import typing_extensions
from sphinx.application import Sphinx
Expand Down Expand Up @@ -201,6 +202,55 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t
(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"}:' f"`~typing.NewType`\\(``W``, :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`]"),
# Ellipsis in single tuple also gets flattened
(Tuple[(int, ...)], ":py:data:`~typing.Tuple`\\[:py:class:`int`, ...]"),
# Internal tuple with following additional type cannot be flattened (specific to nptyping?)
# These cases will fail if nptyping restructures its internal module hierarchy
(
nptyping.NDArray[(Any,), nptyping.Float],
(
":py:class:`~nptyping.types._ndarray.NDArray`\\[(:py:data:`~typing.Any`, ), "
":py:class:`~nptyping.types._number.Float`]"
),
),
(
nptyping.NDArray[(Any,), nptyping.Float[64]],
(
":py:class:`~nptyping.types._ndarray.NDArray`\\[(:py:data:`~typing.Any`, ), "
":py:class:`~nptyping.types._number.Float`\\[64]]"
),
),
(
nptyping.NDArray[(Any, Any), nptyping.Float],
(
":py:class:`~nptyping.types._ndarray.NDArray`\\[(:py:data:`~typing.Any`, "
":py:data:`~typing.Any`), :py:class:`~nptyping.types._number.Float`]"
),
),
(
nptyping.NDArray[(Any, ...), nptyping.Float],
(
":py:class:`~nptyping.types._ndarray.NDArray`\\[(:py:data:`~typing.Any`, ...), "
":py:class:`~nptyping.types._number.Float`]"
),
),
(
nptyping.NDArray[(Any, 3), nptyping.Float],
(
":py:class:`~nptyping.types._ndarray.NDArray`\\[(:py:data:`~typing.Any`, 3), "
":py:class:`~nptyping.types._number.Float`]"
),
),
(
nptyping.NDArray[(3, ...), nptyping.Float],
(":py:class:`~nptyping.types._ndarray.NDArray`\\[(3, ...), :py:class:`~nptyping.types._number.Float`]"),
),
],
)
def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str) -> None:
Expand All @@ -226,8 +276,9 @@ def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str
assert format_annotation(annotation, conf) == expected_result_not_simplified

# Test with the "fully_qualified" flag turned on
if "typing" in expected_result or __name__ in expected_result:
if "typing" in expected_result or "nptyping" in expected_result or __name__ in expected_result:
expected_result = expected_result.replace("~typing", "typing")
expected_result = expected_result.replace("~nptyping", "nptyping")
expected_result = expected_result.replace("~" + __name__, __name__)
conf = create_autospec(Config, typehints_fully_qualified=True)
assert format_annotation(annotation, conf) == expected_result
Expand Down
1 change: 1 addition & 0 deletions whitelist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ kwonlyargs
libs
metaclass
multiline
nptyping
param
parametrized
params
Expand Down

0 comments on commit f75d19b

Please sign in to comment.