Skip to content

Commit f75d19b

Browse files
authored
Add handling of tuples in type subscriptions (#212)
1 parent 7b8c357 commit f75d19b

File tree

5 files changed

+84
-2
lines changed

5 files changed

+84
-2
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 1.16.0
4+
5+
- Add support for type subscriptions with multiple elements, where one or more elements
6+
are tuples; e.g., `nptyping.NDArray[(Any, ...), nptyping.Float]`
7+
- Fix bug for arbitrary types accepting singleton subscriptions; e.g., `nptyping.Float[64]`
8+
39
## 1.15.3
410

511
- Prevents reaching inner blocks that contains `if TYPE_CHECKING`

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ testing =
4343
covdefaults>=2
4444
coverage>=6
4545
diff-cover>=6.4
46+
nptyping>=1
4647
pytest>=6
4748
pytest-cov>=3
4849
sphobjinv>=2

src/sphinx_autodoc_typehints/__init__.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@ def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[
8989
return getattr(annotation, "__args__", ())
9090

9191

92+
def format_internal_tuple(t: tuple[Any, ...], config: Config) -> str:
93+
# An annotation can be a tuple, e.g., for nptyping:
94+
# NDArray[(typing.Any, ...), Float]
95+
# In this case, format_annotation receives:
96+
# (typing.Any, Ellipsis)
97+
# This solution should hopefully be general for *any* type that allows tuples in annotations
98+
fmt = [format_annotation(a, config) for a in t]
99+
if len(fmt) == 0:
100+
return "()"
101+
elif len(fmt) == 1:
102+
return f"({fmt[0]}, )"
103+
else:
104+
return f"({', '.join(fmt)})"
105+
106+
92107
def format_annotation(annotation: Any, config: Config) -> str:
93108
typehints_formatter: Callable[..., str] | None = getattr(config, "typehints_formatter", None)
94109
if typehints_formatter is not None:
@@ -102,6 +117,9 @@ def format_annotation(annotation: Any, config: Config) -> str:
102117
elif annotation is Ellipsis:
103118
return "..."
104119

120+
if isinstance(annotation, tuple):
121+
return format_internal_tuple(annotation, config)
122+
105123
# Type variables are also handled specially
106124
try:
107125
if isinstance(annotation, TypeVar) and annotation is not AnyStr:
@@ -150,7 +168,12 @@ def format_annotation(annotation: Any, config: Config) -> str:
150168
formatted_args = f"\\[{', '.join(repr(arg) for arg in args)}]"
151169

152170
if args and not formatted_args:
153-
fmt = [format_annotation(arg, config) for arg in args]
171+
try:
172+
iter(args)
173+
except TypeError:
174+
fmt = [format_annotation(args, config)]
175+
else:
176+
fmt = [format_annotation(arg, config) for arg in args]
154177
formatted_args = args_format.format(", ".join(fmt))
155178

156179
return f":py:{role}:`{prefix}{full_name}`{formatted_args}"

tests/test_sphinx_autodoc_typehints.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
)
2828
from unittest.mock import create_autospec, patch
2929

30+
import nptyping # type: ignore
3031
import pytest
3132
import typing_extensions
3233
from sphinx.application import Sphinx
@@ -201,6 +202,55 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t
201202
(E, ":py:class:`~%s.E`" % __name__),
202203
(E[int], ":py:class:`~%s.E`\\[:py:class:`int`]" % __name__),
203204
(W, f':py:{"class" if PY310_PLUS else "func"}:' f"`~typing.NewType`\\(``W``, :py:class:`str`)"),
205+
# ## These test for correct internal tuple rendering, even if not all are valid Tuple types
206+
# Zero-length tuple remains
207+
(Tuple[()], ":py:data:`~typing.Tuple`\\[()]"),
208+
# Internal single tuple with simple types is flattened in the output
209+
(Tuple[(int,)], ":py:data:`~typing.Tuple`\\[:py:class:`int`]"),
210+
(Tuple[(int, int)], ":py:data:`~typing.Tuple`\\[:py:class:`int`, :py:class:`int`]"),
211+
# Ellipsis in single tuple also gets flattened
212+
(Tuple[(int, ...)], ":py:data:`~typing.Tuple`\\[:py:class:`int`, ...]"),
213+
# Internal tuple with following additional type cannot be flattened (specific to nptyping?)
214+
# These cases will fail if nptyping restructures its internal module hierarchy
215+
(
216+
nptyping.NDArray[(Any,), nptyping.Float],
217+
(
218+
":py:class:`~nptyping.types._ndarray.NDArray`\\[(:py:data:`~typing.Any`, ), "
219+
":py:class:`~nptyping.types._number.Float`]"
220+
),
221+
),
222+
(
223+
nptyping.NDArray[(Any,), nptyping.Float[64]],
224+
(
225+
":py:class:`~nptyping.types._ndarray.NDArray`\\[(:py:data:`~typing.Any`, ), "
226+
":py:class:`~nptyping.types._number.Float`\\[64]]"
227+
),
228+
),
229+
(
230+
nptyping.NDArray[(Any, Any), nptyping.Float],
231+
(
232+
":py:class:`~nptyping.types._ndarray.NDArray`\\[(:py:data:`~typing.Any`, "
233+
":py:data:`~typing.Any`), :py:class:`~nptyping.types._number.Float`]"
234+
),
235+
),
236+
(
237+
nptyping.NDArray[(Any, ...), nptyping.Float],
238+
(
239+
":py:class:`~nptyping.types._ndarray.NDArray`\\[(:py:data:`~typing.Any`, ...), "
240+
":py:class:`~nptyping.types._number.Float`]"
241+
),
242+
),
243+
(
244+
nptyping.NDArray[(Any, 3), nptyping.Float],
245+
(
246+
":py:class:`~nptyping.types._ndarray.NDArray`\\[(:py:data:`~typing.Any`, 3), "
247+
":py:class:`~nptyping.types._number.Float`]"
248+
),
249+
),
250+
(
251+
nptyping.NDArray[(3, ...), nptyping.Float],
252+
(":py:class:`~nptyping.types._ndarray.NDArray`\\[(3, ...), :py:class:`~nptyping.types._number.Float`]"),
253+
),
204254
],
205255
)
206256
def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str) -> None:
@@ -226,8 +276,9 @@ def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str
226276
assert format_annotation(annotation, conf) == expected_result_not_simplified
227277

228278
# Test with the "fully_qualified" flag turned on
229-
if "typing" in expected_result or __name__ in expected_result:
279+
if "typing" in expected_result or "nptyping" in expected_result or __name__ in expected_result:
230280
expected_result = expected_result.replace("~typing", "typing")
281+
expected_result = expected_result.replace("~nptyping", "nptyping")
231282
expected_result = expected_result.replace("~" + __name__, __name__)
232283
conf = create_autospec(Config, typehints_fully_qualified=True)
233284
assert format_annotation(annotation, conf) == expected_result

whitelist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ kwonlyargs
2828
libs
2929
metaclass
3030
multiline
31+
nptyping
3132
param
3233
parametrized
3334
params

0 commit comments

Comments
 (0)