From 07746bc34fe44befd3cc8b508056a22a692c0d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 24 Feb 2024 13:19:18 +0100 Subject: [PATCH 01/19] updates CHANGES --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f52b9e3d16b..a776bc474cb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -82,6 +82,8 @@ Bugs fixed Patch by James Addison. * #11962: Fix target resolution when using ``:paramtype:`` fields. Patch by Bénédikt Tran. +* #11995: autodoc: add support for :confval:`python_display_short_literal_types`. + Patch by Bénédikt Tran. Testing ------- From 31877b08402d556d775819603ea241df4449bdf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 24 Feb 2024 13:19:34 +0100 Subject: [PATCH 02/19] fix :confval:`python_display_short_literal_types` --- sphinx/ext/autodoc/__init__.py | 100 ++++++++++++---------- sphinx/ext/autodoc/typehints.py | 9 +- sphinx/ext/napoleon/docstring.py | 8 +- sphinx/util/inspect.py | 41 +++++---- sphinx/util/typing.py | 142 ++++++++++++++++++++++--------- 5 files changed, 193 insertions(+), 107 deletions(-) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 97d1c50848e..e8058feabb0 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -33,7 +33,13 @@ safe_getattr, stringify_signature, ) -from sphinx.util.typing import OptionSpec, get_type_hints, restify, stringify_annotation +from sphinx.util.typing import ( + OptionSpec, + RenderMode, + get_type_hints, + restify, + stringify_annotation, +) if TYPE_CHECKING: from collections.abc import Iterator, Sequence @@ -64,6 +70,20 @@ special_member_re = re.compile(r'^__\S+__$') +def _get_render_mode( + config: Config, default: RenderMode = RenderMode.fully_qualified_except_typing, +) -> RenderMode: + if config.autodoc_typehints_format == "short": + mode = RenderMode.smart + else: + mode = default + + if config.python_display_short_literal_types: + mode |= RenderMode.short_literal + + return mode + + def identity(x: Any) -> Any: return x @@ -499,8 +519,8 @@ def format_signature(self, **kwargs: Any) -> str: retann = self.retann else: # try to introspect the signature + retann = None try: - retann = None args = self._call_format_args(**kwargs) if args: matched = re.match(r'^(\(.*\))\s+->\s+(.*)$', args) @@ -1314,6 +1334,8 @@ def format_args(self, **kwargs: Any) -> str: kwargs.setdefault('show_annotation', False) if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) + if self.config.python_display_short_literal_types: + kwargs.setdefault('short_literal_types', True) try: self.env.app.emit('autodoc-before-process-signature', self.object, False) @@ -1344,6 +1366,8 @@ def add_directive_header(self, sig: str) -> None: def format_signature(self, **kwargs: Any) -> str: if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) + if self.config.python_display_short_literal_types: + kwargs.setdefault('short_literal_types', True) sigs = [] if (self.analyzer and @@ -1606,6 +1630,8 @@ def format_args(self, **kwargs: Any) -> str: kwargs.setdefault('show_annotation', False) if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) + if self.config.python_display_short_literal_types: + kwargs.setdefault('short_literal_types', True) try: self._signature_class, _signature_method_name, sig = self._get_signature() @@ -1644,6 +1670,8 @@ def format_signature(self, **kwargs: Any) -> str: if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) + if self.config.python_display_short_literal_types: + kwargs.setdefault('short_literal_types', True) sig = super().format_signature() sigs = [] @@ -1731,10 +1759,8 @@ def add_directive_header(self, sig: str) -> None: self.env.events.emit('autodoc-process-bases', self.fullname, self.object, self.options, bases) - if self.config.autodoc_typehints_format == "short": - base_classes = [restify(cls, "smart") for cls in bases] - else: - base_classes = [restify(cls) for cls in bases] + mode = _get_render_mode(self.config) + base_classes = [restify(cls, mode) for cls in bases] sourcename = self.get_sourcename() self.add_line('', sourcename) @@ -1845,25 +1871,19 @@ def get_variable_comment(self) -> list[str] | None: return None def add_content(self, more_content: StringList | None) -> None: + mode = _get_render_mode(self.config) + if inspect.isNewType(self.object): - if self.config.autodoc_typehints_format == "short": - supertype = restify(self.object.__supertype__, "smart") - else: - supertype = restify(self.object.__supertype__) + supertype = restify(self.object.__supertype__, mode) more_content = StringList([_('alias of %s') % supertype, ''], source='') if isinstance(self.object, TypeVar): attrs = [repr(self.object.__name__)] + for constraint in self.object.__constraints__: - if self.config.autodoc_typehints_format == "short": - attrs.append(stringify_annotation(constraint, "smart")) - else: - attrs.append(stringify_annotation(constraint)) + attrs.append(stringify_annotation(constraint, mode)) if self.object.__bound__: - if self.config.autodoc_typehints_format == "short": - bound = restify(self.object.__bound__, "smart") - else: - bound = restify(self.object.__bound__) + bound = restify(self.object.__bound__, mode) attrs.append(r"bound=\ " + bound) if self.object.__covariant__: attrs.append("covariant=True") @@ -1884,10 +1904,7 @@ def add_content(self, more_content: StringList | None) -> None: if self.doc_as_attr and not self.get_variable_comment(): try: - if self.config.autodoc_typehints_format == "short": - alias = restify(self.object, "smart") - else: - alias = restify(self.object) + alias = restify(self.object, mode) more_content = StringList([_('alias of %s') % alias], source='') except AttributeError: pass # Invalid class object is passed. @@ -1978,11 +1995,8 @@ def should_suppress_directive_header(self) -> bool: def update_content(self, more_content: StringList) -> None: if inspect.isgenericalias(self.object): - if self.config.autodoc_typehints_format == "short": - alias = restify(self.object, "smart") - else: - alias = restify(self.object) - + mode = _get_render_mode(self.config) + alias = restify(self.object, mode) more_content.append(_('alias of %s') % alias, '') more_content.append('', '') @@ -2095,12 +2109,8 @@ def add_directive_header(self, sig: str) -> None: annotations = get_type_hints(self.parent, None, self.config.autodoc_type_aliases) if self.objpath[-1] in annotations: - if self.config.autodoc_typehints_format == "short": - objrepr = stringify_annotation(annotations.get(self.objpath[-1]), - "smart") - else: - objrepr = stringify_annotation(annotations.get(self.objpath[-1]), - "fully-qualified-except-typing") + mode = _get_render_mode(self.config) + objrepr = stringify_annotation(annotations.get(self.objpath[-1]), mode) self.add_line(' :type: ' + objrepr, sourcename) try: @@ -2190,6 +2200,8 @@ def format_args(self, **kwargs: Any) -> str: kwargs.setdefault('show_annotation', False) if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) + if self.config.python_display_short_literal_types: + kwargs.setdefault('short_literal_types', True) try: if self.object == object.__init__ and self.parent != object: # NoQA: E721 @@ -2242,6 +2254,8 @@ def document_members(self, all_members: bool = False) -> None: def format_signature(self, **kwargs: Any) -> str: if self.config.autodoc_typehints_format == "short": kwargs.setdefault('unqualified_typehints', True) + if self.config.python_display_short_literal_types: + kwargs.setdefault('short_literal_types', True) sigs = [] if (self.analyzer and @@ -2672,12 +2686,8 @@ def add_directive_header(self, sig: str) -> None: annotations = get_type_hints(self.parent, None, self.config.autodoc_type_aliases) if self.objpath[-1] in annotations: - if self.config.autodoc_typehints_format == "short": - objrepr = stringify_annotation(annotations.get(self.objpath[-1]), - "smart") - else: - objrepr = stringify_annotation(annotations.get(self.objpath[-1]), - "fully-qualified-except-typing") + mode = _get_render_mode(self.config) + objrepr = stringify_annotation(annotations.get(self.objpath[-1]), mode) self.add_line(' :type: ' + objrepr, sourcename) try: @@ -2713,11 +2723,11 @@ def get_doc(self) -> list[list[str]] | None: if comment: return [comment] + orig = self.config.autodoc_inherit_docstrings try: # Disable `autodoc_inherit_docstring` temporarily to avoid to obtain # a docstring from the value which descriptor returns unexpectedly. # ref: https://github.com/sphinx-doc/sphinx/issues/7805 - orig = self.config.autodoc_inherit_docstrings self.config.autodoc_inherit_docstrings = False # type: ignore[attr-defined] return super().get_doc() finally: @@ -2808,14 +2818,10 @@ def add_directive_header(self, sig: str) -> None: return try: - signature = inspect.signature(func, - type_aliases=self.config.autodoc_type_aliases) + signature = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) if signature.return_annotation is not Parameter.empty: - if self.config.autodoc_typehints_format == "short": - objrepr = stringify_annotation(signature.return_annotation, "smart") - else: - objrepr = stringify_annotation(signature.return_annotation, - "fully-qualified-except-typing") + mode = _get_render_mode(self.config) + objrepr = stringify_annotation(signature.return_annotation, mode) self.add_line(' :type: ' + objrepr, sourcename) except TypeError as exc: logger.warning(__("Failed to get a function signature for %s: %s"), diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index ac90c58df79..4001ca456b8 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -11,7 +11,7 @@ import sphinx from sphinx import addnodes from sphinx.util import inspect -from sphinx.util.typing import stringify_annotation +from sphinx.util.typing import RenderMode, stringify_annotation if TYPE_CHECKING: from docutils.nodes import Element @@ -24,9 +24,12 @@ def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any, options: Options, args: str, retann: str) -> None: """Record type hints to env object.""" if app.config.autodoc_typehints_format == 'short': - mode = 'smart' + mode = RenderMode.smart else: - mode = 'fully-qualified' + mode = RenderMode.fully_qualified + + if app.config.python_display_short_literal_types: + mode |= RenderMode.short_literal try: if callable(obj): diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index f86b9d2c1b7..78b651e4802 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -11,7 +11,7 @@ from sphinx.locale import _, __ from sphinx.util import logging -from sphinx.util.typing import get_type_hints, stringify_annotation +from sphinx.util.typing import RenderMode, get_type_hints, stringify_annotation if TYPE_CHECKING: from collections.abc import Iterator @@ -880,8 +880,10 @@ def _lookup_annotation(self, _name: str) -> str: ) or {}) self._annotations = get_type_hints(self._obj, None, localns) if _name in self._annotations: - return stringify_annotation(self._annotations[_name], - 'fully-qualified-except-typing') + mode = RenderMode.fully_qualified_except_typing + if getattr(self._config, 'python_display_short_literal_types', None): + mode |= RenderMode.short_literal + return stringify_annotation(self._annotations[_name], mode) # No annotation found return "" diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 120437da6c6..9486ed981f7 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -34,7 +34,7 @@ from sphinx.pycode.ast import unparse as ast_unparse from sphinx.util import logging -from sphinx.util.typing import ForwardRef, stringify_annotation +from sphinx.util.typing import ForwardRef, RenderMode, stringify_annotation logger = logging.getLogger(__name__) @@ -91,7 +91,8 @@ def getannotations(obj: Any) -> Mapping[str, Any]: __annotations__ = safe_getattr(obj, '__annotations__', None) else: # Workaround for bugfix not available until python 3.10 as recommended by docs - # https://docs.python.org/3.10/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older + # https://docs.python.org/3.10/howto/annotations.html#accessing-the-annotations-dict-of + # -an-object-in-python-3-9-and-older __dict__ = safe_getattr(obj, '__dict__', {}) __annotations__ = __dict__.get('__annotations__', None) if isinstance(__annotations__, Mapping): @@ -539,15 +540,16 @@ def _should_unwrap(subject: Callable) -> bool: """Check the function should be unwrapped on getting signature.""" __globals__ = getglobals(subject) if (__globals__.get('__name__') == 'contextlib' and - __globals__.get('__file__') == contextlib.__file__): + __globals__.get('__file__') == contextlib.__file__): # contextmanger should be unwrapped return True return False -def signature(subject: Callable, bound_method: bool = False, type_aliases: dict | None = None, - ) -> inspect.Signature: +def signature( + subject: Callable, bound_method: bool = False, type_aliases: dict | None = None, + ) -> inspect.Signature: """Return a Signature object for the given *subject*. :param bound_method: Specify *subject* is a bound method or not @@ -604,9 +606,10 @@ def signature(subject: Callable, bound_method: bool = False, type_aliases: dict __validate_parameters__=False) -def evaluate_signature(sig: inspect.Signature, globalns: dict | None = None, - localns: dict | None = None, - ) -> inspect.Signature: +def evaluate_signature( + sig: inspect.Signature, globalns: dict | None = None, + localns: dict | None = None, + ) -> inspect.Signature: """Evaluate unresolved type annotations in a signature object.""" def evaluate_forwardref(ref: ForwardRef, globalns: dict, localns: dict) -> Any: """Evaluate a forward reference.""" @@ -649,20 +652,28 @@ def evaluate(annotation: Any, globalns: dict, localns: dict) -> Any: return sig.replace(parameters=parameters, return_annotation=return_annotation) -def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, - show_return_annotation: bool = True, - unqualified_typehints: bool = False) -> str: +def stringify_signature( + sig: inspect.Signature, + show_annotation: bool = True, + show_return_annotation: bool = True, + unqualified_typehints: bool = False, + short_literal_types: bool = False, +) -> str: """Stringify a Signature object. :param show_annotation: If enabled, show annotations on the signature :param show_return_annotation: If enabled, show annotation of the return value :param unqualified_typehints: If enabled, show annotations as unqualified (ex. io.StringIO -> StringIO) + :param short_literal_types: If enabled, use short literal types. """ if unqualified_typehints: - mode = 'smart' + mode = RenderMode.smart else: - mode = 'fully-qualified' + mode = RenderMode.fully_qualified + + if short_literal_types: + mode |= RenderMode.short_literal args = [] last_kind = None @@ -703,8 +714,8 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, concatenated_args = ', '.join(args) if (sig.return_annotation is Parameter.empty or - show_annotation is False or - show_return_annotation is False): + show_annotation is False or + show_return_annotation is False): return f'({concatenated_args})' else: annotation = stringify_annotation(sig.return_annotation, mode) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 67e7fc1d3cf..8af8c322420 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -2,6 +2,8 @@ from __future__ import annotations +import enum +import itertools import sys import types import typing @@ -14,8 +16,6 @@ from docutils.parsers.rst.states import Inliner if TYPE_CHECKING: - import enum - from sphinx.application import Sphinx if sys.version_info >= (3, 10): @@ -71,8 +71,10 @@ def is_invalid_builtin_class(obj: Any) -> bool: PathMatcher = Callable[[str], bool] # common role functions -RoleFunction = Callable[[str, str, str, int, Inliner, dict[str, Any], Sequence[str]], - tuple[list[nodes.Node], list[nodes.system_message]]] +RoleFunction = Callable[ + [str, str, str, int, Inliner, dict[str, Any], Sequence[str]], + tuple[list[nodes.Node], list[nodes.system_message]], +] # A option spec for directive OptionSpec = dict[str, Callable[[str], Any]] @@ -136,7 +138,63 @@ def is_system_TypeVar(typ: Any) -> bool: return modname == 'typing' and isinstance(typ, TypeVar) -def restify(cls: type | None, mode: str = 'fully-qualified-except-typing') -> str: +class RenderMode(enum.Flag): + """Additional flags for rendering annotations or reST content.""" + + # principal modes (mutually exclusive with each other) + + smart = enum.auto() + """Show the annotation name.""" + + fully_qualified = enum.auto() + """Show the module name and qualified name of the annotation. + + This mode is mutually exclusive with :attr:`smart` and :attr:`fully_qualified_except_typing`. + """ # NoQA: E501 + + fully_qualified_except_typing = enum.auto() + """Same as :attr:`fully_qualified` but do not show module name for ``typing`` members. + + This mode is mutually exclusive with :attr:`smart` and :attr:`fully_qualified`. + """ + + # secondary modes (can be used as flags on the primary modes) + + short_literal = enum.auto() + """Use PEP 604 style to render literals.""" + + +_MUTUALLY_EXCLUSIVE_MODES = list(itertools.combinations([ + RenderMode.smart, + RenderMode.fully_qualified, + RenderMode.fully_qualified_except_typing, +], 2)) + + +def _normalize_mode(mode: str | RenderMode) -> RenderMode: # for backwards compatibility + if isinstance(mode, RenderMode): + for a, b in _MUTUALLY_EXCLUSIVE_MODES: + if mode & a and mode & b: + msg = f'mode {a!r} and {b!r} are mutually exclusive' + raise ValueError(msg) + return mode + + if mode == 'smart': + return RenderMode.smart + + if mode == 'fully-qualified': + return RenderMode.fully_qualified + + if mode == 'fully-qualified-except-typing': + return RenderMode.fully_qualified_except_typing + + raise ValueError('unknown render mode: %r' % mode) + + +def restify( + cls: type | None, + mode: str | RenderMode = RenderMode.fully_qualified_except_typing, +) -> str: """Convert python class to a reST reference. :param mode: Specify a method how annotations will be stringified. @@ -150,7 +208,9 @@ def restify(cls: type | None, mode: str = 'fully-qualified-except-typing') -> st from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading from sphinx.util import inspect # lazy loading - if mode == 'smart': + mode = _normalize_mode(mode) + + if mode & RenderMode.smart: modprefix = '~' else: modprefix = '' @@ -228,14 +288,18 @@ def restify(cls: type | None, mode: str = 'fully-qualified-except-typing') -> st args = ', '.join(restify(a, mode) for a in cls.__args__[:-1]) text += fr"\ [[{args}], {restify(cls.__args__[-1], mode)}]" elif cls.__module__ == 'typing' and getattr(origin, '_name', None) == 'Literal': - literal_args = [] - for a in cls.__args__: - if inspect.isenumattribute(a): - literal_args.append(_format_literal_enum_arg(a, mode=mode)) - else: - literal_args.append(repr(a)) - text += r"\ [%s]" % ', '.join(literal_args) - del literal_args + # For now, ignore :confval:`python_display_short_literal_types` + # in restification since the latter is used for 'aliasing' and + # keeping 'Literal' is preferable for readability. + def format_literal_arg(arg: Any) -> str: + if inspect.isenumattribute(arg): + enumcls = arg.__class__ + refname = f'{enumcls.__module__}.{enumcls.__qualname__}.{arg.name}' + if mode & RenderMode.smart or enumcls.__module__ == 'typing': + return f':py:attr:`~{refname}`' + return f':py:attr:`{refname}`' + return repr(arg) + text += r"\ [%s]" % ', '.join(map(format_literal_arg, cls.__args__)) elif cls.__args__: text += r"\ [%s]" % ", ".join(restify(a, mode) for a in cls.__args__) @@ -265,30 +329,32 @@ def restify(cls: type | None, mode: str = 'fully-qualified-except-typing') -> st def stringify_annotation( annotation: Any, /, - mode: str = 'fully-qualified-except-typing', + mode: str | RenderMode = RenderMode.fully_qualified_except_typing, ) -> str: """Stringify type annotation object. :param annotation: The annotation to stringified. :param mode: Specify a method how annotations will be stringified. - 'fully-qualified-except-typing' - Show the module name and qualified name of the annotation except - the "typing" module. - 'smart' - Show the name of the annotation. - 'fully-qualified' - Show the module name and qualified name of the annotation. + The following values can be given as a shorthand of a rendering mode: + + * ``smart`` -- :attr:`RenderMode.smart`. + * ``fully-qualified`` -- :attr:`RenderMode.fully_qualified`. + * ``fully-qualified-except-typing`` -- :attr:`RenderMode.fully_qualified_except_typing`. """ from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading from sphinx.util.inspect import isNewType # lazy loading - if mode not in {'fully-qualified-except-typing', 'fully-qualified', 'smart'}: + if isinstance(mode, str) and mode not in { + 'fully-qualified-except-typing', 'fully-qualified', 'smart', + }: msg = ("'mode' must be one of 'fully-qualified-except-typing', " f"'fully-qualified', or 'smart'; got {mode!r}.") raise ValueError(msg) - if mode == 'smart': + mode = _normalize_mode(mode) + + if mode & RenderMode.smart: module_prefix = '~' else: module_prefix = '' @@ -305,7 +371,10 @@ def stringify_annotation( else: return annotation elif isinstance(annotation, TypeVar): - if annotation_module_is_typing and mode in {'fully-qualified-except-typing', 'smart'}: + if ( + annotation_module_is_typing and + mode & (RenderMode.smart | RenderMode.fully_qualified_except_typing) + ): return annotation_name else: return module_prefix + f'{annotation_module}.{annotation_name}' @@ -342,9 +411,9 @@ def stringify_annotation( module_prefix = f'{annotation_module}.' annotation_forward_arg = getattr(annotation, '__forward_arg__', None) if annotation_qualname or (annotation_module_is_typing and not annotation_forward_arg): - if mode == 'smart': + if mode & RenderMode.smart: module_prefix = '~' + module_prefix - if annotation_module_is_typing and mode == 'fully-qualified-except-typing': + if annotation_module_is_typing and mode & RenderMode.fully_qualified_except_typing: module_prefix = '' else: module_prefix = '' @@ -361,7 +430,7 @@ def stringify_annotation( qualname = annotation_qualname else: qualname = stringify_annotation( - annotation.__origin__, 'fully-qualified-except-typing', + annotation.__origin__, RenderMode.fully_qualified_except_typing, ).replace('typing.', '') # ex. Union elif annotation_qualname: qualname = annotation_qualname @@ -393,7 +462,7 @@ def format_literal_arg(arg: Any) -> str: if isenumattribute(arg): enumcls = arg.__class__ - if mode == 'smart': + if mode & RenderMode.smart: # MyEnum.member return f'{enumcls.__qualname__}.{arg.name}' @@ -401,8 +470,11 @@ def format_literal_arg(arg: Any) -> str: return f'{enumcls.__module__}.{enumcls.__qualname__}.{arg.name}' return repr(arg) - args = ', '.join(map(format_literal_arg, annotation_args)) - return f'{module_prefix}Literal[{args}]' + args = map(format_literal_arg, annotation_args) + + if mode & RenderMode.short_literal: + return ' | '.join(args) + return f"{module_prefix}Literal[{', '.join(args)}]" elif str(annotation).startswith('typing.Annotated'): # for py39+ return stringify_annotation(annotation_args[0], mode) elif all(is_system_TypeVar(a) for a in annotation_args): @@ -415,14 +487,6 @@ def format_literal_arg(arg: Any) -> str: return module_prefix + qualname -def _format_literal_enum_arg(arg: enum.Enum, /, *, mode: str) -> str: - enum_cls = arg.__class__ - if mode == 'smart' or enum_cls.__module__ == 'typing': - return f':py:attr:`~{enum_cls.__module__}.{enum_cls.__qualname__}.{arg.name}`' - else: - return f':py:attr:`{enum_cls.__module__}.{enum_cls.__qualname__}.{arg.name}`' - - # deprecated name -> (object to return, canonical path or empty string) _DEPRECATED_OBJECTS = { 'stringify': (stringify_annotation, 'sphinx.util.typing.stringify_annotation'), From 92eb9452d948a4c4ed9fe1ccc6667f0c96868bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 24 Feb 2024 13:19:39 +0100 Subject: [PATCH 03/19] add tests --- .../roots/test-ext-autodoc/target/literal.py | 9 +- tests/test_extensions/test_ext_autodoc.py | 108 ++++++++++++------ 2 files changed, 80 insertions(+), 37 deletions(-) diff --git a/tests/roots/test-ext-autodoc/target/literal.py b/tests/roots/test-ext-autodoc/target/literal.py index 4340e5103f0..dcf65dab606 100644 --- a/tests/roots/test-ext-autodoc/target/literal.py +++ b/tests/roots/test-ext-autodoc/target/literal.py @@ -6,19 +6,20 @@ class MyEnum(Enum): a = 1 + b = 2 -T = TypeVar('T', bound=Literal[1234]) +T = TypeVar('T', bound=Literal[1234, "abcd"]) """docstring""" -U = TypeVar('U', bound=Literal[MyEnum.a]) +U = TypeVar('U', bound=Literal[MyEnum.a, MyEnum.b]) """docstring""" -def bar(x: Literal[1234]): +def bar(x: Literal[1234, "abcd"]): """docstring""" -def foo(x: Literal[MyEnum.a]): +def foo(x: Literal[MyEnum.a, MyEnum.b]): """docstring""" diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index 6ef4b924910..9f48c090a3c 100644 --- a/tests/test_extensions/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -2484,8 +2484,80 @@ def test_canonical(app): ] -@pytest.mark.sphinx('html', testroot='ext-autodoc') -def test_literal_render(app): +class TestLiteralRender: + @pytest.mark.sphinx('html', testroot='ext-autodoc', freshenv=True) + def test_literal_render(self, app): + # autodoc_typehints_format can take 'short' or 'fully-qualified' values + # and this will be interpreted as 'smart' or 'fully-qualified-except-typing' by restify() + # and 'smart' or 'fully-qualified' by stringify_annotation(). + options = {'members': None, 'exclude-members': 'MyEnum'} + app.config.autodoc_typehints_format = 'short' + actual = do_autodoc(app, 'module', 'target.literal', options) + assert list(actual) == [ + '', + '.. py:module:: target.literal', + '', + *self.bounded_typevar_rst('T', r"\ :py:obj:`~typing.Literal`\ [1234, 'abcd']"), + *self.bounded_typevar_rst('U', r'\ :py:obj:`~typing.Literal`\ [' + r':py:attr:`~target.literal.MyEnum.a`, ' + r':py:attr:`~target.literal.MyEnum.b`]'), + *self.function_rst('bar', "x: ~typing.Literal[1234, 'abcd']"), + *self.function_rst('foo', 'x: ~typing.Literal[MyEnum.a, MyEnum.b]'), + ] + + # restify() assumes that 'fully-qualified' is 'fully-qualified-except-typing' + # because it is more likely that a user wants to suppress 'typing.*' + app.config.autodoc_typehints_format = 'fully-qualified' + actual = do_autodoc(app, 'module', 'target.literal', options) + assert list(actual) == [ + '', + '.. py:module:: target.literal', + '', + *self.bounded_typevar_rst('T', r"\ :py:obj:`~typing.Literal`\ [1234, 'abcd']"), + *self.bounded_typevar_rst('U', r'\ :py:obj:`~typing.Literal`\ [' + r':py:attr:`target.literal.MyEnum.a`, ' + r':py:attr:`target.literal.MyEnum.b`]'), + *self.function_rst('bar', "x: typing.Literal[1234, 'abcd']"), + *self.function_rst('foo', 'x: typing.Literal[' + 'target.literal.MyEnum.a, ' + 'target.literal.MyEnum.b]'), + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc', freshenv=True, + confoverrides={'python_display_short_literal_types': True}) + def test_literal_render_pep604(self, app): + options = {'members': None, 'exclude-members': 'MyEnum'} + app.config.autodoc_typehints_format = 'short' + actual = do_autodoc(app, 'module', 'target.literal', options) + assert list(actual) == [ + '', + '.. py:module:: target.literal', + '', + *self.bounded_typevar_rst('T', r"\ :py:obj:`~typing.Literal`\ [1234, 'abcd']"), + *self.bounded_typevar_rst('U', r'\ :py:obj:`~typing.Literal`\ [' + r':py:attr:`~target.literal.MyEnum.a`, ' + r':py:attr:`~target.literal.MyEnum.b`]'), + *self.function_rst('bar', "x: 1234 | 'abcd'"), + *self.function_rst('foo', 'x: MyEnum.a | MyEnum.b'), + ] + + # restify() assumes that 'fully-qualified' is 'fully-qualified-except-typing' + # because it is more likely that a user wants to suppress 'typing.*' + app.config.autodoc_typehints_format = 'fully-qualified' + actual = do_autodoc(app, 'module', 'target.literal', options) + assert list(actual) == [ + '', + '.. py:module:: target.literal', + '', + *self.bounded_typevar_rst('T', r"\ :py:obj:`~typing.Literal`\ [1234, 'abcd']"), + *self.bounded_typevar_rst('U', r'\ :py:obj:`~typing.Literal`\ [' + r':py:attr:`target.literal.MyEnum.a`, ' + r':py:attr:`target.literal.MyEnum.b`]'), + *self.function_rst('bar', "x: 1234 | 'abcd'"), + *self.function_rst('foo', 'x: target.literal.MyEnum.a | target.literal.MyEnum.b'), + ] + + @staticmethod def bounded_typevar_rst(name, bound): return [ '', @@ -2498,6 +2570,7 @@ def bounded_typevar_rst(name, bound): '', ] + @staticmethod def function_rst(name, sig): return [ '', @@ -2507,34 +2580,3 @@ def function_rst(name, sig): ' docstring', '', ] - - # autodoc_typehints_format can take 'short' or 'fully-qualified' values - # and this will be interpreted as 'smart' or 'fully-qualified-except-typing' by restify() - # and 'smart' or 'fully-qualified' by stringify_annotation(). - - options = {'members': None, 'exclude-members': 'MyEnum'} - app.config.autodoc_typehints_format = 'short' - actual = do_autodoc(app, 'module', 'target.literal', options) - assert list(actual) == [ - '', - '.. py:module:: target.literal', - '', - *bounded_typevar_rst('T', r'\ :py:obj:`~typing.Literal`\ [1234]'), - *bounded_typevar_rst('U', r'\ :py:obj:`~typing.Literal`\ [:py:attr:`~target.literal.MyEnum.a`]'), - *function_rst('bar', 'x: ~typing.Literal[1234]'), - *function_rst('foo', 'x: ~typing.Literal[MyEnum.a]'), - ] - - # restify() assumes that 'fully-qualified' is 'fully-qualified-except-typing' - # because it is more likely that a user wants to suppress 'typing.*' - app.config.autodoc_typehints_format = 'fully-qualified' - actual = do_autodoc(app, 'module', 'target.literal', options) - assert list(actual) == [ - '', - '.. py:module:: target.literal', - '', - *bounded_typevar_rst('T', r'\ :py:obj:`~typing.Literal`\ [1234]'), - *bounded_typevar_rst('U', r'\ :py:obj:`~typing.Literal`\ [:py:attr:`target.literal.MyEnum.a`]'), - *function_rst('bar', 'x: typing.Literal[1234]'), - *function_rst('foo', 'x: typing.Literal[target.literal.MyEnum.a]'), - ] From dbd4628de544a2738cbd613b6efe925c204ba143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 24 Feb 2024 13:25:08 +0100 Subject: [PATCH 04/19] Fix lint. --- sphinx/util/inspect.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 9486ed981f7..c78846cb6ff 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -540,7 +540,7 @@ def _should_unwrap(subject: Callable) -> bool: """Check the function should be unwrapped on getting signature.""" __globals__ = getglobals(subject) if (__globals__.get('__name__') == 'contextlib' and - __globals__.get('__file__') == contextlib.__file__): + __globals__.get('__file__') == contextlib.__file__): # contextmanger should be unwrapped return True @@ -549,7 +549,7 @@ def _should_unwrap(subject: Callable) -> bool: def signature( subject: Callable, bound_method: bool = False, type_aliases: dict | None = None, - ) -> inspect.Signature: +) -> inspect.Signature: """Return a Signature object for the given *subject*. :param bound_method: Specify *subject* is a bound method or not @@ -609,7 +609,7 @@ def signature( def evaluate_signature( sig: inspect.Signature, globalns: dict | None = None, localns: dict | None = None, - ) -> inspect.Signature: +) -> inspect.Signature: """Evaluate unresolved type annotations in a signature object.""" def evaluate_forwardref(ref: ForwardRef, globalns: dict, localns: dict) -> Any: """Evaluate a forward reference.""" @@ -714,8 +714,7 @@ def stringify_signature( concatenated_args = ', '.join(args) if (sig.return_annotation is Parameter.empty or - show_annotation is False or - show_return_annotation is False): + show_annotation is False or show_return_annotation is False): return f'({concatenated_args})' else: annotation = stringify_annotation(sig.return_annotation, mode) From 93f822a31c7d638083e664b824937c8bce194dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 23 Mar 2024 21:15:23 +0100 Subject: [PATCH 05/19] Update CHANGES.rst --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index ec8f1907ab6..70558bd52d3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -135,7 +135,7 @@ Bugs fixed is removed during Sphinx execution. Patch by Bénédikt Tran. * #11995: autodoc: add support for :confval:`python_display_short_literal_types`. - Patch by Bénédikt Tran. + Patch by Bénédikt Tran. Testing ------- From 5ab3b876ef67e87f59650478a7958d58f3f3a54e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 23 Mar 2024 21:29:08 +0100 Subject: [PATCH 06/19] Update __init__.py --- sphinx/ext/autodoc/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index d13bfc62468..5c17c021ab5 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1877,10 +1877,10 @@ def add_content(self, more_content: StringList | None) -> None: more_content = StringList([_('alias of %s') % supertype, ''], source='') if isinstance(self.object, TypeVar): - attrs = [repr(self.object.__name__)] - - for constraint in self.object.__constraints__: - attrs.append(stringify_annotation(constraint, mode)) + attrs = [ + repr(self.object.__name__), + *(stringify_annotation(c, mode) for c in self.object.__constraints__), + ] if self.object.__bound__: bound = restify(self.object.__bound__, mode) attrs.append(r"bound=\ " + bound) From 5cc3657c099f43945af90dfd26428b4ee8f9efec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 23 Mar 2024 22:41:16 +0100 Subject: [PATCH 07/19] Update inspect.py --- sphinx/util/inspect.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 99758be0afd..c1970722d96 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -91,8 +91,7 @@ def getannotations(obj: Any) -> Mapping[str, Any]: __annotations__ = safe_getattr(obj, '__annotations__', None) else: # Workaround for bugfix not available until python 3.10 as recommended by docs - # https://docs.python.org/3.10/howto/annotations.html#accessing-the-annotations-dict-of - # -an-object-in-python-3-9-and-older + # https://docs.python.org/3.10/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older __dict__ = safe_getattr(obj, '__dict__', {}) __annotations__ = __dict__.get('__annotations__', None) if isinstance(__annotations__, Mapping): From 032bf8ce65716c9032bebb1153e0092e35f6bf3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 23 Mar 2024 22:49:45 +0100 Subject: [PATCH 08/19] Update sphinx/util/typing.py Co-authored-by: James Addison <55152140+jayaddison@users.noreply.github.com> --- sphinx/util/typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 928a197be42..e7148b8f97e 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -383,8 +383,8 @@ def stringify_annotation( return annotation elif isinstance(annotation, TypeVar): if ( - annotation_module_is_typing and - mode & (RenderMode.smart | RenderMode.fully_qualified_except_typing) + annotation_module_is_typing and + mode & (RenderMode.smart | RenderMode.fully_qualified_except_typing) ): return annotation_name else: From 8c6f1ed01e3f3a0830272927e1bfc603818bb895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:21:01 +0100 Subject: [PATCH 09/19] revert some format --- sphinx/util/typing.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 5695383bce3..ce4343c0221 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -71,10 +71,8 @@ def is_invalid_builtin_class(obj: Any) -> bool: PathMatcher = Callable[[str], bool] # common role functions -RoleFunction = Callable[ - [str, str, str, int, Inliner, dict[str, Any], Sequence[str]], - tuple[list[nodes.Node], list[nodes.system_message]], -] +RoleFunction = Callable[[str, str, str, int, Inliner, dict[str, Any], Sequence[str]], + tuple[list[nodes.Node], list[nodes.system_message]]] # A option spec for directive OptionSpec = dict[str, Callable[[str], Any]] From ae4145e92f1fe4ea8f76f74066db9cba1fd71263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:31:49 +0100 Subject: [PATCH 10/19] revert some format --- sphinx/util/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index ce4343c0221..4d949fa8f79 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -72,7 +72,7 @@ def is_invalid_builtin_class(obj: Any) -> bool: # common role functions RoleFunction = Callable[[str, str, str, int, Inliner, dict[str, Any], Sequence[str]], - tuple[list[nodes.Node], list[nodes.system_message]]] + tuple[list[nodes.Node], list[nodes.system_message]]] # A option spec for directive OptionSpec = dict[str, Callable[[str], Any]] From deed23c3a8cc6f40b5c8a3a0cbb483ad5387dc9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:55:11 +0200 Subject: [PATCH 11/19] fixup! --- sphinx/util/typing.py | 204 +++++++++++++++++++++++++++++------------- 1 file changed, 144 insertions(+), 60 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index dd40156f9f0..26490d56a39 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -4,11 +4,13 @@ import enum import itertools +import operator import sys import types import typing -from collections.abc import Sequence, Collection +from collections.abc import Sequence from contextvars import Context, ContextVar, Token +from functools import reduce from struct import Struct from typing import ( TYPE_CHECKING, @@ -32,16 +34,6 @@ from sphinx.application import Sphinx - _RestifyMode: TypeAlias = Literal[ - 'fully-qualified-except-typing', - 'smart', - ] - _StringifyMode: TypeAlias = Literal[ - 'fully-qualified-except-typing', - 'fully-qualified', - 'smart', - ] - if sys.version_info >= (3, 10): from types import UnionType else: @@ -175,74 +167,126 @@ def is_system_TypeVar(typ: Any) -> bool: return modname == 'typing' and isinstance(typ, TypeVar) -class RenderMode(enum.Flag): - """Additional flags for rendering annotations or reST content.""" +def _is_annotated_form(obj: Any) -> TypeGuard[Annotated[Any, ...]]: + """Check if *obj* is an annotated type.""" + return typing.get_origin(obj) is Annotated or str(obj).startswith('typing.Annotated') - # principal modes (mutually exclusive with each other) +def _typing_internal_name(obj: Any) -> str | None: + if sys.version_info[:2] >= (3, 10): + return obj.__name__ + return getattr(obj, '_name', None) + + +class _Mode(enum.IntFlag): smart = enum.auto() + fully_qualified = enum.auto() + fully_qualified_except_typing = enum.auto() + short_literal = enum.auto() + + +_VALID_RENDER_MODE_COMBINATIONS: Final[frozenset[int]] = frozenset( + int(reduce(operator.__or__, __modes)) + for __modes in itertools.product({0, *_Mode}, {0, _Mode.short_literal}) +) + + +class RenderMode(enum.IntFlag): + """Additional flags for rendering annotations or reST content.""" + + # primary modes (mutually exclusive) + smart = _Mode.smart """Show the annotation name.""" - fully_qualified = enum.auto() + fully_qualified = _Mode.fully_qualified """Show the module name and qualified name of the annotation. This mode is mutually exclusive with :attr:`smart` and :attr:`fully_qualified_except_typing`. """ # NoQA: E501 - fully_qualified_except_typing = enum.auto() + fully_qualified_except_typing = _Mode.fully_qualified_except_typing """Same as :attr:`fully_qualified` but do not show module name for ``typing`` members. This mode is mutually exclusive with :attr:`smart` and :attr:`fully_qualified`. """ - # secondary modes (can be used as flags on the primary modes) - - short_literal = enum.auto() + # extra bit flags + short_literal = _Mode.short_literal """Use PEP 604 style to render literals.""" + smart_short_literal = smart | short_literal + """Apply PEP 604 style to :attr:`smart`.""" + fully_qualified_short_literal = fully_qualified | short_literal + """Apply PEP 605 style to :attr:`fully_qualified`.""" + fully_qualified_except_typing_short_literal = fully_qualified_except_typing | short_literal + """Apply PEP 604 style to :attr:`fully_qualified_except_typing`.""" -_MUTUALLY_EXCLUSIVE_MODES = list(itertools.combinations([ - RenderMode.smart, - RenderMode.fully_qualified, - RenderMode.fully_qualified_except_typing, -], 2)) - - -def _normalize_mode(mode: _StringifyMode | RenderMode, valid_modes: Collection[_StringifyMode]) -> RenderMode: - if isinstance(mode, RenderMode): - for a, b in _MUTUALLY_EXCLUSIVE_MODES: - if mode & a and mode & b: - msg = f'mode {a!r} and {b!r} are mutually exclusive' - raise ValueError(msg) - return mode - - if mode in valid_modes: - if mode == 'smart': - return RenderMode.smart - elif mode == 'fully-qualified': - return RenderMode.fully_qualified - elif mode == 'fully-qualified-except-typing': - return RenderMode.fully_qualified_except_typing - else: - raise ValueError('invalid string mode name: %r' % mode) + @classmethod + def _missing_(cls, value: object) -> Any: + # As soon as Python 3.11 becomes the minimal version, remove + # the ``_Mode`` enumeration and use the STRICT boundary instead. + if value not in _VALID_RENDER_MODE_COMBINATIONS: + raise ValueError("%r is not a valid %s" % (value, cls.__qualname__)) + return super()._missing_(value) - valid = ', '.join(map(repr, sorted(valid_modes))) - msg = f'mode must be one of {valid}; got {mode!r}' - raise ValueError(msg) +if TYPE_CHECKING: + _RestifyRenderMode: TypeAlias = Literal[ + RenderMode.smart, + RenderMode.smart_short_literal, + RenderMode.fully_qualified_except_typing, + RenderMode.fully_qualified_except_typing_short_literal, + ] -def _is_annotated_form(obj: Any) -> TypeGuard[Annotated[Any, ...]]: - """Check if *obj* is an annotated type.""" - return typing.get_origin(obj) is Annotated or str(obj).startswith('typing.Annotated') + _RestifyMode: TypeAlias = Literal[ + 'smart', + 'fully-qualified-except-typing', + _RestifyRenderMode, + ] + _StringifyRenderMode: TypeAlias = Literal[ + RenderMode.smart, + RenderMode.smart_short_literal, + RenderMode.fully_qualified, + RenderMode.fully_qualified_short_literal, + RenderMode.fully_qualified_except_typing, + RenderMode.fully_qualified_except_typing_short_literal, + ] -def _typing_internal_name(obj: Any) -> str | None: - if sys.version_info[:2] >= (3, 10): - return obj.__name__ - return getattr(obj, '_name', None) + _StringifyMode: TypeAlias = Literal[ + 'smart', + 'fully-qualified', + 'fully-qualified-except-typing', + _StringifyRenderMode + ] -def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> str: +@typing.overload +def _normalize_mode( # NoQA: E704 + mode: Literal['smart'] +) -> Literal[RenderMode.smart]: ... +@typing.overload # NoQA: E302 +def _normalize_mode( # NoQA: E704 + mode: Literal['fully-qualified'] +) -> Literal[RenderMode.fully_qualified]: ... +@typing.overload # NoQA: E302 +def _normalize_mode( # NoQA: E704 + mode: Literal['fully-qualified-except-typing'] +) -> Literal[RenderMode.fully_qualified_except_typing]: ... +def _normalize_mode(mode: str) -> RenderMode: # NoQA: E302 + if mode == 'smart': + return RenderMode.smart + if mode == 'fully-qualified': + return RenderMode.fully_qualified + if mode == 'fully-qualified-except-typing': + return RenderMode.fully_qualified_except_typing + raise ValueError('invalid string mode name: %r' % mode) + + +def restify( + cls: Any, + mode: _RestifyMode = 'fully-qualified-except-typing' +) -> str: """Convert python class to a reST reference. :param mode: Specify a method how annotations will be stringified. @@ -256,7 +300,7 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading from sphinx.util import inspect # lazy loading - mode = _normalize_mode(mode, {'fully-qualified-except-typing', 'smart'}) + mode = _normalize_restify_mode(mode) # things that are not types if cls in {None, NoneType}: @@ -358,6 +402,24 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s return inspect.object_description(cls) +def _normalize_restify_mode(mode: _RestifyMode) -> _RestifyRenderMode: + valid_modes = { + 'smart', + 'fully-qualified', + 'fully-qualified-except-typing', + RenderMode.smart, + RenderMode.smart_short_literal, + RenderMode.fully_qualified_except_typing, + RenderMode.fully_qualified_except_typing_short_literal, + } + + if mode not in valid_modes: + valid = ', '.join(sorted(map(repr, valid_modes))) + msg = f'mode must be one of {valid}; got {mode!r}' + raise ValueError(msg) + return mode if isinstance(mode, RenderMode) else _normalize_mode(mode) + + def _format_literal_arg_restify(arg: Any, /, *, mode: RenderMode) -> str: from sphinx.util.inspect import isenumattribute # lazy loading @@ -377,7 +439,7 @@ def _format_literal_arg_restify(arg: Any, /, *, mode: RenderMode) -> str: def stringify_annotation( annotation: Any, /, - mode: _StringifyMode | RenderMode = RenderMode.fully_qualified_except_typing, + mode: _StringifyMode = RenderMode.fully_qualified_except_typing, ) -> str: """Stringify type annotation object. @@ -393,7 +455,7 @@ def stringify_annotation( from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading from sphinx.util.inspect import isNewType # lazy loading - mode = _normalize_mode(mode, {'fully-qualified-except-typing', 'fully-qualified', 'smart'}) + mode = _normalize_stringify_mode(mode) # things that are not types if annotation in {None, NoneType}: @@ -418,7 +480,9 @@ def stringify_annotation( # Extract the annotation's base type by considering formattable cases if isinstance(annotation, TypeVar): - if annotation_module_is_typing and mode & (RenderMode.smart | RenderMode.fully_qualified_except_typing): + if annotation_module_is_typing and mode & ( + RenderMode.smart | RenderMode.fully_qualified_except_typing + ): return annotation_name return module_prefix + f'{annotation_module}.{annotation_name}' elif isNewType(annotation): @@ -513,12 +577,32 @@ def stringify_annotation( return module_prefix + qualname -def _format_literal_arg_stringify(arg: Any, /, *, mode: str) -> str: +def _normalize_stringify_mode(mode: _StringifyMode) -> _StringifyRenderMode: + valid_modes = { + 'smart', + 'fully-qualified', + 'fully-qualified-except-typing', + RenderMode.smart, + RenderMode.smart_short_literal, + RenderMode.fully_qualified, + RenderMode.fully_qualified_short_literal, + RenderMode.fully_qualified_except_typing, + RenderMode.fully_qualified_except_typing_short_literal, + } + + if mode not in valid_modes: + valid = ', '.join(sorted(map(repr, valid_modes))) + msg = f'mode must be one of {valid}; got {mode!r}' + raise ValueError(msg) + return mode if isinstance(mode, RenderMode) else _normalize_mode(mode) + + +def _format_literal_arg_stringify(arg: Any, /, *, mode: RenderMode) -> str: from sphinx.util.inspect import isenumattribute # lazy loading if isenumattribute(arg): enum_cls = arg.__class__ - if mode == 'smart' or enum_cls.__module__ == 'typing': + if mode & RenderMode.smart or enum_cls.__module__ == 'typing': # MyEnum.member return f'{enum_cls.__qualname__}.{arg.name}' # module.MyEnum.member From 00632e5b7e81c77560e92b44ad75da7518194b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:57:31 +0200 Subject: [PATCH 12/19] revert some mistake --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fdbb6658da9..ff3ca593dcb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -24,6 +24,9 @@ Bugs fixed * #11995: autodoc: add support for :confval:`python_display_short_literal_types`. Patch by Bénédikt Tran. +* #12162: Fix a performance regression in the C domain that has + been present since version 3.0.0. + Patch by Donald Hunter. Testing ------- From cb1c317b12e8f6edbadb715dfa01dda351f62b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:08:19 +0200 Subject: [PATCH 13/19] fixup --- sphinx/util/typing.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 26490d56a39..d15e36f31a8 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -217,7 +217,7 @@ class RenderMode(enum.IntFlag): smart_short_literal = smart | short_literal """Apply PEP 604 style to :attr:`smart`.""" fully_qualified_short_literal = fully_qualified | short_literal - """Apply PEP 605 style to :attr:`fully_qualified`.""" + """Apply PEP 604 style to :attr:`fully_qualified`.""" fully_qualified_except_typing_short_literal = fully_qualified_except_typing | short_literal """Apply PEP 604 style to :attr:`fully_qualified_except_typing`.""" @@ -283,19 +283,15 @@ def _normalize_mode(mode: str) -> RenderMode: # NoQA: E302 raise ValueError('invalid string mode name: %r' % mode) -def restify( - cls: Any, - mode: _RestifyMode = 'fully-qualified-except-typing' -) -> str: +def restify(cls: Any, mode: _RestifyMode = RenderMode.fully_qualified_except_typing) -> str: """Convert python class to a reST reference. :param mode: Specify a method how annotations will be stringified. - 'fully-qualified-except-typing' - Show the module name and qualified name of the annotation except - the "typing" module. - 'smart' - Show the name of the annotation. + The following values can be given as a shorthand of a rendering mode: + + * ``smart`` -- :attr:`RenderMode.smart`. + * ``fully-qualified-except-typing`` -- :attr:`RenderMode.fully_qualified_except_typing`. """ from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading from sphinx.util import inspect # lazy loading @@ -437,9 +433,7 @@ def _format_literal_arg_restify(arg: Any, /, *, mode: RenderMode) -> str: def stringify_annotation( - annotation: Any, - /, - mode: _StringifyMode = RenderMode.fully_qualified_except_typing, + annotation: Any, /, mode: _StringifyMode = RenderMode.fully_qualified_except_typing ) -> str: """Stringify type annotation object. From 0b6f61a3dbed71bad90ec53275203589c7c82c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:13:34 +0200 Subject: [PATCH 14/19] fixup --- sphinx/ext/autodoc/__init__.py | 15 ++++++++------- sphinx/ext/napoleon/docstring.py | 8 +++++--- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index ab06a54634d..e28e0fdfc7f 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -13,12 +13,12 @@ import sys import warnings from inspect import Parameter, Signature -from typing import TYPE_CHECKING, Any, Callable, ClassVar, TypeVar +from typing import TYPE_CHECKING, TypeVar from docutils.statemachine import StringList import sphinx -from sphinx.config import ENUM, Config +from sphinx.config import ENUM from sphinx.deprecation import RemovedInSphinx80Warning from sphinx.ext.autodoc.importer import get_class_members, import_module, import_object from sphinx.ext.autodoc.mock import ismock, mock, undecorate @@ -34,8 +34,6 @@ stringify_signature, ) from sphinx.util.typing import ( - ExtensionMetadata, - OptionSpec, RenderMode, get_type_hints, restify, @@ -45,10 +43,13 @@ if TYPE_CHECKING: from collections.abc import Iterator, Sequence from types import ModuleType + from typing import Any, Callable, ClassVar from sphinx.application import Sphinx + from sphinx.config import Config from sphinx.environment import BuildEnvironment from sphinx.ext.autodoc.directive import DocumenterBridge + from sphinx.util.typing import ExtensionMetadata, OptionSpec, _RestifyRenderMode logger = logging.getLogger(__name__) @@ -72,10 +73,10 @@ def _get_render_mode( - config: Config, default: RenderMode = RenderMode.fully_qualified_except_typing, -) -> RenderMode: + config: Config, default: _RestifyRenderMode = RenderMode.fully_qualified_except_typing, +) -> _RestifyRenderMode: if config.autodoc_typehints_format == "short": - mode = RenderMode.smart + mode: _RestifyRenderMode = RenderMode.smart else: mode = default diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index e8f87a16583..acdd6043d23 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -8,7 +8,7 @@ import re from functools import partial from itertools import starmap -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING from sphinx.locale import _, __ from sphinx.util import logging @@ -16,9 +16,11 @@ if TYPE_CHECKING: from collections.abc import Iterator + from typing import Any, Callable from sphinx.application import Sphinx from sphinx.config import Config as SphinxConfig + from sphinx.util.typing import _StringifyMode logger = logging.getLogger(__name__) @@ -880,9 +882,9 @@ def _lookup_annotation(self, _name: str) -> str: ) or {}) self._annotations = get_type_hints(self._obj, None, localns) if _name in self._annotations: - mode = RenderMode.fully_qualified_except_typing + mode: _StringifyMode = RenderMode.fully_qualified_except_typing if getattr(self._config, 'python_display_short_literal_types', None): - mode |= RenderMode.short_literal + mode = RenderMode.fully_qualified_except_typing_short_literal return stringify_annotation(self._annotations[_name], mode) # No annotation found return "" From c4731c7ed78a5c31bd12eb125ef58581405926e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 25 Apr 2024 09:08:10 +0200 Subject: [PATCH 15/19] cleanup --- sphinx/ext/autodoc/__init__.py | 4 ++-- sphinx/ext/napoleon/docstring.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index e28e0fdfc7f..dfbbac4d7c9 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -41,9 +41,9 @@ ) if TYPE_CHECKING: - from collections.abc import Iterator, Sequence + from collections.abc import Callable, Iterator, Sequence from types import ModuleType - from typing import Any, Callable, ClassVar + from typing import Any, ClassVar from sphinx.application import Sphinx from sphinx.config import Config diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index acdd6043d23..de9b0078af8 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -15,8 +15,8 @@ from sphinx.util.typing import RenderMode, get_type_hints, stringify_annotation if TYPE_CHECKING: - from collections.abc import Iterator - from typing import Any, Callable + from collections.abc import Callable, Iterator + from typing import Any from sphinx.application import Sphinx from sphinx.config import Config as SphinxConfig From 520363f99566773573bbd79f63ac1a49cf1e559c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 20 Jul 2024 13:27:31 +0200 Subject: [PATCH 16/19] update CHANGES & fixup --- CHANGES.rst | 5 ++--- sphinx/util/typing.py | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4bcb6c1ec4d..c11a910ed1f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,8 @@ Bugs fixed ``sphinx.ext.autodoc``, especially when using :mod:`dataclasses` as type metadata. Patch by Adam Turner. +* #11995: autodoc: add support for :confval:`python_display_short_literal_types`. + Patch by Bénédikt Tran. Release 7.4.6 (released Jul 18, 2024) ===================================== @@ -248,9 +250,6 @@ Bugs fixed * #12543: Fix :pep:`695` formatting for LaTeX output. Patch by Bénédikt Tran. -* #11995: autodoc: add support for :confval:`python_display_short_literal_types`. - Patch by Bénédikt Tran. - Testing ------- diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index f333f0b751c..41bc68dc7e1 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -495,7 +495,9 @@ def _format_literal_arg_restify(arg: Any, /, *, mode: RenderMode) -> str: def stringify_annotation( - annotation: Any, /, mode: _StringifyMode = RenderMode.fully_qualified_except_typing + annotation: Any, + /, + mode: _StringifyMode = RenderMode.fully_qualified_except_typing, ) -> str: """Stringify type annotation object. From 327011f9afa892bc07ebd0cffde60526173c4755 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 2 Feb 2025 21:12:07 +0000 Subject: [PATCH 17/19] post-merge --- sphinx/ext/autodoc/__init__.py | 20 +- sphinx/ext/autodoc/typehints.py | 1 - sphinx/ext/napoleon/docstring.py | 9 +- sphinx/util/inspect.py | 3 +- sphinx/util/typing.py | 50 ++--- .../roots/test-ext-autodoc/target/literal.py | 4 +- tests/test_extensions/test_ext_autodoc.py | 208 ++++++++++-------- 7 files changed, 159 insertions(+), 136 deletions(-) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 93845c3a492..fe4e4933193 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -32,7 +32,6 @@ safe_getattr, stringify_signature, ) -from sphinx.util.typing import get_type_hints, restify, stringify_annotation from sphinx.util.typing import ( RenderMode, get_type_hints, @@ -44,7 +43,6 @@ from collections.abc import Callable, Iterator, Sequence from types import ModuleType from typing import ClassVar, Literal, TypeAlias - from typing import Any, ClassVar from sphinx.application import Sphinx from sphinx.config import Config @@ -52,7 +50,6 @@ from sphinx.events import EventManager from sphinx.ext.autodoc.directive import DocumenterBridge from sphinx.util.typing import ExtensionMetadata, OptionSpec, _RestifyRenderMode - from sphinx.util.typing import ExtensionMetadata, OptionSpec _AutodocObjType = Literal[ 'module', 'class', 'exception', 'function', 'method', 'attribute' @@ -85,9 +82,10 @@ def _get_render_mode( - config: Config, default: _RestifyRenderMode = RenderMode.fully_qualified_except_typing, + config: Config, + default: _RestifyRenderMode = RenderMode.fully_qualified_except_typing, ) -> _RestifyRenderMode: - if config.autodoc_typehints_format == "short": + if config.autodoc_typehints_format == 'short': mode: _RestifyRenderMode = RenderMode.smart else: mode = default @@ -2325,7 +2323,9 @@ def add_directive_header(self, sig: str) -> None: ) if self.objpath[-1] in annotations: mode = _get_render_mode(self.config) - objrepr = stringify_annotation(annotations.get(self.objpath[-1]), mode) + objrepr = stringify_annotation( + annotations.get(self.objpath[-1]), mode + ) if self.config.autodoc_typehints_format == 'short': objrepr = stringify_annotation( annotations.get(self.objpath[-1]), 'smart' @@ -2981,7 +2981,9 @@ def add_directive_header(self, sig: str) -> None: ) if self.objpath[-1] in annotations: mode = _get_render_mode(self.config) - objrepr = stringify_annotation(annotations.get(self.objpath[-1]), mode) + objrepr = stringify_annotation( + annotations.get(self.objpath[-1]), mode + ) if self.config.autodoc_typehints_format == 'short': objrepr = stringify_annotation( annotations.get(self.objpath[-1]), 'smart' @@ -3122,7 +3124,9 @@ def add_directive_header(self, sig: str) -> None: return try: - signature = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) + signature = inspect.signature( + func, type_aliases=self.config.autodoc_type_aliases + ) if signature.return_annotation is not Parameter.empty: mode = _get_render_mode(self.config) objrepr = stringify_annotation(signature.return_annotation, mode) diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index c570cd0c55f..0e044353d55 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -10,7 +10,6 @@ import sphinx from sphinx import addnodes from sphinx.util import inspect -from sphinx.util.typing import stringify_annotation from sphinx.util.typing import ExtensionMetadata, RenderMode, stringify_annotation if TYPE_CHECKING: diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index f06aa7e5f15..cf0f25979eb 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -9,7 +9,6 @@ from functools import partial from itertools import starmap from typing import TYPE_CHECKING, Any -from typing import TYPE_CHECKING from sphinx.locale import _, __ from sphinx.util import logging @@ -17,7 +16,6 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterator - from typing import Any from sphinx.application import Sphinx from sphinx.config import Config as SphinxConfig @@ -1099,10 +1097,11 @@ def _lookup_annotation(self, _name: str) -> str: self._annotations = get_type_hints(self._obj, None, localns) if _name in self._annotations: mode: _StringifyMode = RenderMode.fully_qualified_except_typing - if getattr(self._config, 'python_display_short_literal_types', None): + if getattr( + self._config, 'python_display_short_literal_types', None + ): mode = RenderMode.fully_qualified_except_typing_short_literal - return stringify_annotation(self._annotations[_name], mode - ) + return stringify_annotation(self._annotations[_name], mode) # No annotation found return '' diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 5d78afc2b74..0cc6d0a03ba 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -21,8 +21,7 @@ from sphinx.pycode.ast import unparse as ast_unparse from sphinx.util import logging -from sphinx.util.typing import stringify_annotation -from sphinx.util.typing import ForwardRef, RenderMode, stringify_annotation +from sphinx.util.typing import RenderMode, stringify_annotation if TYPE_CHECKING: from collections.abc import Callable, Iterator, Sequence diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index f441ff76db5..c11d6fd0e42 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -10,20 +10,11 @@ import types import typing from collections.abc import Callable, Sequence -from typing import TYPE_CHECKING -from collections.abc import Sequence -from contextvars import Context, ContextVar, Token from functools import reduce -from struct import Struct from typing import ( TYPE_CHECKING, Annotated, Any, - Callable, - ForwardRef, - TypedDict, - TypeVar, - Union, ) from docutils import nodes @@ -285,7 +276,9 @@ class RenderMode(enum.IntFlag): """Apply PEP 604 style to :attr:`smart`.""" fully_qualified_short_literal = fully_qualified | short_literal """Apply PEP 604 style to :attr:`fully_qualified`.""" - fully_qualified_except_typing_short_literal = fully_qualified_except_typing | short_literal + fully_qualified_except_typing_short_literal = ( + fully_qualified_except_typing | short_literal + ) """Apply PEP 604 style to :attr:`fully_qualified_except_typing`.""" @classmethod @@ -293,7 +286,7 @@ def _missing_(cls, value: object) -> Any: # As soon as Python 3.11 becomes the minimal version, remove # the ``_Mode`` enumeration and use the STRICT boundary instead. if value not in _VALID_RENDER_MODE_COMBINATIONS: - raise ValueError("%r is not a valid %s" % (value, cls.__qualname__)) + raise ValueError('%r is not a valid %s' % (value, cls.__qualname__)) return super()._missing_(value) @@ -324,23 +317,23 @@ def _missing_(cls, value: object) -> Any: 'smart', 'fully-qualified', 'fully-qualified-except-typing', - _StringifyRenderMode + _StringifyRenderMode, ] @typing.overload -def _normalize_mode( # NoQA: E704 - mode: Literal['smart'] +def _normalize_mode( + mode: Literal['smart'], ) -> Literal[RenderMode.smart]: ... -@typing.overload # NoQA: E302 -def _normalize_mode( # NoQA: E704 - mode: Literal['fully-qualified'] +@typing.overload +def _normalize_mode( + mode: Literal['fully-qualified'], ) -> Literal[RenderMode.fully_qualified]: ... -@typing.overload # NoQA: E302 -def _normalize_mode( # NoQA: E704 - mode: Literal['fully-qualified-except-typing'] +@typing.overload +def _normalize_mode( + mode: Literal['fully-qualified-except-typing'], ) -> Literal[RenderMode.fully_qualified_except_typing]: ... -def _normalize_mode(mode: str) -> RenderMode: # NoQA: E302 +def _normalize_mode(mode: str) -> RenderMode: if mode == 'smart': return RenderMode.smart if mode == 'fully-qualified': @@ -350,7 +343,9 @@ def _normalize_mode(mode: str) -> RenderMode: # NoQA: E302 raise ValueError('invalid string mode name: %r' % mode) -def restify(cls: Any, mode: _RestifyMode = RenderMode.fully_qualified_except_typing) -> str: +def restify( + cls: Any, mode: _RestifyMode = RenderMode.fully_qualified_except_typing +) -> str: """Convert a type-like object to a reST reference. :param mode: Specify a method how annotations will be stringified. @@ -606,7 +601,10 @@ def stringify_annotation( ): if mode & RenderMode.smart: module_prefix = f'~{module_prefix}' - if annotation_module_is_typing and mode & RenderMode.fully_qualified_except_typing: + if ( + annotation_module_is_typing + and mode & RenderMode.fully_qualified_except_typing + ): module_prefix = '' elif _is_unpack_form(annotation) and annotation_module == 'typing_extensions': module_prefix = '~' if mode & RenderMode.smart else '' @@ -664,10 +662,12 @@ def stringify_annotation( returns = stringify_annotation(annotation_args[-1], mode) return f'{module_prefix}Callable[[{args}], {returns}]' elif qualname == 'Literal': - args = (_format_literal_arg_stringify(a, mode=mode) for a in annotation_args) + args = ( + _format_literal_arg_stringify(a, mode=mode) for a in annotation_args + ) if mode & RenderMode.short_literal: return ' | '.join(args) - return f"{module_prefix}Literal[{', '.join(args)}]" + return f'{module_prefix}Literal[{", ".join(args)}]' elif _is_annotated_form(annotation): # for py310+ args = stringify_annotation(annotation_args[0], mode) meta_args = [] diff --git a/tests/roots/test-ext-autodoc/target/literal.py b/tests/roots/test-ext-autodoc/target/literal.py index dcf65dab606..4ba841c7f10 100644 --- a/tests/roots/test-ext-autodoc/target/literal.py +++ b/tests/roots/test-ext-autodoc/target/literal.py @@ -9,7 +9,7 @@ class MyEnum(Enum): b = 2 -T = TypeVar('T', bound=Literal[1234, "abcd"]) +T = TypeVar('T', bound=Literal[1234, 'abcd']) """docstring""" @@ -17,7 +17,7 @@ class MyEnum(Enum): """docstring""" -def bar(x: Literal[1234, "abcd"]): +def bar(x: Literal[1234, 'abcd']): """docstring""" diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index 8d605a2a9c5..b078d8a4872 100644 --- a/tests/test_extensions/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -3119,104 +3119,126 @@ def test_canonical(app): ] -class TestLiteralRender: - @pytest.mark.sphinx('html', testroot='ext-autodoc', freshenv=True) - def test_literal_render(self, app): - # autodoc_typehints_format can take 'short' or 'fully-qualified' values - # and this will be interpreted as 'smart' or 'fully-qualified-except-typing' by restify() - # and 'smart' or 'fully-qualified' by stringify_annotation(). - options = {'members': None, 'exclude-members': 'MyEnum', - }app.config.autodoc_typehints_format = 'short' - actual = do_autodoc(app, 'module', 'target.literal', options) - assert list(actual) == [ - '', - '.. py:module:: target.literal', - '', - *self.bounded_typevar_rst('T', r"\ :py:obj:`~typing.Literal`\ [1234, 'abcd']"), - *self.bounded_typevar_rst( - 'U', r'\ :py:obj:`~typing.Literal`\ [' - r':py:attr:`~target.literal.MyEnum.a`, ' - r':py:attr:`~target.literal.MyEnum.b`]'), - *self.function_rst('bar', "x: ~typing.Literal[1234, 'abcd']"), - *self.function_rst('foo', 'x: ~typing.Literal[MyEnum.a, MyEnum.b]'), - ] +def bounded_typevar_rst(name, bound): + return [ + '', + f'.. py:class:: {name}', + ' :module: target.literal', + '', + ' docstring', + '', + f' alias of TypeVar({name!r}, bound={bound})', + '', + ] - # restify() assumes that 'fully-qualified' is 'fully-qualified-except-typing' - # because it is more likely that a user wants to suppress 'typing.*' - app.config.autodoc_typehints_format = 'fully-qualified' - actual = do_autodoc(app, 'module', 'target.literal', options) - assert list(actual) == [ - '', - '.. py:module:: target.literal', - '', - *self.bounded_typevar_rst('T', r"\ :py:obj:`~typing.Literal`\ [1234, 'abcd']"), - *self.bounded_typevar_rst( - 'U', r'\ :py:obj:`~typing.Literal`\ [' - r':py:attr:`target.literal.MyEnum.a`, ' - r':py:attr:`target.literal.MyEnum.b`]'), - *self.function_rst('bar', "x: typing.Literal[1234, 'abcd']"), - *self.function_rst('foo', 'x: typing.Literal[' - 'target.literal.MyEnum.a, ' - 'target.literal.MyEnum.b]'), - ] - @pytest.mark.sphinx('html', testroot='ext-autodoc', freshenv=True, - confoverrides={'python_display_short_literal_types': True}) - def test_literal_render_pep604(self, app): - options = {'members': None, 'exclude-members': 'MyEnum'} - app.config.autodoc_typehints_format = 'short' - actual = do_autodoc(app, 'module', 'target.literal', options) - assert list(actual) == [ - '', - '.. py:module:: target.literal', - '', - *self.bounded_typevar_rst('T', r"\ :py:obj:`~typing.Literal`\ [1234, 'abcd']"), - *self.bounded_typevar_rst('U', r'\ :py:obj:`~typing.Literal`\ [' - r':py:attr:`~target.literal.MyEnum.a`, ' - r':py:attr:`~target.literal.MyEnum.b`]'), - *self.function_rst('bar', "x: 1234 | 'abcd'"), - *self.function_rst('foo', 'x: MyEnum.a | MyEnum.b'), - ] +def function_rst(name, sig): + return [ + '', + f'.. py:function:: {name}({sig})', + ' :module: target.literal', + '', + ' docstring', + '', + ] - # restify() assumes that 'fully-qualified' is 'fully-qualified-except-typing' - # because it is more likely that a user wants to suppress 'typing.*' - app.config.autodoc_typehints_format = 'fully-qualified' - actual = do_autodoc(app, 'module', 'target.literal', options) - assert list(actual) == [ - '', - '.. py:module:: target.literal', - '', - *self.bounded_typevar_rst('T', r"\ :py:obj:`~typing.Literal`\ [1234, 'abcd']"), - *self.bounded_typevar_rst('U', r'\ :py:obj:`~typing.Literal`\ [' - r':py:attr:`target.literal.MyEnum.a`, ' - r':py:attr:`target.literal.MyEnum.b`]'), - *self.function_rst('bar', "x: 1234 | 'abcd'"), - *self.function_rst('foo', 'x: target.literal.MyEnum.a | target.literal.MyEnum.b'), - ] - @staticmethod - def bounded_typevar_rst(name, bound): - return [ - '', - f'.. py:class:: {name}', - ' :module: target.literal', - '', - ' docstring', - '', - f' alias of TypeVar({name!r}, bound={bound})', - '', - ] +@pytest.mark.sphinx('html', testroot='ext-autodoc', freshenv=True) +def test_literal_render(app): + # autodoc_typehints_format can take 'short' or 'fully-qualified' values + # and this will be interpreted as 'smart' or 'fully-qualified-except-typing' by restify() + # and 'smart' or 'fully-qualified' by stringify_annotation(). - @staticmethod - def function_rst(name, sig): - return [ - '', - f'.. py:function:: {name}({sig})', - ' :module: target.literal', - '', - ' docstring', - '', - ] + options = { + 'members': None, + 'exclude-members': 'MyEnum', + } + app.config.autodoc_typehints_format = 'short' + actual = do_autodoc(app, 'module', 'target.literal', options) + assert list(actual) == [ + '', + '.. py:module:: target.literal', + '', + *bounded_typevar_rst('T', r"\ :py:obj:`~typing.Literal`\ [1234, 'abcd']"), + *bounded_typevar_rst( + 'U', + r'\ :py:obj:`~typing.Literal`\ [' + r':py:attr:`~target.literal.MyEnum.a`, ' + r':py:attr:`~target.literal.MyEnum.b`]', + ), + *function_rst('bar', "x: ~typing.Literal[1234, 'abcd']"), + *function_rst('foo', 'x: ~typing.Literal[MyEnum.a, MyEnum.b]'), + ] + + # restify() assumes that 'fully-qualified' is 'fully-qualified-except-typing' + # because it is more likely that a user wants to suppress 'typing.*' + app.config.autodoc_typehints_format = 'fully-qualified' + actual = do_autodoc(app, 'module', 'target.literal', options) + assert list(actual) == [ + '', + '.. py:module:: target.literal', + '', + *bounded_typevar_rst('T', r"\ :py:obj:`~typing.Literal`\ [1234, 'abcd']"), + *bounded_typevar_rst( + 'U', + r'\ :py:obj:`~typing.Literal`\ [' + r':py:attr:`target.literal.MyEnum.a`, ' + r':py:attr:`target.literal.MyEnum.b`]', + ), + *function_rst('bar', "x: typing.Literal[1234, 'abcd']"), + *function_rst( + 'foo', + 'x: typing.Literal[target.literal.MyEnum.a, target.literal.MyEnum.b]', + ), + ] + + +@pytest.mark.sphinx( + 'html', + testroot='ext-autodoc', + freshenv=True, + confoverrides={'python_display_short_literal_types': True}, +) +def test_literal_render_pep604(self, app): + options = { + 'members': None, + 'exclude-members': 'MyEnum', + } + app.config.autodoc_typehints_format = 'short' + actual = do_autodoc(app, 'module', 'target.literal', options) + assert list(actual) == [ + '', + '.. py:module:: target.literal', + '', + *bounded_typevar_rst('T', r"\ :py:obj:`~typing.Literal`\ [1234, 'abcd']"), + *bounded_typevar_rst( + 'U', + r'\ :py:obj:`~typing.Literal`\ [' + r':py:attr:`~target.literal.MyEnum.a`, ' + r':py:attr:`~target.literal.MyEnum.b`]', + ), + *function_rst('bar', "x: 1234 | 'abcd'"), + *function_rst('foo', 'x: MyEnum.a | MyEnum.b'), + ] + + # restify() assumes that 'fully-qualified' is 'fully-qualified-except-typing' + # because it is more likely that a user wants to suppress 'typing.*' + app.config.autodoc_typehints_format = 'fully-qualified' + actual = do_autodoc(app, 'module', 'target.literal', options) + assert list(actual) == [ + '', + '.. py:module:: target.literal', + '', + *bounded_typevar_rst('T', r"\ :py:obj:`~typing.Literal`\ [1234, 'abcd']"), + *bounded_typevar_rst( + 'U', + r'\ :py:obj:`~typing.Literal`\ [' + r':py:attr:`target.literal.MyEnum.a`, ' + r':py:attr:`target.literal.MyEnum.b`]', + ), + *function_rst('bar', "x: 1234 | 'abcd'"), + *function_rst('foo', 'x: target.literal.MyEnum.a | target.literal.MyEnum.b'), + ] @pytest.mark.sphinx('html', testroot='ext-autodoc') From 04bc826caf8f0a3f05fc5d721a44e801e0517416 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 2 Feb 2025 21:20:38 +0000 Subject: [PATCH 18/19] Drop _Mode --- sphinx/util/typing.py | 33 ++++++--------------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index c11d6fd0e42..1cfb0e72229 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -236,40 +236,27 @@ def _is_unpack_form(obj: Any) -> bool: return typing.get_origin(obj) is typing.Unpack -class _Mode(enum.IntFlag): - smart = enum.auto() - fully_qualified = enum.auto() - fully_qualified_except_typing = enum.auto() - short_literal = enum.auto() - - -_VALID_RENDER_MODE_COMBINATIONS: Final[frozenset[int]] = frozenset( - int(reduce(operator.__or__, __modes)) - for __modes in itertools.product({0, *_Mode}, {0, _Mode.short_literal}) -) - - -class RenderMode(enum.IntFlag): +class RenderMode(enum.IntFlag, boundary=enum.STRICT): """Additional flags for rendering annotations or reST content.""" # primary modes (mutually exclusive) - smart = _Mode.smart + smart = enum.auto() """Show the annotation name.""" - fully_qualified = _Mode.fully_qualified + fully_qualified = enum.auto() """Show the module name and qualified name of the annotation. This mode is mutually exclusive with :attr:`smart` and :attr:`fully_qualified_except_typing`. - """ # NoQA: E501 + """ - fully_qualified_except_typing = _Mode.fully_qualified_except_typing + fully_qualified_except_typing = enum.auto() """Same as :attr:`fully_qualified` but do not show module name for ``typing`` members. This mode is mutually exclusive with :attr:`smart` and :attr:`fully_qualified`. """ # extra bit flags - short_literal = _Mode.short_literal + short_literal = enum.auto() """Use PEP 604 style to render literals.""" smart_short_literal = smart | short_literal @@ -281,14 +268,6 @@ class RenderMode(enum.IntFlag): ) """Apply PEP 604 style to :attr:`fully_qualified_except_typing`.""" - @classmethod - def _missing_(cls, value: object) -> Any: - # As soon as Python 3.11 becomes the minimal version, remove - # the ``_Mode`` enumeration and use the STRICT boundary instead. - if value not in _VALID_RENDER_MODE_COMBINATIONS: - raise ValueError('%r is not a valid %s' % (value, cls.__qualname__)) - return super()._missing_(value) - if TYPE_CHECKING: _RestifyRenderMode: TypeAlias = Literal[ From bb6a01946857421a129068ef614f23443d79fe01 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 2 Feb 2025 22:13:12 +0000 Subject: [PATCH 19/19] Remove all RenderMode enum changes --- CHANGES.rst | 4 +- sphinx/ext/autodoc/__init__.py | 112 ++++------ sphinx/ext/autodoc/typehints.py | 19 +- sphinx/ext/napoleon/docstring.py | 17 +- sphinx/util/inspect.py | 26 ++- sphinx/util/typing.py | 261 +++++++--------------- tests/test_extensions/test_ext_autodoc.py | 2 +- 7 files changed, 161 insertions(+), 280 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1c63d7c1683..18e7a8038d4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -94,8 +94,8 @@ Features added * #8191, #8159: Add :rst:dir:`inheritance-diagram:include-subclasses` option to the :rst:dir:`inheritance-diagram` directive. Patch by Walter Dörwald. -* #11995: autodoc: add support for :confval:`python_display_short_literal_types`. - Patch by Bénédikt Tran. +* #11995: autodoc: Add support for :confval:`python_display_short_literal_types`. + Patch by Bénédikt Tran and Adam Turner. Bugs fixed ---------- diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index fe4e4933193..5c2b202b927 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -32,12 +32,7 @@ safe_getattr, stringify_signature, ) -from sphinx.util.typing import ( - RenderMode, - get_type_hints, - restify, - stringify_annotation, -) +from sphinx.util.typing import get_type_hints, restify, stringify_annotation if TYPE_CHECKING: from collections.abc import Callable, Iterator, Sequence @@ -49,7 +44,7 @@ from sphinx.environment import BuildEnvironment, _CurrentDocument from sphinx.events import EventManager from sphinx.ext.autodoc.directive import DocumenterBridge - from sphinx.util.typing import ExtensionMetadata, OptionSpec, _RestifyRenderMode + from sphinx.util.typing import ExtensionMetadata, OptionSpec, _RestifyMode _AutodocObjType = Literal[ 'module', 'class', 'exception', 'function', 'method', 'attribute' @@ -82,18 +77,11 @@ def _get_render_mode( - config: Config, - default: _RestifyRenderMode = RenderMode.fully_qualified_except_typing, -) -> _RestifyRenderMode: - if config.autodoc_typehints_format == 'short': - mode: _RestifyRenderMode = RenderMode.smart - else: - mode = default - - if config.python_display_short_literal_types: - mode |= RenderMode.short_literal - - return mode + typehints_format: Literal['fully-qualified', 'short'], +) -> _RestifyMode: + if typehints_format == 'short': + return 'smart' + return 'fully-qualified-except-typing' def identity(x: Any) -> Any: @@ -576,8 +564,8 @@ def format_signature(self, **kwargs: Any) -> str: retann = self.retann else: # try to introspect the signature - retann = None try: + retann = None args = self._call_format_args(**kwargs) if args: matched = re.match(r'^(\(.*\))\s+->\s+(.*)$', args) @@ -1493,7 +1481,7 @@ def format_args(self, **kwargs: Any) -> str: if self.config.autodoc_typehints_format == 'short': kwargs.setdefault('unqualified_typehints', True) if self.config.python_display_short_literal_types: - kwargs.setdefault('short_literal_types', True) + kwargs.setdefault('short_literals', True) try: self._events.emit('autodoc-before-process-signature', self.object, False) @@ -1530,7 +1518,7 @@ def format_signature(self, **kwargs: Any) -> str: if self.config.autodoc_typehints_format == 'short': kwargs.setdefault('unqualified_typehints', True) if self.config.python_display_short_literal_types: - kwargs.setdefault('short_literal_types', True) + kwargs.setdefault('short_literals', True) sigs = [] if ( @@ -1819,7 +1807,7 @@ def format_args(self, **kwargs: Any) -> str: if self.config.autodoc_typehints_format == 'short': kwargs.setdefault('unqualified_typehints', True) if self.config.python_display_short_literal_types: - kwargs.setdefault('short_literal_types', True) + kwargs.setdefault('short_literals', True) try: self._signature_class, _signature_method_name, sig = self._get_signature() @@ -1862,7 +1850,7 @@ def format_signature(self, **kwargs: Any) -> str: if self.config.autodoc_typehints_format == 'short': kwargs.setdefault('unqualified_typehints', True) if self.config.python_display_short_literal_types: - kwargs.setdefault('short_literal_types', True) + kwargs.setdefault('short_literals', True) sig = super().format_signature() sigs = [] @@ -1959,8 +1947,8 @@ def add_directive_header(self, sig: str) -> None: 'autodoc-process-bases', self.fullname, self.object, self.options, bases ) - mode = _get_render_mode(self.config) - base_classes = [restify(cls, mode) for cls in bases] + mode = _get_render_mode(self.config.autodoc_typehints_format) + base_classes = [restify(cls, mode=mode) for cls in bases] sourcename = self.get_sourcename() self.add_line('', sourcename) @@ -2073,19 +2061,21 @@ def get_variable_comment(self) -> list[str] | None: return None def add_content(self, more_content: StringList | None) -> None: - mode = _get_render_mode(self.config) + mode = _get_render_mode(self.config.autodoc_typehints_format) + short_literals = self.config.python_display_short_literal_types if isinstance(self.object, NewType): - supertype = restify(self.object.__supertype__, mode) + supertype = restify(self.object.__supertype__, mode=mode) more_content = StringList([_('alias of %s') % supertype, ''], source='') if isinstance(self.object, TypeVar): - attrs = [ - repr(self.object.__name__), - *(stringify_annotation(c, mode) for c in self.object.__constraints__), - ] + attrs = [repr(self.object.__name__)] + attrs.extend( + stringify_annotation(constraint, mode, short_literals=short_literals) + for constraint in self.object.__constraints__ + ) if self.object.__bound__: - bound = restify(self.object.__bound__, mode) + bound = restify(self.object.__bound__, mode=mode) attrs.append(r'bound=\ ' + bound) if self.object.__covariant__: attrs.append('covariant=True') @@ -2105,7 +2095,7 @@ def add_content(self, more_content: StringList | None) -> None: if self.doc_as_attr and not self.get_variable_comment(): try: - alias = restify(self.object, mode) + alias = restify(self.object, mode=mode) more_content = StringList([_('alias of %s') % alias], source='') except AttributeError: pass # Invalid class object is passed. @@ -2197,8 +2187,8 @@ def should_suppress_directive_header(self) -> bool: def update_content(self, more_content: StringList) -> None: if inspect.isgenericalias(self.object): - mode = _get_render_mode(self.config) - alias = restify(self.object, mode) + mode = _get_render_mode(self.config.autodoc_typehints_format) + alias = restify(self.object, mode=mode) more_content.append(_('alias of %s') % alias, '') more_content.append('', '') @@ -2322,19 +2312,13 @@ def add_directive_header(self, sig: str) -> None: include_extras=True, ) if self.objpath[-1] in annotations: - mode = _get_render_mode(self.config) + mode = _get_render_mode(self.config.autodoc_typehints_format) + short_literals = self.config.python_display_short_literal_types objrepr = stringify_annotation( - annotations.get(self.objpath[-1]), mode + annotations.get(self.objpath[-1]), + mode, + short_literals=short_literals, ) - if self.config.autodoc_typehints_format == 'short': - objrepr = stringify_annotation( - annotations.get(self.objpath[-1]), 'smart' - ) - else: - objrepr = stringify_annotation( - annotations.get(self.objpath[-1]), - 'fully-qualified-except-typing', - ) self.add_line(' :type: ' + objrepr, sourcename) try: @@ -2425,7 +2409,7 @@ def format_args(self, **kwargs: Any) -> str: if self.config.autodoc_typehints_format == 'short': kwargs.setdefault('unqualified_typehints', True) if self.config.python_display_short_literal_types: - kwargs.setdefault('short_literal_types', True) + kwargs.setdefault('short_literals', True) try: if self.object == object.__init__ and self.parent != object: # NoQA: E721 @@ -2496,7 +2480,7 @@ def format_signature(self, **kwargs: Any) -> str: if self.config.autodoc_typehints_format == 'short': kwargs.setdefault('unqualified_typehints', True) if self.config.python_display_short_literal_types: - kwargs.setdefault('short_literal_types', True) + kwargs.setdefault('short_literals', True) sigs = [] if ( @@ -2980,19 +2964,13 @@ def add_directive_header(self, sig: str) -> None: include_extras=True, ) if self.objpath[-1] in annotations: - mode = _get_render_mode(self.config) + mode = _get_render_mode(self.config.autodoc_typehints_format) + short_literals = self.config.python_display_short_literal_types objrepr = stringify_annotation( - annotations.get(self.objpath[-1]), mode + annotations.get(self.objpath[-1]), + mode, + short_literals=short_literals, ) - if self.config.autodoc_typehints_format == 'short': - objrepr = stringify_annotation( - annotations.get(self.objpath[-1]), 'smart' - ) - else: - objrepr = stringify_annotation( - annotations.get(self.objpath[-1]), - 'fully-qualified-except-typing', - ) self.add_line(' :type: ' + objrepr, sourcename) try: @@ -3031,7 +3009,6 @@ def get_doc(self) -> list[list[str]] | None: if comment: return [comment] - orig = self.config.autodoc_inherit_docstrings try: # Disable `autodoc_inherit_docstring` temporarily to avoid to obtain # a docstring from the value which descriptor returns unexpectedly. @@ -3128,14 +3105,11 @@ def add_directive_header(self, sig: str) -> None: func, type_aliases=self.config.autodoc_type_aliases ) if signature.return_annotation is not Parameter.empty: - mode = _get_render_mode(self.config) - objrepr = stringify_annotation(signature.return_annotation, mode) - if self.config.autodoc_typehints_format == 'short': - objrepr = stringify_annotation(signature.return_annotation, 'smart') - else: - objrepr = stringify_annotation( - signature.return_annotation, 'fully-qualified-except-typing' - ) + mode = _get_render_mode(self.config.autodoc_typehints_format) + short_literals = self.config.python_display_short_literal_types + objrepr = stringify_annotation( + signature.return_annotation, mode, short_literals=short_literals + ) self.add_line(' :type: ' + objrepr, sourcename) except TypeError as exc: logger.warning( diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index 0e044353d55..63403772137 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -10,7 +10,7 @@ import sphinx from sphinx import addnodes from sphinx.util import inspect -from sphinx.util.typing import ExtensionMetadata, RenderMode, stringify_annotation +from sphinx.util.typing import stringify_annotation if TYPE_CHECKING: from collections.abc import Iterable @@ -20,7 +20,7 @@ from sphinx.application import Sphinx from sphinx.ext.autodoc import Options - from sphinx.util.typing import ExtensionMetadata + from sphinx.util.typing import ExtensionMetadata, _StringifyMode def record_typehints( @@ -33,13 +33,13 @@ def record_typehints( retann: str, ) -> None: """Record type hints to env object.""" + mode: _StringifyMode if app.config.autodoc_typehints_format == 'short': - mode = RenderMode.smart + mode = 'smart' else: - mode = RenderMode.fully_qualified + mode = 'fully-qualified' - if app.config.python_display_short_literal_types: - mode |= RenderMode.short_literal + short_literals = app.config.python_display_short_literal_types try: if callable(obj): @@ -49,11 +49,12 @@ def record_typehints( for param in sig.parameters.values(): if param.annotation is not param.empty: annotation[param.name] = stringify_annotation( - param.annotation, - mode, # type: ignore[arg-type] + param.annotation, mode, short_literals=short_literals ) if sig.return_annotation is not sig.empty: - annotation['return'] = stringify_annotation(sig.return_annotation, mode) # type: ignore[arg-type] + annotation['return'] = stringify_annotation( + sig.return_annotation, mode, short_literals=short_literals + ) except (TypeError, ValueError): pass diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index cf0f25979eb..d1317e9d841 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -12,14 +12,13 @@ from sphinx.locale import _, __ from sphinx.util import logging -from sphinx.util.typing import RenderMode, get_type_hints, stringify_annotation +from sphinx.util.typing import get_type_hints, stringify_annotation if TYPE_CHECKING: from collections.abc import Callable, Iterator from sphinx.application import Sphinx from sphinx.config import Config as SphinxConfig - from sphinx.util.typing import _StringifyMode logger = logging.getLogger(__name__) @@ -1096,12 +1095,14 @@ def _lookup_annotation(self, _name: str) -> str: ) self._annotations = get_type_hints(self._obj, None, localns) if _name in self._annotations: - mode: _StringifyMode = RenderMode.fully_qualified_except_typing - if getattr( - self._config, 'python_display_short_literal_types', None - ): - mode = RenderMode.fully_qualified_except_typing_short_literal - return stringify_annotation(self._annotations[_name], mode) + short_literals = getattr( + self._config, 'python_display_short_literal_types', False + ) + return stringify_annotation( + self._annotations[_name], + mode='fully-qualified-except-typing', + short_literals=short_literals, + ) # No annotation found return '' diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 0cc6d0a03ba..d77f31161ec 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -21,7 +21,7 @@ from sphinx.pycode.ast import unparse as ast_unparse from sphinx.util import logging -from sphinx.util.typing import RenderMode, stringify_annotation +from sphinx.util.typing import stringify_annotation if TYPE_CHECKING: from collections.abc import Callable, Iterator, Sequence @@ -31,6 +31,8 @@ from typing_extensions import TypeIs + from sphinx.util.typing import _StringifyMode + class _SupportsGet(Protocol): def __get__(self, instance: Any, owner: type | None = ..., /) -> Any: ... @@ -842,7 +844,7 @@ def stringify_signature( show_annotation: bool = True, show_return_annotation: bool = True, unqualified_typehints: bool = False, - short_literal_types: bool = False, + short_literals: bool = False, ) -> str: """Stringify a :class:`~inspect.Signature` object. @@ -850,15 +852,13 @@ def stringify_signature( :param show_return_annotation: If enabled, show annotation of the return value :param unqualified_typehints: If enabled, show annotations as unqualified (ex. io.StringIO -> StringIO) - :param short_literal_types: If enabled, use short literal types. + :param short_literals: If enabled, use short literal types. """ + mode: _StringifyMode if unqualified_typehints: - mode = RenderMode.smart + mode = 'smart' else: - mode = RenderMode.fully_qualified - - if short_literal_types: - mode |= RenderMode.short_literal + mode = 'fully-qualified' EMPTY = Parameter.empty @@ -889,7 +889,11 @@ def stringify_signature( if show_annotation and param.annotation is not EMPTY: arg.write(': ') - arg.write(stringify_annotation(param.annotation, mode)) # type: ignore[arg-type] + arg.write( + stringify_annotation( + param.annotation, mode, short_literals=short_literals + ) + ) if param.default is not EMPTY: if show_annotation and param.annotation is not EMPTY: arg.write(' = ') @@ -912,7 +916,9 @@ def stringify_signature( ): return f'({concatenated_args})' else: - retann = stringify_annotation(sig.return_annotation, mode) # type: ignore[arg-type] + retann = stringify_annotation( + sig.return_annotation, mode, short_literals=short_literals + ) return f'({concatenated_args}) -> {retann}' diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 1cfb0e72229..594b050dfa6 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -3,19 +3,11 @@ from __future__ import annotations import dataclasses -import enum -import itertools -import operator import sys import types import typing from collections.abc import Callable, Sequence -from functools import reduce -from typing import ( - TYPE_CHECKING, - Annotated, - Any, -) +from typing import TYPE_CHECKING from docutils import nodes from docutils.parsers.rst.states import Inliner @@ -236,108 +228,25 @@ def _is_unpack_form(obj: Any) -> bool: return typing.get_origin(obj) is typing.Unpack -class RenderMode(enum.IntFlag, boundary=enum.STRICT): - """Additional flags for rendering annotations or reST content.""" - - # primary modes (mutually exclusive) - smart = enum.auto() - """Show the annotation name.""" - - fully_qualified = enum.auto() - """Show the module name and qualified name of the annotation. - - This mode is mutually exclusive with :attr:`smart` and :attr:`fully_qualified_except_typing`. - """ - - fully_qualified_except_typing = enum.auto() - """Same as :attr:`fully_qualified` but do not show module name for ``typing`` members. - - This mode is mutually exclusive with :attr:`smart` and :attr:`fully_qualified`. - """ - - # extra bit flags - short_literal = enum.auto() - """Use PEP 604 style to render literals.""" - - smart_short_literal = smart | short_literal - """Apply PEP 604 style to :attr:`smart`.""" - fully_qualified_short_literal = fully_qualified | short_literal - """Apply PEP 604 style to :attr:`fully_qualified`.""" - fully_qualified_except_typing_short_literal = ( - fully_qualified_except_typing | short_literal - ) - """Apply PEP 604 style to :attr:`fully_qualified_except_typing`.""" - - -if TYPE_CHECKING: - _RestifyRenderMode: TypeAlias = Literal[ - RenderMode.smart, - RenderMode.smart_short_literal, - RenderMode.fully_qualified_except_typing, - RenderMode.fully_qualified_except_typing_short_literal, - ] - - _RestifyMode: TypeAlias = Literal[ - 'smart', - 'fully-qualified-except-typing', - _RestifyRenderMode, - ] - - _StringifyRenderMode: TypeAlias = Literal[ - RenderMode.smart, - RenderMode.smart_short_literal, - RenderMode.fully_qualified, - RenderMode.fully_qualified_short_literal, - RenderMode.fully_qualified_except_typing, - RenderMode.fully_qualified_except_typing_short_literal, - ] - - _StringifyMode: TypeAlias = Literal[ - 'smart', - 'fully-qualified', - 'fully-qualified-except-typing', - _StringifyRenderMode, - ] - - -@typing.overload -def _normalize_mode( - mode: Literal['smart'], -) -> Literal[RenderMode.smart]: ... -@typing.overload -def _normalize_mode( - mode: Literal['fully-qualified'], -) -> Literal[RenderMode.fully_qualified]: ... -@typing.overload -def _normalize_mode( - mode: Literal['fully-qualified-except-typing'], -) -> Literal[RenderMode.fully_qualified_except_typing]: ... -def _normalize_mode(mode: str) -> RenderMode: - if mode == 'smart': - return RenderMode.smart - if mode == 'fully-qualified': - return RenderMode.fully_qualified - if mode == 'fully-qualified-except-typing': - return RenderMode.fully_qualified_except_typing - raise ValueError('invalid string mode name: %r' % mode) - - -def restify( - cls: Any, mode: _RestifyMode = RenderMode.fully_qualified_except_typing -) -> str: +def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> str: """Convert a type-like object to a reST reference. :param mode: Specify a method how annotations will be stringified. - The following values can be given as a shorthand of a rendering mode: - - * ``smart`` -- :attr:`RenderMode.smart`. - * ``fully-qualified-except-typing`` -- :attr:`RenderMode.fully_qualified_except_typing`. + 'fully-qualified-except-typing' + Show the module name and qualified name of the annotation except + the "typing" module. + 'smart' + Show the name of the annotation. """ from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading from sphinx.util.inspect import isgenericalias, object_description # lazy loading - mode = _normalize_restify_mode(mode) + valid_modes = {'fully-qualified-except-typing', 'smart'} + if mode not in valid_modes: + valid = ', '.join(map(repr, sorted(valid_modes))) + msg = f'mode must be one of {valid}; got {mode!r}' + raise ValueError(msg) # things that are not types if cls is None or cls == types.NoneType: @@ -352,7 +261,7 @@ def restify( # If the mode is 'smart', we always use '~'. # If the mode is 'fully-qualified-except-typing', # we use '~' only for the objects in the ``typing`` module. - module_prefix = '~' if mode & RenderMode.smart or cls_module_is_typing else '' + module_prefix = '~' if mode == 'smart' or cls_module_is_typing else '' try: if ismockmodule(cls): @@ -460,33 +369,12 @@ def restify( return object_description(cls) -def _normalize_restify_mode(mode: _RestifyMode) -> _RestifyRenderMode: - valid_modes = { - 'smart', - 'fully-qualified', - 'fully-qualified-except-typing', - RenderMode.smart, - RenderMode.smart_short_literal, - RenderMode.fully_qualified_except_typing, - RenderMode.fully_qualified_except_typing_short_literal, - } - - if mode not in valid_modes: - valid = ', '.join(sorted(map(repr, valid_modes))) - msg = f'mode must be one of {valid}; got {mode!r}' - raise ValueError(msg) - return mode if isinstance(mode, RenderMode) else _normalize_mode(mode) - - -def _format_literal_arg_restify(arg: Any, /, *, mode: RenderMode) -> str: +def _format_literal_arg_restify(arg: Any, /, *, mode: str) -> str: from sphinx.util.inspect import isenumattribute # lazy loading if isenumattribute(arg): enum_cls = arg.__class__ - # For now, ignore :confval:`python_display_short_literal_types` - # in restification since the latter is used for 'aliasing' and - # keeping 'Literal' is preferable for readability. - if mode & RenderMode.smart or enum_cls.__module__ == 'typing': + if mode == 'smart' or enum_cls.__module__ == 'typing': # MyEnum.member return ( f':py:attr:`~{enum_cls.__module__}.{enum_cls.__qualname__}.{arg.name}`' @@ -499,22 +387,32 @@ def _format_literal_arg_restify(arg: Any, /, *, mode: RenderMode) -> str: def stringify_annotation( annotation: Any, /, - mode: _StringifyMode = RenderMode.fully_qualified_except_typing, + mode: _StringifyMode = 'fully-qualified-except-typing', + *, + short_literals: bool = False, ) -> str: """Stringify type annotation object. :param annotation: The annotation to stringified. :param mode: Specify a method how annotations will be stringified. - The following values can be given as a shorthand of a rendering mode: + 'fully-qualified-except-typing' + Show the module name and qualified name of the annotation except + the "typing" module. + 'smart' + Show the name of the annotation. + 'fully-qualified' + Show the module name and qualified name of the annotation. - * ``smart`` -- :attr:`RenderMode.smart`. - * ``fully-qualified`` -- :attr:`RenderMode.fully_qualified`. - * ``fully-qualified-except-typing`` -- :attr:`RenderMode.fully_qualified_except_typing`. + :param short_literals: Render :py:class:`Literals` in PEP 604 style (``|``). """ from sphinx.ext.autodoc.mock import ismock, ismockmodule # lazy loading - mode = _normalize_stringify_mode(mode) + valid_modes = {'fully-qualified-except-typing', 'fully-qualified', 'smart'} + if mode not in valid_modes: + valid = ', '.join(map(repr, sorted(valid_modes))) + msg = f'mode must be one of {valid}; got {mode!r}' + raise ValueError(msg) # things that are not types if annotation is None or annotation == types.NoneType: @@ -529,7 +427,7 @@ def stringify_annotation( if not annotation: return repr(annotation) - module_prefix = '~' if mode & RenderMode.smart else '' + module_prefix = '~' if mode == 'smart' else '' # The values below must be strings if the objects are well-formed. annotation_qualname: str = getattr(annotation, '__qualname__', '') @@ -543,9 +441,10 @@ def stringify_annotation( # Extract the annotation's base type by considering formattable cases if isinstance(annotation, typing.TypeVar) and not _is_unpack_form(annotation): # typing_extensions.Unpack is incorrectly determined as a TypeVar - if annotation_module_is_typing and mode & ( - RenderMode.smart | RenderMode.fully_qualified_except_typing - ): + if annotation_module_is_typing and mode in { + 'fully-qualified-except-typing', + 'smart', + }: return annotation_name return module_prefix + f'{annotation_module}.{annotation_name}' elif isinstance(annotation, typing.NewType): @@ -567,7 +466,10 @@ def stringify_annotation( if not args: # Empty tuple, list, ... return repr(annotation) - concatenated_args = ', '.join(stringify_annotation(arg, mode) for arg in args) + concatenated_args = ', '.join( + stringify_annotation(arg, mode=mode, short_literals=short_literals) + for arg in args + ) return f'{annotation_qualname}[{concatenated_args}]' else: # add other special cases that can be directly formatted @@ -578,15 +480,12 @@ def stringify_annotation( if annotation_qualname or ( annotation_module_is_typing and not annotation_forward_arg ): - if mode & RenderMode.smart: + if mode == 'smart': module_prefix = f'~{module_prefix}' - if ( - annotation_module_is_typing - and mode & RenderMode.fully_qualified_except_typing - ): + if annotation_module_is_typing and mode == 'fully-qualified-except-typing': module_prefix = '' elif _is_unpack_form(annotation) and annotation_module == 'typing_extensions': - module_prefix = '~' if mode & RenderMode.smart else '' + module_prefix = '~' if mode == 'smart' else '' else: module_prefix = '' @@ -604,13 +503,16 @@ def stringify_annotation( # of ``typing`` and all of them define ``__origin__`` qualname = stringify_annotation( annotation.__origin__, - RenderMode.fully_qualified_except_typing, + mode='fully-qualified-except-typing', + short_literals=short_literals, ).replace('typing.', '') # ex. Union elif annotation_qualname: qualname = annotation_qualname elif hasattr(annotation, '__origin__'): # instantiated generic provided by a user - qualname = stringify_annotation(annotation.__origin__, mode) + qualname = stringify_annotation( + annotation.__origin__, mode=mode, short_literals=short_literals + ) elif isinstance(annotation, types.UnionType): qualname = 'types.UnionType' else: @@ -633,41 +535,55 @@ def stringify_annotation( ) return f'{module_prefix}Literal[{args}]' if qualname in {'Optional', 'Union', 'types.UnionType'}: - return ' | '.join(stringify_annotation(a, mode) for a in annotation_args) + return ' | '.join( + stringify_annotation(a, mode=mode, short_literals=short_literals) + for a in annotation_args + ) elif qualname == 'Callable': args = ', '.join( - stringify_annotation(a, mode) for a in annotation_args[:-1] + stringify_annotation(a, mode=mode, short_literals=short_literals) + for a in annotation_args[:-1] + ) + returns = stringify_annotation( + annotation_args[-1], mode=mode, short_literals=short_literals ) - returns = stringify_annotation(annotation_args[-1], mode) return f'{module_prefix}Callable[[{args}], {returns}]' elif qualname == 'Literal': - args = ( + if short_literals: + return ' | '.join( + _format_literal_arg_stringify(a, mode=mode) for a in annotation_args + ) + args = ', '.join( _format_literal_arg_stringify(a, mode=mode) for a in annotation_args ) - if mode & RenderMode.short_literal: - return ' | '.join(args) - return f'{module_prefix}Literal[{", ".join(args)}]' + return f'{module_prefix}Literal[{args}]' elif _is_annotated_form(annotation): # for py310+ - args = stringify_annotation(annotation_args[0], mode) + args = stringify_annotation( + annotation_args[0], mode=mode, short_literals=short_literals + ) meta_args = [] for m in annotation.__metadata__: if isinstance(m, type): - meta_args.append(stringify_annotation(m, mode)) + meta_args.append( + stringify_annotation( + m, mode=mode, short_literals=short_literals + ) + ) elif dataclasses.is_dataclass(m): # use stringify_annotation for the repr of field values rather than repr d_fields = ', '.join([ - f'{f.name}={stringify_annotation(getattr(m, f.name), mode)}' + f'{f.name}={stringify_annotation(getattr(m, f.name), mode=mode, short_literals=short_literals)}' # NoQA: E501 for f in dataclasses.fields(m) if f.repr ]) meta_args.append( - f'{stringify_annotation(type(m), mode)}({d_fields})' + f'{stringify_annotation(type(m), mode=mode, short_literals=short_literals)}({d_fields})' # NoQA: E501 ) else: meta_args.append(repr(m)) meta = ', '.join(meta_args) if sys.version_info[:2] <= (3, 11): - if mode & RenderMode.fully_qualified_except_typing: + if mode == 'fully-qualified-except-typing': return f'Annotated[{args}, {meta}]' module_prefix = module_prefix.replace('builtins', 'typing') return f'{module_prefix}Annotated[{args}, {meta}]' @@ -676,38 +592,21 @@ def stringify_annotation( # Suppress arguments if all system defined TypeVars (ex. Dict[KT, VT]) return module_prefix + qualname else: - args = ', '.join(stringify_annotation(a, mode) for a in annotation_args) + args = ', '.join( + stringify_annotation(a, mode=mode, short_literals=short_literals) + for a in annotation_args + ) return f'{module_prefix}{qualname}[{args}]' return module_prefix + qualname -def _normalize_stringify_mode(mode: _StringifyMode) -> _StringifyRenderMode: - valid_modes = { - 'smart', - 'fully-qualified', - 'fully-qualified-except-typing', - RenderMode.smart, - RenderMode.smart_short_literal, - RenderMode.fully_qualified, - RenderMode.fully_qualified_short_literal, - RenderMode.fully_qualified_except_typing, - RenderMode.fully_qualified_except_typing_short_literal, - } - - if mode not in valid_modes: - valid = ', '.join(sorted(map(repr, valid_modes))) - msg = f'mode must be one of {valid}; got {mode!r}' - raise ValueError(msg) - return mode if isinstance(mode, RenderMode) else _normalize_mode(mode) - - -def _format_literal_arg_stringify(arg: Any, /, *, mode: RenderMode) -> str: +def _format_literal_arg_stringify(arg: Any, /, *, mode: str) -> str: from sphinx.util.inspect import isenumattribute # lazy loading if isenumattribute(arg): enum_cls = arg.__class__ - if mode & RenderMode.smart or enum_cls.__module__ == 'typing': + if mode == 'smart' or enum_cls.__module__ == 'typing': # MyEnum.member return f'{enum_cls.__qualname__}.{arg.name}' # module.MyEnum.member diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index b078d8a4872..b1ba884033c 100644 --- a/tests/test_extensions/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -3199,7 +3199,7 @@ def test_literal_render(app): freshenv=True, confoverrides={'python_display_short_literal_types': True}, ) -def test_literal_render_pep604(self, app): +def test_literal_render_pep604(app): options = { 'members': None, 'exclude-members': 'MyEnum',