Skip to content

Commit 2364f16

Browse files
authored
Fix parsing of PEP 695 functions (#13328)
1 parent 37b7b54 commit 2364f16

File tree

5 files changed

+57
-16
lines changed

5 files changed

+57
-16
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ Bugs fixed
153153
* #13302, #13319: Use the correct indentation for continuation lines
154154
in :rst:dir:`productionlist` directives.
155155
Patch by Adam Turner.
156+
* #13328: Fix parsing of PEP 695 functions with return annotations.
157+
Patch by Bénédikt Tran. Initial work by Arash Badie-Modiri.
156158

157159
Testing
158160
-------

sphinx/domains/python/_object.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
py_sig_re = re.compile(
4242
r"""^ ([\w.]*\.)? # class name(s)
4343
(\w+) \s* # thing name
44-
(?: \[\s*(.*)\s*])? # optional: type parameters list
44+
(?: \[\s*(.*?)\s*])? # optional: type parameters list
4545
(?: \(\s*(.*)\s*\) # optional: arguments
4646
(?:\s* -> \s* (.*))? # return annotation
4747
)? $ # and nothing more

sphinx/ext/autodoc/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
r"""^ ([\w.]+::)? # explicit module name
6767
([\w.]+\.)? # module and/or class name(s)
6868
(\w+) \s* # thing name
69-
(?: \[\s*(.*)\s*])? # optional: type parameters list
69+
(?: \[\s*(.*?)\s*])? # optional: type parameters list
7070
(?: \((.*)\) # optional: arguments
7171
(?:\s* -> \s* (.*))? # return annotation
7272
)? $ # and nothing more

tests/test_domains/test_domain_py.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,28 @@ def parse(sig):
5050

5151

5252
def test_function_signatures():
53-
rv = parse('func(a=1) -> int object')
54-
assert rv == '(a=1)'
55-
56-
rv = parse('func(a=1, [b=None])')
57-
assert rv == '(a=1, [b=None])'
58-
59-
rv = parse('func(a=1[, b=None])')
60-
assert rv == '(a=1, [b=None])'
61-
6253
rv = parse("compile(source : string, filename, symbol='file')")
6354
assert rv == "(source : string, filename, symbol='file')"
6455

65-
rv = parse('func(a=[], [b=None])')
66-
assert rv == '(a=[], [b=None])'
67-
68-
rv = parse('func(a=[][, b=None])')
69-
assert rv == '(a=[], [b=None])'
56+
for params, expect in [
57+
('(a=1)', '(a=1)'),
58+
('(a: int = 1)', '(a: int = 1)'),
59+
('(a=1, [b=None])', '(a=1, [b=None])'),
60+
('(a=1[, b=None])', '(a=1, [b=None])'),
61+
('(a=[], [b=None])', '(a=[], [b=None])'),
62+
('(a=[][, b=None])', '(a=[], [b=None])'),
63+
('(a: Foo[Bar]=[][, b=None])', '(a: Foo[Bar]=[], [b=None])'),
64+
]:
65+
rv = parse(f'func{params}')
66+
assert rv == expect
67+
68+
# Note: 'def f[Foo[Bar]]()' is not valid Python but people might write
69+
# it in a reST document to convene the intent of a higher-kinded type
70+
# variable.
71+
for tparams in ['', '[Foo]', '[Foo[Bar]]']:
72+
for retann in ['', '-> Foo', '-> Foo[Bar]', '-> anything else']:
73+
rv = parse(f'func{tparams}{params} {retann}'.rstrip())
74+
assert rv == expect
7075

7176

7277
@pytest.mark.sphinx('dummy', testroot='domain-py')
@@ -1710,6 +1715,10 @@ def test_pep_695_and_pep_696_whitespaces_in_bound(app, tp_list, tptext):
17101715
doctree = restructuredtext.parse(app, text)
17111716
assert doctree.astext() == f'\n\nf{tptext}()\n\n'
17121717

1718+
text = f'.. py:function:: f{tp_list}() -> Annotated[T, Qux[int]()]'
1719+
doctree = restructuredtext.parse(app, text)
1720+
assert doctree.astext() == f'\n\nf{tptext}() -> Annotated[T, Qux[int]()]\n\n'
1721+
17131722

17141723
@pytest.mark.parametrize(
17151724
('tp_list', 'tptext'),
@@ -1724,6 +1733,10 @@ def test_pep_695_and_pep_696_whitespaces_in_constraints(app, tp_list, tptext):
17241733
doctree = restructuredtext.parse(app, text)
17251734
assert doctree.astext() == f'\n\nf{tptext}()\n\n'
17261735

1736+
text = f'.. py:function:: f{tp_list}() -> Annotated[T, Qux[int]()]'
1737+
doctree = restructuredtext.parse(app, text)
1738+
assert doctree.astext() == f'\n\nf{tptext}() -> Annotated[T, Qux[int]()]\n\n'
1739+
17271740

17281741
@pytest.mark.parametrize(
17291742
('tp_list', 'tptext'),
@@ -1747,3 +1760,7 @@ def test_pep_695_and_pep_696_whitespaces_in_default(app, tp_list, tptext):
17471760
text = f'.. py:function:: f{tp_list}()'
17481761
doctree = restructuredtext.parse(app, text)
17491762
assert doctree.astext() == f'\n\nf{tptext}()\n\n'
1763+
1764+
text = f'.. py:function:: f{tp_list}() -> Annotated[T, Qux[int]()]'
1765+
doctree = restructuredtext.parse(app, text)
1766+
assert doctree.astext() == f'\n\nf{tptext}() -> Annotated[T, Qux[int]()]\n\n'

tests/test_extensions/test_ext_autodoc.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,28 @@ def g(a='\n'):
177177
assert formatsig('function', 'f', f, 'a, b, c, d', None) == '(a, b, c, d)'
178178
assert formatsig('function', 'g', g, None, None) == r"(a='\n')"
179179

180+
if sys.version_info >= (3, 12):
181+
for params, expect in [
182+
('(a=1)', '(a=1)'),
183+
('(a: int=1)', '(a: int = 1)'), # auto whitespace formatting
184+
('(a:list[T] =[], b=None)', '(a: list[T] = [], b=None)'), # idem
185+
]:
186+
ns = {}
187+
exec(f'def f[T]{params}: pass', ns) # NoQA: S102
188+
f = ns['f']
189+
assert formatsig('function', 'f', f, None, None) == expect
190+
assert formatsig('function', 'f', f, '...', None) == '(...)'
191+
assert formatsig('function', 'f', f, '...', '...') == '(...) -> ...'
192+
193+
exec(f'def f[T]{params} -> list[T]: return []', ns) # NoQA: S102
194+
f = ns['f']
195+
assert formatsig('function', 'f', f, None, None) == f'{expect} -> list[T]'
196+
assert formatsig('function', 'f', f, '...', None) == '(...)'
197+
assert formatsig('function', 'f', f, '...', '...') == '(...) -> ...'
198+
199+
# TODO(picnixz): add more test cases for PEP-695 classes as well (though
200+
# complex cases are less likely to appear and are painful to test).
201+
180202
# test for classes
181203
class D:
182204
pass

0 commit comments

Comments
 (0)