From 84122c0f810b0b9134fae318d91471ab5f72568c Mon Sep 17 00:00:00 2001 From: Ian Wienand Date: Tue, 6 Aug 2024 08:10:52 +1000 Subject: [PATCH] Add force_refs_lower flag (#179) --- CHANGELOG.md | 1 + README.md | 13 ++++++++++++- roots/test-force-refs-lower/conf.py | 8 ++++++++ roots/test-force-refs-lower/index.rst | 8 ++++++++ roots/test-force-refs-lower/parser.py | 11 +++++++++++ src/sphinx_argparse_cli/_logic.py | 18 ++++++++++++++---- tests/test_logic.py | 22 ++++++++++++++++++++++ 7 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 roots/test-force-refs-lower/conf.py create mode 100644 roots/test-force-refs-lower/index.rst create mode 100644 roots/test-force-refs-lower/parser.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d317a93..bac7aae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. - Allow to add content to directive. - Fix Sphinx warnings about parallel reads. +- Add `force_args_lower` to enable `:ref:` links with mixed-case program names and arguments. ## 1.13.1 diff --git a/README.md b/README.md index 4ce961b..c8018f1 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Within the reStructuredText files use the `sphinx_argparse_cli` directive that t | group_title_prefix | (optional) groups subsections title prefixes, accepts the string `{prog}` as a replacement for the program name - defaults to `{prog}` | | group_sub_title_prefix | (optional) subcommands groups subsections title prefixes, accepts replacement of `{prog}` and `{subcommand}` for program and subcommand name - defaults to `{prog} {subcommand}` | | no_default_values | (optional) suppresses generation of `default` entries | - +| force_refs_lower | (optional) Sphinx `:ref:` only supports lower-case references. With this, any capital letter in generated reference anchors are lowered and given an `_` prefix (i.e. `A` becomes `_a`) | For example: ```rst @@ -84,3 +84,14 @@ being `cli`: - to refer to the optional arguments group use ``:ref:`cli:tox-optional-arguments` ``, - to refer to the run subcommand use ``:ref:`cli:tox-run` ``, - to refer to flag `--magic` of the `run` sub-command use ``:ref:`cli:tox-run---magic` ``. + +Due to Sphinx's `:ref:` only supporting lower-case values, if you need to distinguish mixed case program names or +arguments, set the `:force_refs_lower:` argument. With this flag, captial-letters in references will be converted to +their lower-case counterpart and prefixed with an `_`. For example: + +- A `prog` name `SampleProgram` will be referenced as ``:ref:`_sample_program...` ``. +- To distinguish between mixed case flags `-a` and `-A` use ``:ref:`_sample_program--a` `` and ``:ref:`_sample_program--_a` `` respectively + +Note that if you are _not_ concernced about using internal Sphinx `:ref:` cross-references, you may choose to leave this +off to maintain mixed-case anchors in your output HTML; but be aware that later enabling it will change your anchors in +the output HTML. diff --git a/roots/test-force-refs-lower/conf.py b/roots/test-force-refs-lower/conf.py new file mode 100644 index 0000000..9f2a54a --- /dev/null +++ b/roots/test-force-refs-lower/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-force-refs-lower/index.rst b/roots/test-force-refs-lower/index.rst new file mode 100644 index 0000000..a495f5c --- /dev/null +++ b/roots/test-force-refs-lower/index.rst @@ -0,0 +1,8 @@ +.. sphinx_argparse_cli:: + :module: parser + :func: make + :force_refs_lower: + +Reference test +-------------- +Flag :ref:`_prog--_b` and :ref:`_prog--b` and positional :ref:`_prog-root`. diff --git a/roots/test-force-refs-lower/parser.py b/roots/test-force-refs-lower/parser.py new file mode 100644 index 0000000..94e2186 --- /dev/null +++ b/roots/test-force-refs-lower/parser.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from argparse import ArgumentParser + + +def make() -> ArgumentParser: + parser = ArgumentParser(description="argparse tester", prog="Prog") + parser.add_argument("root") + parser.add_argument("--build", "-B", action="store_true", help="build flag") + parser.add_argument("--binary", "-b", action="store_true", help="binary flag") + return parser diff --git a/src/sphinx_argparse_cli/_logic.py b/src/sphinx_argparse_cli/_logic.py index d5c929e..2cc7098 100644 --- a/src/sphinx_argparse_cli/_logic.py +++ b/src/sphinx_argparse_cli/_logic.py @@ -52,6 +52,11 @@ def make_id(key: str) -> str: return "-".join(key.split()).rstrip("-") +def make_id_lower(key: str) -> str: + # replace all capital letters "X" with "_lower(X)" + return re.sub("[A-Z]", lambda m: "_" + m.group(0).lower(), make_id(key)) + + logger = getLogger(__name__) @@ -71,6 +76,10 @@ class SphinxArgparseCli(SphinxDirective): "group_title_prefix": unchanged, "group_sub_title_prefix": unchanged, "no_default_values": unchanged, + # :ref: only supports lower-case. If this is set, any + # would-be-upper-case chars will be prefixed with _. Since + # this is backwards incompatible for URL's, this is opt-in. + "force_refs_lower": flag, } def __init__( # noqa: PLR0913 @@ -91,6 +100,7 @@ def __init__( # noqa: PLR0913 self._parser: ArgumentParser | None = None self._std_domain: StandardDomain = cast(StandardDomain, self.env.get_domain("std")) self._raw_format: bool = False + self.make_id = make_id_lower if "force_refs_lower" in self.options else make_id @property def parser(self) -> ArgumentParser: @@ -150,7 +160,7 @@ def run(self) -> list[Node]: if not title_text.strip(): home_section: Element = paragraph() else: - home_section = section("", title("", Text(title_text)), ids=[make_id(title_text)], names=[title_text]) + home_section = section("", title("", Text(title_text)), ids=[self.make_id(title_text)], names=[title_text]) if "usage_first" in self.options: home_section += self._mk_usage(self.parser) @@ -193,7 +203,7 @@ def _mk_option_group(self, group: _ArgumentGroup, prefix: str) -> section: title_text = self._build_opt_grp_title(group, prefix, sub_title_prefix, title_prefix) title_ref: str = f"{prefix}{' ' if prefix else ''}{group.title}" - ref_id = make_id(title_ref) + ref_id = self.make_id(title_ref) # the text sadly needs to be prefixed, because otherwise the autosectionlabel will conflict header = title("", Text(title_text)) group_section = section("", header, ids=[ref_id], names=[ref_id]) @@ -271,7 +281,7 @@ def _mk_option_line(self, action: Action, prefix: str) -> list_item: return point def _mk_option_name(self, line: paragraph, prefix: str, opt: str) -> None: - ref_id = make_id(f"{prefix}-{opt}") + ref_id = self.make_id(f"{prefix}-{opt}") ref_title = f"{prefix} {opt}" ref = reference("", refid=ref_id, reftitle=ref_title) line.attributes["ids"].append(ref_id) @@ -317,7 +327,7 @@ def _mk_sub_command(self, aliases: list[str], help_msg: str, parser: ArgumentPar title_text += aliases_text title_ref += aliases_text title_text = title_text.strip() - ref_id = make_id(title_ref) + ref_id = self.make_id(title_ref) group_section = section("", title("", Text(title_text)), ids=[ref_id], names=[title_ref]) self._register_ref(ref_id, title_ref, group_section) diff --git a/tests/test_logic.py b/tests/test_logic.py index 3426030..77ce6bd 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -7,6 +7,8 @@ import pytest +from sphinx_argparse_cli._logic import make_id, make_id_lower + if TYPE_CHECKING: from io import StringIO @@ -281,6 +283,26 @@ def test_lower_upper_refs(build_outcome: str, warning: StringIO) -> None: assert not warning.getvalue() +@pytest.mark.parametrize( + ("key", "mixed", "lower"), + [ + ("ProgramName", "ProgramName", "_program_name"), + ("ProgramName -A", "ProgramName--A", "_program_name--_a"), + ("ProgramName -a", "ProgramName--a", "_program_name--a"), + ], +) +def test_make_id(key: str, mixed: str, lower: str) -> None: + assert make_id(key) == mixed + assert make_id_lower(key) == lower + + +@pytest.mark.sphinx(buildername="html", testroot="force-refs-lower") +def test_ref_cases(build_outcome: str, warning: StringIO) -> None: + assert '' in build_outcome + assert '' in build_outcome + assert not warning.getvalue() + + @pytest.mark.sphinx(buildername="text", testroot="default-handling") def test_with_default(build_outcome: str) -> None: assert (