From 747b3f6bf8784ac85633eeeb93dade9c53ea5a7b Mon Sep 17 00:00:00 2001 From: Spencer Brown Date: Sun, 27 Apr 2025 18:06:00 +1000 Subject: [PATCH 1/9] Test signature extraction when using typing_extensions.overload --- .../test-ext-autodoc/target/overload3.py | 17 ++++++++++++++++ tests/test_extensions/test_ext_autodoc.py | 20 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 tests/roots/test-ext-autodoc/target/overload3.py diff --git a/tests/roots/test-ext-autodoc/target/overload3.py b/tests/roots/test-ext-autodoc/target/overload3.py new file mode 100644 index 00000000000..69d0cbb930f --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/overload3.py @@ -0,0 +1,17 @@ +from typing import TYPE_CHECKING, overload +import typing + +from typing_extensions import overload as over_ext +import typing_extensions + +@overload +def test(x: int) -> int: ... +@typing.overload +def test(x: bool) -> bool: ... +@over_ext +def test(x: str) -> str: ... +@typing_extensions.overload +def test(x: float) -> float: ... +def test(x): + """Documentation.""" + return x diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index a06c1bbe30d..3ff071b6310 100644 --- a/tests/test_extensions/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -2892,6 +2892,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: bool) -> bool', + ' 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' From ee2e922958f202e96ad06aeccb4228d0c570ca69 Mon Sep 17 00:00:00 2001 From: Spencer Brown Date: Sun, 27 Apr 2025 18:26:54 +1000 Subject: [PATCH 2/9] Test handling of typing_extensions.final --- tests/roots/test-ext-autodoc/target/final.py | 10 ++++++++++ tests/test_extensions/test_ext_autodoc.py | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/tests/roots/test-ext-autodoc/target/final.py b/tests/roots/test-ext-autodoc/target/final.py index a8c3860e384..ffa888c94f8 100644 --- a/tests/roots/test-ext-autodoc/target/final.py +++ b/tests/roots/test-ext-autodoc/target/final.py @@ -1,7 +1,9 @@ from __future__ import annotations import typing +import typing_extensions from typing import final +from typing_extensions import final as final_ext @typing.final @@ -14,3 +16,11 @@ def meth1(self): def meth2(self): """docstring""" + + @final_ext + def meth3(self): + """docstring""" + + @typing_extensions.final + def meth4(self): + """docstring""" diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index 3ff071b6310..bda761ead29 100644 --- a/tests/test_extensions/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -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', + '', ] From bd2f69e36359eceb63002910049440f38a6a4abd Mon Sep 17 00:00:00 2001 From: Spencer Brown Date: Sun, 27 Apr 2025 18:30:17 +1000 Subject: [PATCH 3/9] Handle typing_extensions in pycode.parser --- sphinx/pycode/parser.py | 45 +++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index 34d30200f75..c9f83d5b5cb 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -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: list[str] = [] + self.typing_final: list[str] = [] + self.typing_overload: list[str] = [] super().__init__() def get_qualname_for(self, name: str) -> list[str] | None: @@ -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.copy() + for modname in self.typing: + final.append(f'{modname}.final') for decorator in decorators: try: if ast_unparse(decorator) in final: @@ -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.copy() + for modname in self.typing: + overload.append(f'{modname}.overload') for decorator in decorators: try: @@ -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.append(name.asname or name.name) + elif name.name in ('typing.final', 'typing_extensions.final'): + self.typing_final.append(name.asname or name.name) + elif name.name in ('typing.overload', 'typing_extensions.overload'): + self.typing_overload.append(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.append(name.asname or name.name) + elif name.name == 'overload': + self.typing_overload.append(name.asname or name.name) def visit_Assign(self, node: ast.Assign) -> None: """Handles Assign node and pick up a variable comment.""" From 2ccb530997cfe8506f587ccb1ccbe0dfa936c540 Mon Sep 17 00:00:00 2001 From: Spencer Brown Date: Sun, 27 Apr 2025 18:39:50 +1000 Subject: [PATCH 4/9] Rename attributes --- sphinx/pycode/parser.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index c9f83d5b5cb..3484b5ff642 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -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: list[str] = [] - self.typing_final: list[str] = [] - self.typing_overload: list[str] = [] + self.typing_mods: list[str] = [] + self.typing_final_names: list[str] = [] + self.typing_overload_names: list[str] = [] super().__init__() def get_qualname_for(self, name: str) -> list[str] | None: @@ -295,8 +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 = self.typing_final.copy() - for modname in self.typing: + final = self.typing_final_names.copy() + for modname in self.typing_mods: final.append(f'{modname}.final') for decorator in decorators: try: @@ -308,8 +308,8 @@ def is_final(self, decorators: list[ast.expr]) -> bool: return False def is_overload(self, decorators: list[ast.expr]) -> bool: - overload = self.typing_overload.copy() - for modname in self.typing: + overload = self.typing_overload_names.copy() + for modname in self.typing_mods: overload.append(f'{modname}.overload') for decorator in decorators: @@ -344,11 +344,11 @@ def visit_Import(self, node: ast.Import) -> None: self.add_entry(name.asname or name.name) if name.name in ('typing', 'typing_extensions'): - self.typing.append(name.asname or name.name) + self.typing_mods.append(name.asname or name.name) elif name.name in ('typing.final', 'typing_extensions.final'): - self.typing_final.append(name.asname or name.name) + self.typing_final_names.append(name.asname or name.name) elif name.name in ('typing.overload', 'typing_extensions.overload'): - self.typing_overload.append(name.asname or name.name) + self.typing_overload_names.append(name.asname or name.name) def visit_ImportFrom(self, node: ast.ImportFrom) -> None: """Handles Import node and record the order of definitions.""" @@ -358,9 +358,9 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None: if node.module not in ('typing', 'typing_extensions'): continue if name.name == 'final': - self.typing_final.append(name.asname or name.name) + self.typing_final_names.append(name.asname or name.name) elif name.name == 'overload': - self.typing_overload.append(name.asname or name.name) + self.typing_overload_names.append(name.asname or name.name) def visit_Assign(self, node: ast.Assign) -> None: """Handles Assign node and pick up a variable comment.""" From 98e90c6894fcc988b7029169d58de9b838980a17 Mon Sep 17 00:00:00 2001 From: Spencer Brown Date: Sun, 27 Apr 2025 19:02:39 +1000 Subject: [PATCH 5/9] Add changelog --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fede8b5177b..a9400dd34bb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -19,6 +19,8 @@ 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 ` and :py:func:`~typing.final` decorators. + Patch by Spencer Brown. Bugs fixed ---------- From c9485f01f9e3b9ae2fd46740c9c4b06aad5d76b1 Mon Sep 17 00:00:00 2001 From: Spencer Brown Date: Sun, 27 Apr 2025 19:02:22 +1000 Subject: [PATCH 6/9] Apply lints --- sphinx/pycode/parser.py | 8 ++++---- tests/roots/test-ext-autodoc/target/final.py | 5 +++-- tests/roots/test-ext-autodoc/target/overload3.py | 7 ++++--- tests/test_extensions/test_ext_autodoc.py | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index 3484b5ff642..ce1253c3a59 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -343,11 +343,11 @@ def visit_Import(self, node: ast.Import) -> None: for name in node.names: self.add_entry(name.asname or name.name) - if name.name in ('typing', 'typing_extensions'): + if name.name in {'typing', 'typing_extensions'}: self.typing_mods.append(name.asname or name.name) - elif name.name in ('typing.final', 'typing_extensions.final'): + elif name.name in {'typing.final', 'typing_extensions.final'}: self.typing_final_names.append(name.asname or name.name) - elif name.name in ('typing.overload', 'typing_extensions.overload'): + elif name.name in {'typing.overload', 'typing_extensions.overload'}: self.typing_overload_names.append(name.asname or name.name) def visit_ImportFrom(self, node: ast.ImportFrom) -> None: @@ -355,7 +355,7 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None: for name in node.names: self.add_entry(name.asname or name.name) - if node.module not in ('typing', 'typing_extensions'): + if node.module not in {'typing', 'typing_extensions'}: continue if name.name == 'final': self.typing_final_names.append(name.asname or name.name) diff --git a/tests/roots/test-ext-autodoc/target/final.py b/tests/roots/test-ext-autodoc/target/final.py index ffa888c94f8..bd233abb580 100644 --- a/tests/roots/test-ext-autodoc/target/final.py +++ b/tests/roots/test-ext-autodoc/target/final.py @@ -1,9 +1,10 @@ from __future__ import annotations import typing -import typing_extensions from typing import final -from typing_extensions import final as final_ext + +import typing_extensions +from typing_extensions import final as final_ext # noqa: UP035 @typing.final diff --git a/tests/roots/test-ext-autodoc/target/overload3.py b/tests/roots/test-ext-autodoc/target/overload3.py index 69d0cbb930f..a3cc34a9f85 100644 --- a/tests/roots/test-ext-autodoc/target/overload3.py +++ b/tests/roots/test-ext-autodoc/target/overload3.py @@ -1,13 +1,14 @@ -from typing import TYPE_CHECKING, overload import typing +from typing import TYPE_CHECKING, overload -from typing_extensions import overload as over_ext import typing_extensions +from typing_extensions import overload as over_ext # noqa: UP035 + @overload def test(x: int) -> int: ... @typing.overload -def test(x: bool) -> bool: ... +def test(x: list[int]) -> list[int]: ... @over_ext def test(x: str) -> str: ... @typing_extensions.overload diff --git a/tests/test_extensions/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index bda761ead29..80214bd283d 100644 --- a/tests/test_extensions/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -2916,7 +2916,7 @@ def test_overload3(app): '', '', '.. py:function:: test(x: int) -> int', - ' test(x: bool) -> bool', + ' test(x: list[int]) -> list[int]', ' test(x: str) -> str', ' test(x: float) -> float', ' :module: target.overload3', From 9365553fa2e3580002351eacd96f0accf625add2 Mon Sep 17 00:00:00 2001 From: Spencer Brown Date: Sun, 27 Apr 2025 19:07:38 +1000 Subject: [PATCH 7/9] Word wrap --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a9400dd34bb..66f8f8f2491 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -19,7 +19,8 @@ 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 ` and :py:func:`~typing.final` decorators. +* #13704: Autodoc: Detect :py:func:`typing_extensions.overload ` + and :py:func:`~typing.final` decorators. Patch by Spencer Brown. Bugs fixed From 0ccb5b78d29f076b1309c5997f0f2b2651258c11 Mon Sep 17 00:00:00 2001 From: Spencer Brown Date: Mon, 28 Apr 2025 10:17:03 +1000 Subject: [PATCH 8/9] Switch to use sets --- sphinx/pycode/parser.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index ce1253c3a59..573895620bd 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -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_mods: list[str] = [] - self.typing_final_names: list[str] = [] - self.typing_overload_names: list[str] = [] + 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: @@ -295,9 +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 = self.typing_final_names.copy() - for modname in self.typing_mods: - final.append(f'{modname}.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: @@ -308,9 +308,9 @@ def is_final(self, decorators: list[ast.expr]) -> bool: return False def is_overload(self, decorators: list[ast.expr]) -> bool: - overload = self.typing_overload_names.copy() - for modname in self.typing_mods: - overload.append(f'{modname}.overload') + overload = self.typing_overload_names | { + f'{modname}.overload' for modname in self.typing_mods + } for decorator in decorators: try: @@ -344,11 +344,11 @@ def visit_Import(self, node: ast.Import) -> None: self.add_entry(name.asname or name.name) if name.name in {'typing', 'typing_extensions'}: - self.typing_mods.append(name.asname or name.name) + self.typing_mods.add(name.asname or name.name) elif name.name in {'typing.final', 'typing_extensions.final'}: - self.typing_final_names.append(name.asname or name.name) + self.typing_final_names.add(name.asname or name.name) elif name.name in {'typing.overload', 'typing_extensions.overload'}: - self.typing_overload_names.append(name.asname or name.name) + 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.""" @@ -358,9 +358,9 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None: if node.module not in {'typing', 'typing_extensions'}: continue if name.name == 'final': - self.typing_final_names.append(name.asname or name.name) + self.typing_final_names.add(name.asname or name.name) elif name.name == 'overload': - self.typing_overload_names.append(name.asname or name.name) + 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.""" From 4849c17fb2220993fe959e24da55432c84a8ffe4 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Tue, 20 May 2025 02:47:58 +0100 Subject: [PATCH 9/9] tweaks --- CHANGES.rst | 2 +- sphinx/pycode/parser.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index bb84286c11d..9b86d2df25e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,7 +22,7 @@ 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 ` +* #13704: autodoc: Detect :py:func:`typing_extensions.overload ` and :py:func:`~typing.final` decorators. Patch by Spencer Brown. diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index ed56f1d8dc4..43081c61f13 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -295,9 +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 = self.typing_final_names | { - f'{modname}.final' for modname in self.typing_mods - } + final = {f'{modname}.final' for modname in self.typing_mods} + final |= self.typing_final_names + for decorator in decorators: try: if ast_unparse(decorator) in final: @@ -308,9 +308,8 @@ def is_final(self, decorators: list[ast.expr]) -> bool: return False def is_overload(self, decorators: list[ast.expr]) -> bool: - overload = self.typing_overload_names | { - f'{modname}.overload' for modname in self.typing_mods - } + overload = {f'{modname}.overload' for modname in self.typing_mods} + overload |= self.typing_overload_names for decorator in decorators: try: