From 05a4708e92cf623202dc846efed9974e882a01bf Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Fri, 26 Dec 2025 18:57:56 +0000 Subject: [PATCH 1/2] fix(autodoc): restore type alias docstrings --- sphinx/pycode/parser.py | 39 ++++++++++++------- .../roots/test-ext-autodoc-typealias/conf.py | 6 +++ .../test-ext-autodoc-typealias/index.rst | 4 ++ .../type_alias_docstrings.py | 38 ++++++++++++++++++ tests/test_ext_autodoc_autodata.py | 32 +++++++++++++++ 5 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 tests/roots/test-ext-autodoc-typealias/conf.py create mode 100644 tests/roots/test-ext-autodoc-typealias/index.rst create mode 100644 tests/roots/test-ext-autodoc-typealias/type_alias_docstrings.py diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index a00b481ceb4..30d27c6bac5 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -400,21 +400,32 @@ def visit_AnnAssign(self, node: ast.AnnAssign) -> None: def visit_Expr(self, node: ast.Expr) -> None: """Handles Expr node and pick up a comment if string.""" - if (isinstance(self.previous, (ast.Assign, ast.AnnAssign)) and - isinstance(node.value, ast.Str)): - try: - targets = get_assign_targets(self.previous) - varnames = get_lvar_names(targets[0], self.get_self()) - for varname in varnames: - if isinstance(node.value.s, str): - docstring = node.value.s - else: - docstring = node.value.s.decode(self.encoding or 'utf-8') + if not isinstance(self.previous, (ast.Assign, ast.AnnAssign)): + return - self.add_variable_comment(varname, dedent_docstring(docstring)) - self.add_entry(varname) - except TypeError: - pass # this assignment is not new definition! + raw_value: Optional[Any] = None + if isinstance(node.value, ast.Str): + raw_value = node.value.s + elif isinstance(node.value, ast.Constant): + if isinstance(node.value.value, (str, bytes)): + raw_value = node.value.value + + if raw_value is None: + return + + try: + targets = get_assign_targets(self.previous) + varnames = get_lvar_names(targets[0], self.get_self()) + for varname in varnames: + if isinstance(raw_value, str): + docstring = raw_value + else: + docstring = raw_value.decode(self.encoding or 'utf-8') + + self.add_variable_comment(varname, dedent_docstring(docstring)) + self.add_entry(varname) + except TypeError: + pass # this assignment is not new definition! def visit_Try(self, node: ast.Try) -> None: """Handles Try node and processes body and else-clause. diff --git a/tests/roots/test-ext-autodoc-typealias/conf.py b/tests/roots/test-ext-autodoc-typealias/conf.py new file mode 100644 index 00000000000..e5f6bb97a20 --- /dev/null +++ b/tests/roots/test-ext-autodoc-typealias/conf.py @@ -0,0 +1,6 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath('.')) + +extensions = ['sphinx.ext.autodoc'] diff --git a/tests/roots/test-ext-autodoc-typealias/index.rst b/tests/roots/test-ext-autodoc-typealias/index.rst new file mode 100644 index 00000000000..7382fc59dfb --- /dev/null +++ b/tests/roots/test-ext-autodoc-typealias/index.rst @@ -0,0 +1,4 @@ +Type Alias Docstring Fixtures +============================= + +.. automodule:: type_alias_docstrings diff --git a/tests/roots/test-ext-autodoc-typealias/type_alias_docstrings.py b/tests/roots/test-ext-autodoc-typealias/type_alias_docstrings.py new file mode 100644 index 00000000000..0fd6508b24f --- /dev/null +++ b/tests/roots/test-ext-autodoc-typealias/type_alias_docstrings.py @@ -0,0 +1,38 @@ +"""Fixtures for type alias docstring handling tests.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, Callable, Dict, TypeAlias + + +ScaffoldOpts = Dict[str, Any] +"""Dictionary with PyScaffold's options, see ``pyscaffold.api.create_project``. +Should be treated as immutable (copy before mutating). + +Please notice some behaviours given by the options **SHOULD** be observed. For +example, files should be overwritten when the **force** option is ``True``. +Similarly when **pretend** is ``True``, no operation should be really +performed, but any action should be logged as if realized. +""" + + +FileContents: TypeAlias = str | None +"""When the file content is ``None``, the file should not be written to disk. +Empty files are represented by an empty string ``""`` as content. +""" + + +FileOp: TypeAlias = Callable[[Path, FileContents, ScaffoldOpts], Path | None] +"""Signature of functions considered file operations:: + + Callable[[Path, FileContents, ScaffoldOpts], Path | None] + +- **path** (:class:`pathlib.Path`): file path potentially written to disk. +- **contents** (:obj:`FileContents`): usual text content. :obj:`None` skips + writing the file. +- **opts** (:obj:`ScaffoldOpts`): a dict with PyScaffold's options. + +If a file is written (or permissions are changed) the implementation should +return the :class:`pathlib.Path`. Otherwise :obj:`None` should be returned. +""" diff --git a/tests/test_ext_autodoc_autodata.py b/tests/test_ext_autodoc_autodata.py index d01e45fc10a..b79b141266b 100644 --- a/tests/test_ext_autodoc_autodata.py +++ b/tests/test_ext_autodoc_autodata.py @@ -155,3 +155,35 @@ def test_autodata_hide_value(app): ' :meta hide-value:', '', ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc-typealias') +def test_autodata_type_alias_docstring(monkeypatch, app): + from sphinx.pycode import parser + + class _FakeStr(parser.ast.AST): + _fields = () + + monkeypatch.setattr(parser.ast, 'Str', _FakeStr, raising=False) + + actual = do_autodoc(app, 'data', 'type_alias_docstrings.ScaffoldOpts') + assert list(actual) == [ + '', + '.. py:data:: ScaffoldOpts', + ' :module: type_alias_docstrings', + '', + ' Dictionary with PyScaffold\'s options, see ' + '``pyscaffold.api.create_project``.', + ' Should be treated as immutable (copy before mutating).', + '', + ' Please notice some behaviours given by the options **SHOULD** be ' + 'observed. For', + ' example, files should be overwritten when the **force** option is ' + '``True``.', + ' Similarly when **pretend** is ``True``, no operation should be really', + ' performed, but any action should be logged as if realized.', + '', + ' alias of :class:`~typing.Dict`\\ [:class:`str`, ' + ':class:`~typing.Any`]', + '', + ] From a4425deceab0c991f02eb561878aa1b23aa82e2b Mon Sep 17 00:00:00 2001 From: Casey Brooks Date: Fri, 26 Dec 2025 19:08:57 +0000 Subject: [PATCH 2/2] test(autodoc): support py36 in alias fixtures --- .../type_alias_docstrings.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/roots/test-ext-autodoc-typealias/type_alias_docstrings.py b/tests/roots/test-ext-autodoc-typealias/type_alias_docstrings.py index 0fd6508b24f..253c931b931 100644 --- a/tests/roots/test-ext-autodoc-typealias/type_alias_docstrings.py +++ b/tests/roots/test-ext-autodoc-typealias/type_alias_docstrings.py @@ -1,9 +1,7 @@ """Fixtures for type alias docstring handling tests.""" -from __future__ import annotations - from pathlib import Path -from typing import Any, Callable, Dict, TypeAlias +from typing import Any, Callable, Dict, Optional ScaffoldOpts = Dict[str, Any] @@ -17,16 +15,16 @@ """ -FileContents: TypeAlias = str | None +FileContents = Optional[str] """When the file content is ``None``, the file should not be written to disk. Empty files are represented by an empty string ``""`` as content. """ -FileOp: TypeAlias = Callable[[Path, FileContents, ScaffoldOpts], Path | None] +FileOp = Callable[[Path, FileContents, ScaffoldOpts], Optional[Path]] """Signature of functions considered file operations:: - Callable[[Path, FileContents, ScaffoldOpts], Path | None] + Callable[[Path, FileContents, ScaffoldOpts], Optional[Path]] - **path** (:class:`pathlib.Path`): file path potentially written to disk. - **contents** (:obj:`FileContents`): usual text content. :obj:`None` skips