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..253c931b931 --- /dev/null +++ b/tests/roots/test-ext-autodoc-typealias/type_alias_docstrings.py @@ -0,0 +1,36 @@ +"""Fixtures for type alias docstring handling tests.""" + +from pathlib import Path +from typing import Any, Callable, Dict, Optional + + +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 = 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 = Callable[[Path, FileContents, ScaffoldOpts], Optional[Path]] +"""Signature of functions considered file operations:: + + 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 + 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`]', + '', + ]