From 03ba2791a260c8afc7b2e5b448949cff45a70d9a Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 26 Jan 2024 11:44:02 +0100 Subject: [PATCH 1/4] Fix parsing --- src/sphinx_autodoc_typehints/__init__.py | 14 +++---- .../attributes_patch.py | 7 ++-- src/sphinx_autodoc_typehints/parser.py | 24 ++++++++++++ .../dummy_module_future_annotations.py | 2 +- tests/test_sphinx_autodoc_typehints.py | 38 ++++++++++++++++++- 5 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 src/sphinx_autodoc_typehints/parser.py diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py index a4f735a3..69e1489a 100644 --- a/src/sphinx_autodoc_typehints/__init__.py +++ b/src/sphinx_autodoc_typehints/__init__.py @@ -13,14 +13,13 @@ from docutils import nodes from docutils.frontend import OptionParser -from docutils.parsers.rst import Parser as RstParser -from docutils.parsers.rst import states -from docutils.utils import new_document from sphinx.ext.autodoc.mock import mock +from sphinx.parsers import RSTParser from sphinx.util import logging, rst from sphinx.util.inspect import signature as sphinx_signature from sphinx.util.inspect import stringify_signature +from .parser import parse from .patches import install_patches from .version import __version__ @@ -28,6 +27,7 @@ from ast import FunctionDef, Module, stmt from docutils.nodes import Node + from docutils.parsers.rst import states from sphinx.application import Sphinx from sphinx.config import Config from sphinx.environment import BuildEnvironment @@ -793,10 +793,9 @@ def get_insert_index(app: Sphinx, lines: list[str]) -> InsertIndexInfo | None: # 3. Insert after the parameters. # To find the parameters, parse as a docutils tree. - settings = OptionParser(components=(RstParser,)).get_default_values() + settings = OptionParser(components=(RSTParser,)).get_default_values() settings.env = app.env - doc = new_document("", settings=settings) - RstParser().parse("\n".join(lines), doc) + doc = parse("\n".join(lines), settings) # Find a top level child which is a field_list that contains a field whose # name starts with one of the PARAM_SYNONYMS. This is the parameter list. We @@ -915,8 +914,7 @@ def sphinx_autodoc_typehints_type_role( """ unescaped = unescape(text) # the typestubs for docutils don't have any info about Inliner - doc = new_document("", inliner.document.settings) # type: ignore[attr-defined] - RstParser().parse(unescaped, doc) + doc = parse(unescaped, inliner.document.settings) # type: ignore[attr-defined] n = nodes.inline(text) n["classes"].append("sphinx_autodoc_typehints-type") n += doc.children[0].children diff --git a/src/sphinx_autodoc_typehints/attributes_patch.py b/src/sphinx_autodoc_typehints/attributes_patch.py index 807fab3e..e7df3335 100644 --- a/src/sphinx_autodoc_typehints/attributes_patch.py +++ b/src/sphinx_autodoc_typehints/attributes_patch.py @@ -7,11 +7,11 @@ import sphinx.domains.python import sphinx.ext.autodoc -from docutils.parsers.rst import Parser as RstParser -from docutils.utils import new_document from sphinx.domains.python import PyAttribute from sphinx.ext.autodoc import AttributeDocumenter +from .parser import parse + if TYPE_CHECKING: from optparse import Values @@ -62,8 +62,7 @@ def add_directive_header(*args: Any, **kwargs: Any) -> Any: def rst_to_docutils(settings: Values, rst: str) -> Any: """Convert rst to a sequence of docutils nodes.""" - doc = new_document("", settings) - RstParser().parse(rst, doc) + doc = parse(rst, settings) # Remove top level paragraph node so that there is no line break. return doc.children[0].children diff --git a/src/sphinx_autodoc_typehints/parser.py b/src/sphinx_autodoc_typehints/parser.py new file mode 100644 index 00000000..e81d0c2c --- /dev/null +++ b/src/sphinx_autodoc_typehints/parser.py @@ -0,0 +1,24 @@ +"""Utilities for side-effect-free rST parsing.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from docutils.utils import new_document +from sphinx.parsers import RSTParser +from sphinx.util.docutils import sphinx_domains + +if TYPE_CHECKING: + from optparse import Values + + from docutils import nodes + + +def parse(inputstr: str, settings: Values) -> nodes.document: + """Parse inputstr and return a docutils document.""" + doc = new_document("", settings=settings) + with sphinx_domains(settings.env): + parser = RSTParser() + parser.set_application(settings.env.app) + parser.parse(inputstr, doc) + return doc diff --git a/tests/roots/test-dummy/dummy_module_future_annotations.py b/tests/roots/test-dummy/dummy_module_future_annotations.py index 389cc98b..701f1ac1 100644 --- a/tests/roots/test-dummy/dummy_module_future_annotations.py +++ b/tests/roots/test-dummy/dummy_module_future_annotations.py @@ -10,7 +10,7 @@ def function_with_py310_annotations( """ Method docstring. - :param x: foo + :param x: `foo` :param y: bar :param z: baz """ diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index 10b22d3b..b7bdf86c 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -554,7 +554,7 @@ def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) Method docstring. Parameters: - * **x** (bool | None) -- foo + * **x** (bool | None) -- *foo* * **y** ("int" | "str" | "float") -- bar @@ -567,6 +567,42 @@ def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) assert contents == expected_contents +@pytest.mark.sphinx("pseudoxml", testroot="dummy") +@patch("sphinx.writers.text.MAXWIDTH", 2000) +def test_sphinx_output_default_role(app: SphinxTestApp, status: StringIO) -> None: + set_python_path() + + app.config.master_doc = "future_annotations" # type: ignore[attr-defined] # create flag + app.config.default_role = "literal" + app.build() + + assert "build succeeded" in status.getvalue() # Build succeeded + + contents_lines = (Path(app.srcdir) / "_build/pseudoxml/future_annotations.pseudoxml").read_text().splitlines() + list_item_idxs = [i for i, line in enumerate(contents_lines) if line.strip() == ""] + foo_param = dedent("\n".join(contents_lines[list_item_idxs[0] : list_item_idxs[1]])) + expected_foo_param = """\ + + + + x + ( + + + Optional + [ + + bool + ] + ) + \N{EN DASH}\N{SPACE} + + foo + """.rstrip() + expected_foo_param = maybe_fix_py310(dedent(expected_foo_param)) + assert foo_param == expected_foo_param + + @pytest.mark.parametrize( ("defaults_config_val", "expected"), [ From 46f9ef45a550f84ac934ff84b2f03c6ae947be3b Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 26 Jan 2024 11:57:10 +0100 Subject: [PATCH 2/4] simplify --- .../test-dummy/dummy_module_future_annotations.py | 2 +- .../test-dummy/dummy_module_simple_default_role.py | 10 ++++++++++ tests/roots/test-dummy/simple_default_role.rst | 4 ++++ tests/test_sphinx_autodoc_typehints.py | 12 ++++-------- 4 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 tests/roots/test-dummy/dummy_module_simple_default_role.py create mode 100644 tests/roots/test-dummy/simple_default_role.rst diff --git a/tests/roots/test-dummy/dummy_module_future_annotations.py b/tests/roots/test-dummy/dummy_module_future_annotations.py index 701f1ac1..389cc98b 100644 --- a/tests/roots/test-dummy/dummy_module_future_annotations.py +++ b/tests/roots/test-dummy/dummy_module_future_annotations.py @@ -10,7 +10,7 @@ def function_with_py310_annotations( """ Method docstring. - :param x: `foo` + :param x: foo :param y: bar :param z: baz """ diff --git a/tests/roots/test-dummy/dummy_module_simple_default_role.py b/tests/roots/test-dummy/dummy_module_simple_default_role.py new file mode 100644 index 00000000..758196f4 --- /dev/null +++ b/tests/roots/test-dummy/dummy_module_simple_default_role.py @@ -0,0 +1,10 @@ +from __future__ import annotations + + +def function(x: bool, y: int) -> str: # noqa: ARG001 + """ + Function docstring. + + :param x: `foo` + :param y: ``bar`` + """ diff --git a/tests/roots/test-dummy/simple_default_role.rst b/tests/roots/test-dummy/simple_default_role.rst new file mode 100644 index 00000000..c3148a79 --- /dev/null +++ b/tests/roots/test-dummy/simple_default_role.rst @@ -0,0 +1,4 @@ +Simple Module +============= + +.. autofunction:: dummy_module_simple_default_role.function diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index b7bdf86c..9d398861 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -554,7 +554,7 @@ def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) Method docstring. Parameters: - * **x** (bool | None) -- *foo* + * **x** (bool | None) -- foo * **y** ("int" | "str" | "float") -- bar @@ -572,13 +572,13 @@ def test_sphinx_output_future_annotations(app: SphinxTestApp, status: StringIO) def test_sphinx_output_default_role(app: SphinxTestApp, status: StringIO) -> None: set_python_path() - app.config.master_doc = "future_annotations" # type: ignore[attr-defined] # create flag + app.config.master_doc = "simple_default_role" # type: ignore[attr-defined] # create flag app.config.default_role = "literal" app.build() assert "build succeeded" in status.getvalue() # Build succeeded - contents_lines = (Path(app.srcdir) / "_build/pseudoxml/future_annotations.pseudoxml").read_text().splitlines() + contents_lines = (Path(app.srcdir) / "_build/pseudoxml/simple_default_role.pseudoxml").read_text().splitlines() list_item_idxs = [i for i, line in enumerate(contents_lines) if line.strip() == ""] foo_param = dedent("\n".join(contents_lines[list_item_idxs[0] : list_item_idxs[1]])) expected_foo_param = """\ @@ -588,18 +588,14 @@ def test_sphinx_output_default_role(app: SphinxTestApp, status: StringIO) -> Non x ( - - Optional - [ bool - ] ) \N{EN DASH}\N{SPACE} foo """.rstrip() - expected_foo_param = maybe_fix_py310(dedent(expected_foo_param)) + expected_foo_param = dedent(expected_foo_param) assert foo_param == expected_foo_param From 412e62493239b17accb46be187b8ec0735276ef1 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 26 Jan 2024 12:03:17 +0100 Subject: [PATCH 3/4] Type fixes --- src/sphinx_autodoc_typehints/attributes_patch.py | 5 ++--- src/sphinx_autodoc_typehints/parser.py | 3 +-- tests/test_sphinx_autodoc_typehints.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/sphinx_autodoc_typehints/attributes_patch.py b/src/sphinx_autodoc_typehints/attributes_patch.py index e7df3335..d1996756 100644 --- a/src/sphinx_autodoc_typehints/attributes_patch.py +++ b/src/sphinx_autodoc_typehints/attributes_patch.py @@ -13,8 +13,7 @@ from .parser import parse if TYPE_CHECKING: - from optparse import Values - + from docutils.frontend import Values from sphinx.addnodes import desc_signature from sphinx.application import Sphinx @@ -64,7 +63,7 @@ def rst_to_docutils(settings: Values, rst: str) -> Any: """Convert rst to a sequence of docutils nodes.""" doc = parse(rst, settings) # Remove top level paragraph node so that there is no line break. - return doc.children[0].children + return doc.children[0].children # type:ignore[attr-defined] def patched_parse_annotation(settings: Values, typ: str, env: Any) -> Any: diff --git a/src/sphinx_autodoc_typehints/parser.py b/src/sphinx_autodoc_typehints/parser.py index e81d0c2c..00de1531 100644 --- a/src/sphinx_autodoc_typehints/parser.py +++ b/src/sphinx_autodoc_typehints/parser.py @@ -9,9 +9,8 @@ from sphinx.util.docutils import sphinx_domains if TYPE_CHECKING: - from optparse import Values - from docutils import nodes + from docutils.frontend import Values def parse(inputstr: str, settings: Values) -> nodes.document: diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index 9d398861..2c05c991 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -573,7 +573,7 @@ def test_sphinx_output_default_role(app: SphinxTestApp, status: StringIO) -> Non set_python_path() app.config.master_doc = "simple_default_role" # type: ignore[attr-defined] # create flag - app.config.default_role = "literal" + app.config.default_role = "literal" # type: ignore[attr-defined] app.build() assert "build succeeded" in status.getvalue() # Build succeeded From ac10c0be7b5bfcf146ef25a9f45867d2d69ee8d4 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Fri, 26 Jan 2024 12:07:43 +0100 Subject: [PATCH 4/4] more type fixes --- src/sphinx_autodoc_typehints/attributes_patch.py | 2 +- src/sphinx_autodoc_typehints/parser.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sphinx_autodoc_typehints/attributes_patch.py b/src/sphinx_autodoc_typehints/attributes_patch.py index d1996756..e0919556 100644 --- a/src/sphinx_autodoc_typehints/attributes_patch.py +++ b/src/sphinx_autodoc_typehints/attributes_patch.py @@ -63,7 +63,7 @@ def rst_to_docutils(settings: Values, rst: str) -> Any: """Convert rst to a sequence of docutils nodes.""" doc = parse(rst, settings) # Remove top level paragraph node so that there is no line break. - return doc.children[0].children # type:ignore[attr-defined] + return doc.children[0].children def patched_parse_annotation(settings: Values, typ: str, env: Any) -> Any: diff --git a/src/sphinx_autodoc_typehints/parser.py b/src/sphinx_autodoc_typehints/parser.py index 00de1531..ea384448 100644 --- a/src/sphinx_autodoc_typehints/parser.py +++ b/src/sphinx_autodoc_typehints/parser.py @@ -9,11 +9,13 @@ from sphinx.util.docutils import sphinx_domains if TYPE_CHECKING: + import optparse + from docutils import nodes from docutils.frontend import Values -def parse(inputstr: str, settings: Values) -> nodes.document: +def parse(inputstr: str, settings: Values | optparse.Values) -> nodes.document: """Parse inputstr and return a docutils document.""" doc = new_document("", settings=settings) with sphinx_domains(settings.env):