From f75d19be9275b25d2f6caa23392a6072f49c3d56 Mon Sep 17 00:00:00 2001 From: Brian Skinn Date: Sat, 22 Jan 2022 14:22:09 -0500 Subject: [PATCH] Add handling of tuples in type subscriptions (#212) --- CHANGELOG.md | 6 +++ setup.cfg | 1 + src/sphinx_autodoc_typehints/__init__.py | 25 ++++++++++- tests/test_sphinx_autodoc_typehints.py | 53 +++++++++++++++++++++++- whitelist.txt | 1 + 5 files changed, 84 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5237d3b5..81747b37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/setup.cfg b/setup.cfg index 7d4e670f..c73cef44 100644 --- a/setup.cfg +++ b/setup.cfg @@ -43,6 +43,7 @@ testing = covdefaults>=2 coverage>=6 diff-cover>=6.4 + nptyping>=1 pytest>=6 pytest-cov>=3 sphobjinv>=2 diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py index 0876ca74..9a7affb6 100644 --- a/src/sphinx_autodoc_typehints/__init__.py +++ b/src/sphinx_autodoc_typehints/__init__.py @@ -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: @@ -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: @@ -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}" diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index d5a4c4c8..d6d54e4f 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -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 @@ -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: @@ -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 diff --git a/whitelist.txt b/whitelist.txt index 7255ccf4..b4feda17 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -28,6 +28,7 @@ kwonlyargs libs metaclass multiline +nptyping param parametrized params