Skip to content

Commit

Permalink
Support callables in Annotated types (#12625)
Browse files Browse the repository at this point in the history
  • Loading branch information
AA-Turner committed Jul 20, 2024
1 parent 1ed4ca7 commit dd77f85
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 3 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
=====================================
Expand Down
24 changes: 23 additions & 1 deletion sphinx/domains/python/_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 38 additions & 2 deletions tests/test_domains/test_domain_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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, '*'],
Expand Down Expand Up @@ -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}()'
Expand Down

0 comments on commit dd77f85

Please sign in to comment.