Skip to content

Commit d71d399

Browse files
autodoc: Support typing_extensions.{final,overload} (#13509)
Co-authored-by: Adam Turner <[email protected]>
1 parent fb628cc commit d71d399

File tree

5 files changed

+85
-23
lines changed

5 files changed

+85
-23
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ Features added
2222
* #13497: Support C domain objects in the table of contents.
2323
* #13535: html search: Update to the latest version of Snowball (v3.0.1).
2424
Patch by Adam Turner.
25+
* #13704: autodoc: Detect :py:func:`typing_extensions.overload <typing.overload>`
26+
and :py:func:`~typing.final` decorators.
27+
Patch by Spencer Brown.
2528

2629
Bugs fixed
2730
----------

sphinx/pycode/parser.py

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,9 @@ def __init__(self, buffers: list[str], encoding: str) -> None:
247247
self.deforders: dict[str, int] = {}
248248
self.finals: list[str] = []
249249
self.overloads: dict[str, list[Signature]] = {}
250-
self.typing: str | None = None
251-
self.typing_final: str | None = None
252-
self.typing_overload: str | None = None
250+
self.typing_mods: set[str] = set()
251+
self.typing_final_names: set[str] = set()
252+
self.typing_overload_names: set[str] = set()
253253
super().__init__()
254254

255255
def get_qualname_for(self, name: str) -> list[str] | None:
@@ -295,11 +295,8 @@ def add_variable_annotation(self, name: str, annotation: ast.AST) -> None:
295295
self.annotations[basename, name] = ast_unparse(annotation)
296296

297297
def is_final(self, decorators: list[ast.expr]) -> bool:
298-
final = []
299-
if self.typing:
300-
final.append('%s.final' % self.typing)
301-
if self.typing_final:
302-
final.append(self.typing_final)
298+
final = {f'{modname}.final' for modname in self.typing_mods}
299+
final |= self.typing_final_names
303300

304301
for decorator in decorators:
305302
try:
@@ -311,11 +308,8 @@ def is_final(self, decorators: list[ast.expr]) -> bool:
311308
return False
312309

313310
def is_overload(self, decorators: list[ast.expr]) -> bool:
314-
overload = []
315-
if self.typing:
316-
overload.append('%s.overload' % self.typing)
317-
if self.typing_overload:
318-
overload.append(self.typing_overload)
311+
overload = {f'{modname}.overload' for modname in self.typing_mods}
312+
overload |= self.typing_overload_names
319313

320314
for decorator in decorators:
321315
try:
@@ -348,22 +342,24 @@ def visit_Import(self, node: ast.Import) -> None:
348342
for name in node.names:
349343
self.add_entry(name.asname or name.name)
350344

351-
if name.name == 'typing':
352-
self.typing = name.asname or name.name
353-
elif name.name == 'typing.final':
354-
self.typing_final = name.asname or name.name
355-
elif name.name == 'typing.overload':
356-
self.typing_overload = name.asname or name.name
345+
if name.name in {'typing', 'typing_extensions'}:
346+
self.typing_mods.add(name.asname or name.name)
347+
elif name.name in {'typing.final', 'typing_extensions.final'}:
348+
self.typing_final_names.add(name.asname or name.name)
349+
elif name.name in {'typing.overload', 'typing_extensions.overload'}:
350+
self.typing_overload_names.add(name.asname or name.name)
357351

358352
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
359353
"""Handles Import node and record the order of definitions."""
360354
for name in node.names:
361355
self.add_entry(name.asname or name.name)
362356

363-
if node.module == 'typing' and name.name == 'final':
364-
self.typing_final = name.asname or name.name
365-
elif node.module == 'typing' and name.name == 'overload':
366-
self.typing_overload = name.asname or name.name
357+
if node.module not in {'typing', 'typing_extensions'}:
358+
continue
359+
if name.name == 'final':
360+
self.typing_final_names.add(name.asname or name.name)
361+
elif name.name == 'overload':
362+
self.typing_overload_names.add(name.asname or name.name)
367363

368364
def visit_Assign(self, node: ast.Assign) -> None:
369365
"""Handles Assign node and pick up a variable comment."""

tests/roots/test-ext-autodoc/target/final.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import typing
44
from typing import final
55

6+
import typing_extensions
7+
from typing_extensions import final as final_ext # noqa: UP035
8+
69

710
@typing.final
811
class Class:
@@ -14,3 +17,11 @@ def meth1(self):
1417

1518
def meth2(self):
1619
"""docstring"""
20+
21+
@final_ext
22+
def meth3(self):
23+
"""docstring"""
24+
25+
@typing_extensions.final
26+
def meth4(self):
27+
"""docstring"""
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import typing
2+
from typing import TYPE_CHECKING, overload
3+
4+
import typing_extensions
5+
from typing_extensions import overload as over_ext # noqa: UP035
6+
7+
8+
@overload
9+
def test(x: int) -> int: ...
10+
@typing.overload
11+
def test(x: list[int]) -> list[int]: ...
12+
@over_ext
13+
def test(x: str) -> str: ...
14+
@typing_extensions.overload
15+
def test(x: float) -> float: ...
16+
def test(x):
17+
"""Documentation."""
18+
return x

tests/test_extensions/test_ext_autodoc.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2823,6 +2823,20 @@ def test_final(app):
28232823
'',
28242824
' docstring',
28252825
'',
2826+
'',
2827+
' .. py:method:: Class.meth3()',
2828+
' :module: target.final',
2829+
' :final:',
2830+
'',
2831+
' docstring',
2832+
'',
2833+
'',
2834+
' .. py:method:: Class.meth4()',
2835+
' :module: target.final',
2836+
' :final:',
2837+
'',
2838+
' docstring',
2839+
'',
28262840
]
28272841

28282842

@@ -2896,6 +2910,26 @@ def test_overload2(app):
28962910
]
28972911

28982912

2913+
@pytest.mark.sphinx('html', testroot='ext-autodoc')
2914+
def test_overload3(app):
2915+
options = {'members': None}
2916+
actual = do_autodoc(app, 'module', 'target.overload3', options)
2917+
assert list(actual) == [
2918+
'',
2919+
'.. py:module:: target.overload3',
2920+
'',
2921+
'',
2922+
'.. py:function:: test(x: int) -> int',
2923+
' test(x: list[int]) -> list[int]',
2924+
' test(x: str) -> str',
2925+
' test(x: float) -> float',
2926+
' :module: target.overload3',
2927+
'',
2928+
' Documentation.',
2929+
'',
2930+
]
2931+
2932+
28992933
@pytest.mark.sphinx('html', testroot='ext-autodoc')
29002934
def test_pymodule_for_ModuleLevelDocumenter(app):
29012935
app.env.ref_context['py:module'] = 'target.classes'

0 commit comments

Comments
 (0)