Skip to content

Commit 9b68daa

Browse files
committed
docs/_ext(fix[pretty_argparse]): Escape RST emphasis in argparse patterns
why: Patterns like `session-*` in argparse help trigger RST "Inline emphasis start-string without end-string" warnings during Sphinx build. what: - Add escape_rst_emphasis() function to escape asterisks after hyphens - Override _nested_parse_paragraph() to pre-escape before RST parsing - Add 13 parameterized tests for escape_rst_emphasis behavior
1 parent 32c57d8 commit 9b68daa

2 files changed

Lines changed: 139 additions & 0 deletions

File tree

docs/_ext/pretty_argparse.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,44 @@
2121

2222
_ANSI_RE = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")
2323

24+
# Match asterisks that trigger RST emphasis (preceded by delimiter like - or space)
25+
# but NOT asterisks already escaped or in code/literal contexts
26+
_RST_EMPHASIS_RE = re.compile(r"(?<=[^\s\\])-\*(?=[^\s*]|$)")
27+
28+
29+
def escape_rst_emphasis(text: str) -> str:
30+
r"""Escape asterisks that would trigger RST inline emphasis.
31+
32+
In reStructuredText, ``*text*`` creates emphasis. When argparse help text
33+
contains patterns like ``django-*``, the dash (a delimiter character) followed
34+
by asterisk triggers emphasis detection, causing warnings like:
35+
"Inline emphasis start-string without end-string."
36+
37+
This function escapes such asterisks with a backslash so they render literally.
38+
39+
Parameters
40+
----------
41+
text : str
42+
Text potentially containing problematic asterisks.
43+
44+
Returns
45+
-------
46+
str
47+
Text with asterisks escaped where needed.
48+
49+
Examples
50+
--------
51+
>>> escape_rst_emphasis('tmuxp load "my-*"')
52+
'tmuxp load "my-\\*"'
53+
>>> escape_rst_emphasis("plain text")
54+
'plain text'
55+
>>> escape_rst_emphasis("already \\* escaped")
56+
'already \\* escaped'
57+
>>> escape_rst_emphasis("*emphasis* is ok")
58+
'*emphasis* is ok'
59+
"""
60+
return _RST_EMPHASIS_RE.sub(r"-\*", text)
61+
2462

2563
def strip_ansi(text: str) -> str:
2664
r"""Remove ANSI escape codes from text.
@@ -640,6 +678,16 @@ def _reorder_nodes(processed: list[nodes.Node]) -> list[nodes.Node]:
640678
class CleanArgParseDirective(ArgParseDirective): # type: ignore[misc]
641679
"""ArgParse directive that strips ANSI codes and formats examples."""
642680

681+
def _nested_parse_paragraph(self, text: str) -> nodes.Node:
682+
"""Parse text as RST, escaping problematic characters first.
683+
684+
Overrides the parent class to escape asterisks in patterns like
685+
``session-*`` that would otherwise trigger RST emphasis warnings.
686+
"""
687+
escaped_text = escape_rst_emphasis(text)
688+
result: nodes.Node = super()._nested_parse_paragraph(escaped_text)
689+
return result
690+
643691
def run(self) -> list[nodes.Node]:
644692
"""Run the directive, clean output, format examples, and reorder."""
645693
result = super().run()

tests/docs/_ext/test_pretty_argparse.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
_is_examples_section,
1111
_is_usage_block,
1212
_reorder_nodes,
13+
escape_rst_emphasis,
1314
is_base_examples_term,
1415
is_examples_term,
1516
make_section_id,
@@ -83,6 +84,96 @@ def test_strip_ansi(test_id: str, input_text: str, expected: str) -> None:
8384
assert strip_ansi(input_text) == expected
8485

8586

87+
# --- escape_rst_emphasis tests ---
88+
89+
90+
class EscapeRstEmphasisFixture(t.NamedTuple):
91+
"""Test fixture for escape_rst_emphasis function."""
92+
93+
test_id: str
94+
input_text: str
95+
expected: str
96+
97+
98+
ESCAPE_RST_EMPHASIS_FIXTURES: list[EscapeRstEmphasisFixture] = [
99+
EscapeRstEmphasisFixture(
100+
test_id="glob_pattern_escaped",
101+
input_text='tmuxp load "django-*"',
102+
expected='tmuxp load "django-\\*"',
103+
),
104+
EscapeRstEmphasisFixture(
105+
test_id="multiple_glob_patterns",
106+
input_text='tmuxp load "flask-*" "django-*"',
107+
expected='tmuxp load "flask-\\*" "django-\\*"',
108+
),
109+
EscapeRstEmphasisFixture(
110+
test_id="plain_text_unchanged",
111+
input_text="tmuxp load",
112+
expected="tmuxp load",
113+
),
114+
EscapeRstEmphasisFixture(
115+
test_id="single_asterisk_unchanged",
116+
input_text="tmuxp load *",
117+
expected="tmuxp load *",
118+
),
119+
EscapeRstEmphasisFixture(
120+
test_id="emphasis_unchanged",
121+
input_text="*emphasis* text",
122+
expected="*emphasis* text",
123+
),
124+
EscapeRstEmphasisFixture(
125+
test_id="strong_unchanged",
126+
input_text="**strong** text",
127+
expected="**strong** text",
128+
),
129+
EscapeRstEmphasisFixture(
130+
test_id="already_escaped_unchanged",
131+
input_text='tmuxp load "django-\\*"',
132+
expected='tmuxp load "django-\\*"',
133+
),
134+
EscapeRstEmphasisFixture(
135+
test_id="hyphen_asterisk_space_unchanged",
136+
input_text="- * bullet",
137+
expected="- * bullet",
138+
),
139+
EscapeRstEmphasisFixture(
140+
test_id="glob_at_end_of_string",
141+
input_text='Filter by "flask-*',
142+
expected='Filter by "flask-\\*',
143+
),
144+
EscapeRstEmphasisFixture(
145+
test_id="underscore_asterisk_unchanged",
146+
input_text='Use pattern "my_*"',
147+
expected='Use pattern "my_*"',
148+
),
149+
EscapeRstEmphasisFixture(
150+
test_id="hyphen_escaped_underscore_unchanged",
151+
input_text='"a-*" or "b_*" patterns',
152+
expected='"a-\\*" or "b_*" patterns',
153+
),
154+
EscapeRstEmphasisFixture(
155+
test_id="empty_string",
156+
input_text="",
157+
expected="",
158+
),
159+
EscapeRstEmphasisFixture(
160+
test_id="asterisk_in_word_unchanged",
161+
input_text="multi*plied value",
162+
expected="multi*plied value",
163+
),
164+
]
165+
166+
167+
@pytest.mark.parametrize(
168+
EscapeRstEmphasisFixture._fields,
169+
ESCAPE_RST_EMPHASIS_FIXTURES,
170+
ids=[f.test_id for f in ESCAPE_RST_EMPHASIS_FIXTURES],
171+
)
172+
def test_escape_rst_emphasis(test_id: str, input_text: str, expected: str) -> None:
173+
"""Test RST emphasis escaping for glob patterns."""
174+
assert escape_rst_emphasis(input_text) == expected
175+
176+
86177
# --- is_examples_term tests ---
87178

88179

0 commit comments

Comments
 (0)