Skip to content

Commit

Permalink
Add force_refs_lower flag (#179)
Browse files Browse the repository at this point in the history
  • Loading branch information
ianw authored Aug 5, 2024
1 parent 27990d1 commit 84122c0
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
8 changes: 8 additions & 0 deletions roots/test-force-refs-lower/conf.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions roots/test-force-refs-lower/index.rst
Original file line number Diff line number Diff line change
@@ -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`.
11 changes: 11 additions & 0 deletions roots/test-force-refs-lower/parser.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 14 additions & 4 deletions src/sphinx_argparse_cli/_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)


Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
22 changes: 22 additions & 0 deletions tests/test_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import pytest

from sphinx_argparse_cli._logic import make_id, make_id_lower

if TYPE_CHECKING:
from io import StringIO

Expand Down Expand Up @@ -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 '<a class="reference internal" href="#_prog--_b" title="Prog -B">' in build_outcome
assert '<a class="reference internal" href="#_prog--b" title="Prog -b">' in build_outcome
assert not warning.getvalue()


@pytest.mark.sphinx(buildername="text", testroot="default-handling")
def test_with_default(build_outcome: str) -> None:
assert (
Expand Down

0 comments on commit 84122c0

Please sign in to comment.