diff --git a/CHANGELOG.md b/CHANGELOG.md index ead2ebc..f31b6cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ All notable changes to this project will be documented in this file. - Skip sub-commands added with `help=argparse.SUPPRESS` instead of rendering them with a `==SUPPRESS==` description. - Use the description, or `arguments`, as the heading of an argument group without a title instead of rendering `None` and emitting duplicate label warnings. +- Fix a crash on whitespace-only help text, render help that parses to lists or several paragraphs as blocks under the + argument instead of inside its paragraph, and drop the empty paragraph an empty `:description:` produced. ## 1.13.1 diff --git a/roots/test-help-nodes/conf.py b/roots/test-help-nodes/conf.py new file mode 100644 index 0000000..9f2a54a --- /dev/null +++ b/roots/test-help-nodes/conf.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) +extensions = ["sphinx_argparse_cli"] +nitpicky = True diff --git a/roots/test-help-nodes/index.rst b/roots/test-help-nodes/index.rst new file mode 100644 index 0000000..8fefd09 --- /dev/null +++ b/roots/test-help-nodes/index.rst @@ -0,0 +1,4 @@ +.. sphinx_argparse_cli:: + :module: parser + :func: make + :description: diff --git a/roots/test-help-nodes/parser.py b/roots/test-help-nodes/parser.py new file mode 100644 index 0000000..c2db0ad --- /dev/null +++ b/roots/test-help-nodes/parser.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from argparse import ArgumentParser, RawTextHelpFormatter + + +def make() -> ArgumentParser: + parser = ArgumentParser(prog="prog", formatter_class=RawTextHelpFormatter, add_help=False) + parser.add_argument("--blank", help=" ") + parser.add_argument("--list", help="- item one\n- item two") + parser.add_argument("--two", help="first paragraph\n\nsecond paragraph") + return parser diff --git a/src/sphinx_argparse_cli/_logic.py b/src/sphinx_argparse_cli/_logic.py index 6a5dbc5..ecf1097 100644 --- a/src/sphinx_argparse_cli/_logic.py +++ b/src/sphinx_argparse_cli/_logic.py @@ -48,7 +48,7 @@ from sphinx.util.logging import getLogger if TYPE_CHECKING: - from collections.abc import Callable, Iterator + from collections.abc import Callable, Iterator, Sequence from sphinx.domains.std import StandardDomain from sphinx.util.logging import SphinxLoggerAdapter @@ -198,7 +198,7 @@ def run(self) -> list[Node]: return [home_section] def _pre_format(self, block: str | None) -> paragraph | literal_block | None: - if block is None: + if block is None or not block.strip(): return None if self._raw_format and "\n" in block: lit = literal_block("", Text(block), classes=["sphinx-argparse-cli-wrap"]) @@ -258,13 +258,17 @@ def _mk_option_line(self, action: Action, prefix: str) -> list_item: else: self._mk_option_name(line, prefix, as_key) + extra: Sequence[Node] = () if action.help: - help_text = load_help_text(action.help) temp = paragraph() - self.state.nested_parse(StringList(help_text.split("\n")), 0, temp) - line += Text(" - ") - for content in cast("paragraph", temp.children[0]).children: - line += content + self.state.nested_parse(StringList(load_help_text(action.help).split("\n")), 0, temp) + # only a leading paragraph can share the option's line; anything else becomes a block under it + if temp.children and isinstance(temp.children[0], paragraph): + line += Text(" - ") + line += temp.children[0].children + extra = temp.children[1:] + else: + extra = temp.children if ( "no_default_values" not in self.options and action.default is not None @@ -275,8 +279,9 @@ def _mk_option_line(self, action: Action, prefix: str) -> list_item: line += Text(" (default: ") line += literal(text=str(action.default).replace(str(Path.cwd()), "{cwd}")) line += Text(")") - _protect_option_dashes(line) - return list_item("", line, ids=[]) + item = list_item("", line, *extra, ids=[]) + _protect_option_dashes(item) + return item def _mk_option_name(self, line: paragraph, prefix: str, opt: str) -> None: ref_id = self._make_id(f"{prefix}-{opt}") diff --git a/tests/test_logic.py b/tests/test_logic.py index 2353fd3..a14c5ef 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -118,7 +118,7 @@ def test_set_description_as_text(build_outcome: str) -> None: @pytest.mark.sphinx(buildername="text", testroot="description-empty") def test_empty_description_as_text(build_outcome: str) -> None: - assert build_outcome == "foo - CLI interface\n*******************\n\n\n foo\n" + assert build_outcome == "foo - CLI interface\n*******************\n\n foo\n" @pytest.mark.sphinx(buildername="html", testroot="description-multiline") @@ -140,7 +140,7 @@ def test_set_epilog_as_text(build_outcome: str) -> None: @pytest.mark.sphinx(buildername="text", testroot="epilog-empty") def test_empty_epilog_as_text(build_outcome: str) -> None: - assert build_outcome == "foo - CLI interface\n*******************\n\n foo\n\n" + assert build_outcome == "foo - CLI interface\n*******************\n\n foo\n" @pytest.mark.sphinx(buildername="html", testroot="epilog-multiline") @@ -323,6 +323,40 @@ def test_usage_ignores_python_colors(build_outcome: str) -> None: ) +@pytest.mark.sphinx(buildername="text", testroot="help-nodes") +def test_help_nodes_as_text(build_outcome: str) -> None: + assert ( + build_outcome + == """prog - CLI interface +******************** + + prog [--blank BLANK] [--list LIST] [--two TWO] + + +prog options +============ + +* **"--blank"** "BLANK" + +* **"--list"** "LIST" + + * item one + + * item two + +* **"--two"** "TWO" - first paragraph + + second paragraph +""" + ) + + +@pytest.mark.sphinx(buildername="html", testroot="help-nodes") +def test_help_nodes_as_html(build_outcome: str, warning: StringIO) -> None: + assert "

" not in build_outcome + assert not warning.getvalue() + + @pytest.mark.sphinx(buildername="html", testroot="group-untitled") def test_group_untitled(build_outcome: str, warning: StringIO) -> None: headings = re.findall(r'

(.*?)