From dd77f851494d24d19aecf0328c6913d121b8b51c Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Sat, 20 Jul 2024 11:16:33 +0100 Subject: [PATCH] Support callables in ``Annotated`` types (#12625) --- CHANGES.rst | 3 ++ sphinx/domains/python/_annotations.py | 24 +++++++++++++++- tests/test_domains/test_domain_py.py | 40 +++++++++++++++++++++++++-- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1e7d811b48f..c53bb8f8b0f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,9 @@ Bugs fixed Patch by Adam Turner. * #12620: Ensure that old-style object description options are respected. Patch by Adam Turner. +* #12601, #12625: Support callable objects in :py:class:`~typing.Annotated` type + metadata in the Python domain. + Patch by Adam Turner. Release 7.4.6 (released Jul 18, 2024) ===================================== diff --git a/sphinx/domains/python/_annotations.py b/sphinx/domains/python/_annotations.py index 5d4803cfb60..35525f6b1b3 100644 --- a/sphinx/domains/python/_annotations.py +++ b/sphinx/domains/python/_annotations.py @@ -161,7 +161,29 @@ def unparse(node: ast.AST) -> list[Node]: addnodes.desc_sig_punctuation('', ')')] return result - raise SyntaxError # unsupported syntax + if isinstance(node, ast.Call): + # Call nodes can be used in Annotated type metadata, + # for example Annotated[str, ArbitraryTypeValidator(str, len=10)] + args = [] + for arg in node.args: + args += unparse(arg) + args.append(addnodes.desc_sig_punctuation('', ',')) + args.append(addnodes.desc_sig_space()) + for kwd in node.keywords: + args.append(addnodes.desc_sig_name(kwd.arg, kwd.arg)) # type: ignore[arg-type] + args.append(addnodes.desc_sig_operator('', '=')) + args += unparse(kwd.value) + args.append(addnodes.desc_sig_punctuation('', ',')) + args.append(addnodes.desc_sig_space()) + result = [ + *unparse(node.func), + addnodes.desc_sig_punctuation('', '('), + *args[:-2], # skip the final comma and space + addnodes.desc_sig_punctuation('', ')'), + ] + return result + msg = f'unsupported syntax: {node}' + raise SyntaxError(msg) # unsupported syntax def _unparse_pep_604_annotation(node: ast.Subscript) -> list[Node]: subscript = node.slice diff --git a/tests/test_domains/test_domain_py.py b/tests/test_domains/test_domain_py.py index 08390b7113e..ce3d44465eb 100644 --- a/tests/test_domains/test_domain_py.py +++ b/tests/test_domains/test_domain_py.py @@ -370,6 +370,27 @@ def test_parse_annotation(app): [desc_sig_punctuation, "]"])) assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="typing.Literal") + # Annotated type with callable gets parsed + doctree = _parse_annotation("Annotated[Optional[str], annotated_types.MaxLen(max_length=10)]", app.env) + assert_node(doctree, ( + [pending_xref, 'Annotated'], + [desc_sig_punctuation, '['], + [pending_xref, 'str'], + [desc_sig_space, ' '], + [desc_sig_punctuation, '|'], + [desc_sig_space, ' '], + [pending_xref, 'None'], + [desc_sig_punctuation, ','], + [desc_sig_space, ' '], + [pending_xref, 'annotated_types.MaxLen'], + [desc_sig_punctuation, '('], + [desc_sig_name, 'max_length'], + [desc_sig_operator, '='], + [desc_sig_literal_number, '10'], + [desc_sig_punctuation, ')'], + [desc_sig_punctuation, ']'], + )) + def test_parse_annotation_suppress(app): doctree = _parse_annotation("~typing.Dict[str, str]", app.env) @@ -802,7 +823,22 @@ def test_function_pep_695(app): [desc_sig_name, 'A'], [desc_sig_punctuation, ':'], desc_sig_space, - [desc_sig_name, ([pending_xref, 'int | Annotated[int, ctype("char")]'])], + [desc_sig_name, ( + [pending_xref, 'int'], + [desc_sig_space, ' '], + [desc_sig_punctuation, '|'], + [desc_sig_space, ' '], + [pending_xref, 'Annotated'], + [desc_sig_punctuation, '['], + [pending_xref, 'int'], + [desc_sig_punctuation, ','], + [desc_sig_space, ' '], + [pending_xref, 'ctype'], + [desc_sig_punctuation, '('], + [desc_sig_literal_string, "'char'"], + [desc_sig_punctuation, ')'], + [desc_sig_punctuation, ']'], + )], )], [desc_type_parameter, ( [desc_sig_operator, '*'], @@ -987,7 +1023,7 @@ def test_class_def_pep_696(app): ('[T:(*Ts)|int]', '[T: (*Ts) | int]'), ('[T:(int|(*Ts))]', '[T: (int | (*Ts))]'), ('[T:((*Ts)|int)]', '[T: ((*Ts) | int)]'), - ('[T:Annotated[int,ctype("char")]]', '[T: Annotated[int, ctype("char")]]'), + ("[T:Annotated[int,ctype('char')]]", "[T: Annotated[int, ctype('char')]]"), ]) def test_pep_695_and_pep_696_whitespaces_in_bound(app, tp_list, tptext): text = f'.. py:function:: f{tp_list}()'