Skip to content
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ Features added
* #13497: Support C domain objects in the table of contents.
* #13535: html search: Update to the latest version of Snowball (v3.0.1).
Patch by Adam Turner.
* #13704: autodoc: Detect :py:func:`typing_extensions.overload <typing.overload>`
and :py:func:`~typing.final` decorators.
Patch by Spencer Brown.

Bugs fixed
----------
Expand Down
42 changes: 19 additions & 23 deletions sphinx/pycode/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,9 @@ def __init__(self, buffers: list[str], encoding: str) -> None:
self.deforders: dict[str, int] = {}
self.finals: list[str] = []
self.overloads: dict[str, list[Signature]] = {}
self.typing: str | None = None
self.typing_final: str | None = None
self.typing_overload: str | None = None
self.typing_mods: set[str] = set()
self.typing_final_names: set[str] = set()
self.typing_overload_names: set[str] = set()
super().__init__()

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

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

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

def is_overload(self, decorators: list[ast.expr]) -> bool:
overload = []
if self.typing:
overload.append('%s.overload' % self.typing)
if self.typing_overload:
overload.append(self.typing_overload)
overload = {f'{modname}.overload' for modname in self.typing_mods}
overload |= self.typing_overload_names

for decorator in decorators:
try:
Expand Down Expand Up @@ -348,22 +342,24 @@ def visit_Import(self, node: ast.Import) -> None:
for name in node.names:
self.add_entry(name.asname or name.name)

if name.name == 'typing':
self.typing = name.asname or name.name
elif name.name == 'typing.final':
self.typing_final = name.asname or name.name
elif name.name == 'typing.overload':
self.typing_overload = name.asname or name.name
if name.name in {'typing', 'typing_extensions'}:
self.typing_mods.add(name.asname or name.name)
elif name.name in {'typing.final', 'typing_extensions.final'}:
self.typing_final_names.add(name.asname or name.name)
elif name.name in {'typing.overload', 'typing_extensions.overload'}:
self.typing_overload_names.add(name.asname or name.name)

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

if node.module == 'typing' and name.name == 'final':
self.typing_final = name.asname or name.name
elif node.module == 'typing' and name.name == 'overload':
self.typing_overload = name.asname or name.name
if node.module not in {'typing', 'typing_extensions'}:
continue
if name.name == 'final':
self.typing_final_names.add(name.asname or name.name)
elif name.name == 'overload':
self.typing_overload_names.add(name.asname or name.name)

def visit_Assign(self, node: ast.Assign) -> None:
"""Handles Assign node and pick up a variable comment."""
Expand Down
11 changes: 11 additions & 0 deletions tests/roots/test-ext-autodoc/target/final.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import typing
from typing import final

import typing_extensions
from typing_extensions import final as final_ext # noqa: UP035


@typing.final
class Class:
Expand All @@ -14,3 +17,11 @@ def meth1(self):

def meth2(self):
"""docstring"""

@final_ext
def meth3(self):
"""docstring"""

@typing_extensions.final
def meth4(self):
"""docstring"""
18 changes: 18 additions & 0 deletions tests/roots/test-ext-autodoc/target/overload3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import typing
from typing import TYPE_CHECKING, overload

import typing_extensions
from typing_extensions import overload as over_ext # noqa: UP035


@overload
def test(x: int) -> int: ...
@typing.overload
def test(x: list[int]) -> list[int]: ...
@over_ext
def test(x: str) -> str: ...
@typing_extensions.overload
def test(x: float) -> float: ...
def test(x):
"""Documentation."""
return x
34 changes: 34 additions & 0 deletions tests/test_extensions/test_ext_autodoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2823,6 +2823,20 @@ def test_final(app):
'',
' docstring',
'',
'',
' .. py:method:: Class.meth3()',
' :module: target.final',
' :final:',
'',
' docstring',
'',
'',
' .. py:method:: Class.meth4()',
' :module: target.final',
' :final:',
'',
' docstring',
'',
]


Expand Down Expand Up @@ -2896,6 +2910,26 @@ def test_overload2(app):
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_overload3(app):
options = {'members': None}
actual = do_autodoc(app, 'module', 'target.overload3', options)
assert list(actual) == [
'',
'.. py:module:: target.overload3',
'',
'',
'.. py:function:: test(x: int) -> int',
' test(x: list[int]) -> list[int]',
' test(x: str) -> str',
' test(x: float) -> float',
' :module: target.overload3',
'',
' Documentation.',
'',
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_pymodule_for_ModuleLevelDocumenter(app):
app.env.ref_context['py:module'] = 'target.classes'
Expand Down
Loading