From 601583fa3ad7d3da8e6930567d5726fe95163826 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Wed, 7 Feb 2024 18:23:21 -0800 Subject: [PATCH] Add option to force unions (and options) to be rendered with bars (#418) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/sphinx_autodoc_typehints/__init__.py | 10 +++++- tests/test_sphinx_autodoc_typehints.py | 46 ++++++++++++++++++++---- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py index 106b4f9..792e33a 100644 --- a/src/sphinx_autodoc_typehints/__init__.py +++ b/src/sphinx_autodoc_typehints/__init__.py @@ -215,6 +215,13 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL args_format = "\\[{}]" formatted_args: str | None = "" + always_use_bars_union: bool = getattr(config, "always_use_bars_union", True) + is_bars_union = full_name == "types.UnionType" or ( + always_use_bars_union and type(annotation).__qualname__ == "_UnionGenericAlias" + ) + if is_bars_union: + full_name = "" + # Some types require special handling if full_name == "typing.NewType": args_format = f"\\(``{annotation.__name__}``, {{}})" @@ -248,7 +255,7 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL formatted_args = f"\\[\\[{', '.join(fmt[:-1])}], {fmt[-1]}]" elif full_name == "typing.Literal": formatted_args = f"\\[{', '.join(f'``{arg!r}``' for arg in args)}]" - elif full_name == "types.UnionType": + elif is_bars_union: return " | ".join([format_annotation(arg, config) for arg in args]) if args and not formatted_args: @@ -929,6 +936,7 @@ def setup(app: Sphinx) -> dict[str, bool]: app.add_config_value("typehints_use_rtype", True, "env") # noqa: FBT003 app.add_config_value("typehints_defaults", None, "env") app.add_config_value("simplify_optional_unions", True, "env") # noqa: FBT003 + app.add_config_value("always_use_bars_union", False, "env") # noqa: FBT003 app.add_config_value("typehints_formatter", None, "env") app.add_config_value("typehints_use_signature", False, "env") # noqa: FBT003 app.add_config_value("typehints_use_signature_return", False, "env") # noqa: FBT003 diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index 7a53771..0b68ce6 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -365,7 +365,7 @@ def test_parse_annotation(annotation: Any, module: str, class_name: str, args: t @pytest.mark.parametrize(("annotation", "expected_result"), _CASES) def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str) -> None: - conf = create_autospec(Config, _annotation_globals=globals()) + conf = create_autospec(Config, _annotation_globals=globals(), always_use_bars_union=False) result = format_annotation(annotation, conf) assert result == expected_result @@ -377,7 +377,12 @@ def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str # encapsulate Union in typing.Optional expected_result_not_simplified = ":py:data:`~typing.Optional`\\ \\[" + expected_result_not_simplified expected_result_not_simplified += "]" - conf = create_autospec(Config, simplify_optional_unions=False, _annotation_globals=globals()) + conf = create_autospec( + Config, + simplify_optional_unions=False, + _annotation_globals=globals(), + always_use_bars_union=False, + ) assert format_annotation(annotation, conf) == expected_result_not_simplified # Test with the "fully_qualified" flag turned on @@ -397,7 +402,12 @@ def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str expected_result = expected_result.replace("~nptyping", "nptyping") expected_result = expected_result.replace("~numpy", "numpy") expected_result = expected_result.replace("~" + __name__, __name__) - conf = create_autospec(Config, typehints_fully_qualified=True, _annotation_globals=globals()) + conf = create_autospec( + Config, + typehints_fully_qualified=True, + _annotation_globals=globals(), + always_use_bars_union=False, + ) assert format_annotation(annotation, conf) == expected_result # Test for the correct role (class vs data) using the official Sphinx inventory @@ -413,6 +423,26 @@ def test_format_annotation(inv: Inventory, annotation: Any, expected_result: str assert m.group("role") == expected_role +@pytest.mark.parametrize( + ("annotation", "expected_result"), + [ + ("int | float", ":py:class:`int` | :py:class:`float`"), + ("int | float | None", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), + ("Union[int, float]", ":py:class:`int` | :py:class:`float`"), + ("Union[int, float, None]", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), + ("Optional[int | float]", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), + ("Optional[Union[int, float]]", ":py:class:`int` | :py:class:`float` | :py:obj:`None`"), + ("Union[int | float, str]", ":py:class:`int` | :py:class:`float` | :py:class:`str`"), + ("Union[int, float] | str", ":py:class:`int` | :py:class:`float` | :py:class:`str`"), + ], +) +@pytest.mark.skipif(not PY310_PLUS, reason="| union doesn't work before py310") +def test_always_use_bars_union(annotation: str, expected_result: str) -> None: + conf = create_autospec(Config, always_use_bars_union=True) + result = format_annotation(eval(annotation), conf) # noqa: S307 + assert result == expected_result + + @pytest.mark.parametrize("library", [typing, typing_extensions], ids=["typing", "typing_extensions"]) @pytest.mark.parametrize( ("annotation", "params", "expected_result"), @@ -519,12 +549,13 @@ class dummy_module.DataClass(x) def maybe_fix_py310(expected_contents: str) -> str: + if sys.version_info >= (3, 11): + return expected_contents if not PY310_PLUS: return expected_contents.replace('"', "") for old, new in [ - ("bool | None", '"Optional"["bool"]'), - ("str | None", '"Optional"["str"]'), + ('"str" | "None"', '"Optional"["str"]'), ]: expected_contents = expected_contents.replace(old, new) return expected_contents @@ -550,15 +581,16 @@ def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) Method docstring. Parameters: - * **x** (bool | None) -- foo + * **x** ("bool" | "None") -- foo * **y** ("int" | "str" | "float") -- bar - * **z** (str | None) -- baz + * **z** ("str" | "None") -- baz Return type: "str" """ + expected_contents = dedent(expected_contents) expected_contents = maybe_fix_py310(dedent(expected_contents)) assert contents == expected_contents