diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 0800434..c028c36 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -22,7 +22,6 @@ jobs: - "3.13" - "3.12" - "3.11" - - "3.10" - type - dev - pkg_meta diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aaf240a..fb3e099 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: hooks: - id: prettier additional_dependencies: - - prettier@3.4.2 + - prettier@3.5.1 - "@prettier/plugin-xml@3.4.1" - repo: meta hooks: diff --git a/pyproject.toml b/pyproject.toml index d60d302..1fbdbdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ maintainers = [ authors = [ { name = "Bernát Gábor", email = "gaborjbernat@gmail.com" }, ] -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Sphinx :: Extension", @@ -31,7 +31,6 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -41,16 +40,16 @@ dynamic = [ "version", ] dependencies = [ - "sphinx>=8.1.3", + "sphinx>=8.2", ] optional-dependencies.docs = [ "furo>=2024.8.6", ] optional-dependencies.testing = [ "covdefaults>=2.3", - "coverage>=7.6.10", + "coverage>=7.6.12", "defusedxml>=0.7.1", # required by sphinx.testing - "diff-cover>=9.2.1", + "diff-cover>=9.2.3", "pytest>=8.3.4", "pytest-cov>=6", "sphobjinv>=2.3.1.2", @@ -142,7 +141,7 @@ run.plugins = [ ] [tool.mypy] -python_version = "3.10" +python_version = "3.11" strict = true exclude = "^(.*/roots/.*)|(tests/test_integration.*.py)$" overrides = [ diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py index e621592..432fdf4 100644 --- a/src/sphinx_autodoc_typehints/__init__.py +++ b/src/sphinx_autodoc_typehints/__init__.py @@ -172,11 +172,11 @@ def get_annotation_args(annotation: Any, module: str, class_name: str) -> tuple[ return () if len(result) == 1 and result[0] == () else result # type: ignore[misc] -def format_internal_tuple(t: tuple[Any, ...], config: Config) -> str: +def format_internal_tuple(t: tuple[Any, ...], config: Config, *, short_literals: bool = False) -> str: # An annotation can be a tuple, e.g., for numpy.typing: # In this case, format_annotation receives: # This solution should hopefully be general for *any* type that allows tuples in annotations - fmt = [format_annotation(a, config) for a in t] + fmt = [format_annotation(a, config, short_literals=short_literals) for a in t] if len(fmt) == 0: return "()" if len(fmt) == 1: @@ -196,12 +196,13 @@ def fixup_module_name(config: Config, module: str) -> str: return module -def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PLR0911, PLR0912, PLR0915, PLR0914 +def format_annotation(annotation: Any, config: Config, *, short_literals: bool = False) -> str: # noqa: C901, PLR0911, PLR0912, PLR0915, PLR0914 """ Format the annotation. :param annotation: :param config: + :param short_literals: Render :py:class:`Literals` in PEP 604 style (``|``). :return: """ typehints_formatter: Callable[..., str] | None = getattr(config, "typehints_formatter", None) @@ -222,7 +223,7 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL return format_internal_tuple(annotation, config) if isinstance(annotation, TypeAliasForwardRef): - return str(annotation) + return annotation.name try: module = get_annotation_module(annotation) @@ -254,7 +255,7 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL params = {k: getattr(annotation, f"__{k}__") for k in ("bound", "covariant", "contravariant")} params = {k: v for k, v in params.items() if v} if "bound" in params: - params["bound"] = f" {format_annotation(params['bound'], config)}" + params["bound"] = f" {format_annotation(params['bound'], config, short_literals=short_literals)}" args_format = f"\\(``{annotation.__name__}``{', {}' if args else ''}" if params: args_format += "".join(f", {k}={v}" for k, v in params.items()) @@ -275,20 +276,22 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL args_format = f"\\[:py:data:`{prefix}typing.Union`\\[{{}}]]" 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] + fmt = [format_annotation(arg, config, short_literals=short_literals) for arg in args] formatted_args = f"\\[\\[{', '.join(fmt[:-1])}], {fmt[-1]}]" elif full_name == "typing.Literal": + if short_literals: + return f"\\{' | '.join(f'``{arg!r}``' for arg in args)}" formatted_args = f"\\[{', '.join(f'``{arg!r}``' for arg in args)}]" elif is_bars_union: - return " | ".join([format_annotation(arg, config) for arg in args]) + return " | ".join([format_annotation(arg, config, short_literals=short_literals) for arg in args]) if args and not formatted_args: try: iter(args) except TypeError: - fmt = [format_annotation(args, config)] + fmt = [format_annotation(args, config, short_literals=short_literals)] else: - fmt = [format_annotation(arg, config) for arg in args] + fmt = [format_annotation(arg, config, short_literals=short_literals) for arg in args] formatted_args = args_format.format(", ".join(fmt)) escape = "\\ " if formatted_args else "" @@ -783,7 +786,10 @@ def _inject_signature( if annotation is None: type_annotation = f":type {arg_name}: " else: - formatted_annotation = add_type_css_class(format_annotation(annotation, app.config)) + short_literals = app.config.python_display_short_literal_types + formatted_annotation = add_type_css_class( + format_annotation(annotation, app.config, short_literals=short_literals) + ) type_annotation = f":type {arg_name}: {formatted_annotation}" if app.config.typehints_defaults: @@ -923,7 +929,10 @@ def _inject_rtype( # noqa: PLR0913, PLR0917 if not app.config.typehints_use_rtype and r.found_return and " -- " in lines[insert_index]: return - formatted_annotation = add_type_css_class(format_annotation(type_hints["return"], app.config)) + short_literals = app.config.python_display_short_literal_types + formatted_annotation = add_type_css_class( + format_annotation(type_hints["return"], app.config, short_literals=short_literals) + ) if r.found_param and insert_index < len(lines) and lines[insert_index].strip(): insert_index -= 1 diff --git a/src/sphinx_autodoc_typehints/attributes_patch.py b/src/sphinx_autodoc_typehints/attributes_patch.py index d038eae..276dbd0 100644 --- a/src/sphinx_autodoc_typehints/attributes_patch.py +++ b/src/sphinx_autodoc_typehints/attributes_patch.py @@ -42,12 +42,12 @@ orig_handle_signature = PyAttribute.handle_signature -def _stringify_annotation(app: Sphinx, annotation: Any, mode: str = "") -> str: # noqa: ARG001 +def _stringify_annotation(app: Sphinx, annotation: Any, *args: Any, short_literals: bool = False, **kwargs: Any) -> str: # noqa: ARG001 # Format the annotation with sphinx-autodoc-typehints and inject our magic prefix to tell our patched # PyAttribute.handle_signature to treat it as rst. from . import format_annotation # noqa: PLC0415 - return TYPE_IS_RST_LABEL + format_annotation(annotation, app.config) + return TYPE_IS_RST_LABEL + format_annotation(annotation, app.config, short_literals=short_literals) def patch_attribute_documenter(app: Sphinx) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index 91874ad..19b09f3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ import re import shutil import sys +from contextlib import suppress from pathlib import Path from typing import TYPE_CHECKING @@ -36,11 +37,9 @@ def _remove_sphinx_projects(sphinx_test_tempdir: Path) -> None: # the temporary directory area. # See https://github.com/sphinx-doc/sphinx/issues/4040 for entry in sphinx_test_tempdir.iterdir(): - try: + with suppress(PermissionError): if entry.is_dir() and Path(entry, "_build").exists(): shutil.rmtree(str(entry)) - except PermissionError: # noqa: PERF203 - pass @pytest.fixture diff --git a/tests/test_integration.py b/tests/test_integration.py index c0d218b..900d585 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -9,6 +9,7 @@ from typing import ( # no type comments TYPE_CHECKING, Any, + Literal, NewType, Optional, TypeVar, @@ -661,6 +662,59 @@ def func_with_overload(a: Union[int, str], b: Union[int, str]) -> None: """ +@expected( + """\ +mod.func_literals_long_format(a, b) + + A docstring. + + Parameters: + * **a** ("Literal"["'arg1'", "'arg2'"]) -- Argument that can + take either of two literal values. + + * **b** ("Literal"["'arg1'", "'arg2'"]) -- Argument that can + take either of two literal values. + + Return type: + "None" +""", +) +def func_literals_long_format(a: Literal["arg1", "arg2"], b: Literal["arg1", "arg2"]) -> None: + """ + A docstring. + + :param a: Argument that can take either of two literal values. + :param b: Argument that can take either of two literal values. + """ + + +@expected( + """\ +mod.func_literals_short_format(a, b) + + A docstring. + + Parameters: + * **a** ("'arg1'" | "'arg2'") -- Argument that can take either + of two literal values. + + * **b** ("'arg1'" | "'arg2'") -- Argument that can take either + of two literal values. + + Return type: + "None" +""", + python_display_short_literal_types=True, +) +def func_literals_short_format(a: Literal["arg1", "arg2"], b: Literal["arg1", "arg2"]) -> None: + """ + A docstring. + + :param a: Argument that can take either of two literal values. + :param b: Argument that can take either of two literal values. + """ + + @expected( """\ class mod.TestClassAttributeDocs @@ -1386,7 +1440,7 @@ def has_doctest1() -> None: Unformatted = TypeVar("Unformatted") -@warns("cannot cache unpickable configuration value: 'typehints_formatter'") +@warns("cannot cache unpickleable configuration value: 'typehints_formatter'") @expected( """ mod.typehints_formatter_applied_to_signature(param: Formatted) -> Formatted diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index 31e077e..c0963c2 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -554,17 +554,6 @@ class dummy_module.DataClass(x) assert contents == expected_contents -def maybe_fix_py310(expected_contents: str) -> str: - if sys.version_info >= (3, 11): - return expected_contents - - for old, new in [ - ('"str" | "None"', '"Optional"["str"]'), - ]: - expected_contents = expected_contents.replace(old, new) - return expected_contents - - @pytest.mark.sphinx("text", testroot="dummy") @patch("sphinx.writers.text.MAXWIDTH", 2000) def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) -> None: @@ -595,7 +584,7 @@ def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) "str" """ expected_contents = dedent(expected_contents) - expected_contents = maybe_fix_py310(dedent(expected_contents)) + expected_contents = dedent(expected_contents) assert contents == expected_contents diff --git a/tox.ini b/tox.ini index eef0f28..07cd05a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,12 @@ [tox] requires = - tox>=4.23.2 - tox-uv>=1.16.2 + tox>=4.24.1 + tox-uv>=1.24 env_list = fix 3.13 3.12 3.11 - 3.10 type pkg_meta skip_missing_interpreters = true @@ -45,7 +44,7 @@ commands = [testenv:type] description = run type check on code base deps = - mypy==1.14 + mypy==1.15 types-docutils>=0.21.0.20241128 commands = mypy src @@ -56,8 +55,8 @@ description = check that the long description is valid skip_install = true deps = check-wheel-contents>=0.6.1 - twine>=6.0.1 - uv>=0.5.11 + twine>=6.1 + uv>=0.6.1 commands = uv build --sdist --wheel --out-dir {env_tmp_dir} . twine check {env_tmp_dir}{/}*