Skip to content

Autodoc: Support typing_extensions.overload #13509

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Features added
* #13439: linkcheck: Permit warning on every redirect with
``linkcheck_allowed_redirects = {}``.
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
45 changes: 21 additions & 24 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,12 +295,9 @@ 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 = self.typing_final_names | {
f'{modname}.final' for modname in self.typing_mods
}
for decorator in decorators:
try:
if ast_unparse(decorator) in final:
Expand All @@ -311,11 +308,9 @@ 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 = self.typing_overload_names | {
f'{modname}.overload' for modname in self.typing_mods
}

for decorator in decorators:
try:
Expand Down Expand Up @@ -348,22 +343,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 @@ -2819,6 +2819,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 @@ -2892,6 +2906,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