Skip to content

Commit 786fd11

Browse files
authored
🐛 fix: strip ANSI colors from usage under PYTHON_COLORS=1 (#356)
On Python 3.14 argparse colors its usage output, and `_colorize.can_colorize` consults `PYTHON_COLORS` before `NO_COLOR`. A doc build under `PYTHON_COLORS=1` therefore ignored the `NO_COLOR=1` the directive sets, and every usage block came out as `usage: ^[[0m^[[1;35mprog^[[0m ...`; the `usage: ` prefix sat inside escape codes as well, so the 7-character slice removed the wrong text. 🎨 The environment patch around `format_usage` now sets `PYTHON_COLORS=0` alongside `NO_COLOR=1`, which is the combination the interpreter treats as plain output regardless of the caller's shell.
1 parent 6bd79f0 commit 786fd11

6 files changed

Lines changed: 56 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
1212
- Leave apostrophes inside words (`don't`, `it's`) alone in help text instead of turning them into broken inline
1313
literals.
1414
- Make `:hook:` intercept `parse_intermixed_args()` as well as `parse_args()`.
15+
- Keep ANSI color codes out of usage blocks when `PYTHON_COLORS=1` is set on Python 3.14 or newer.
1516

1617
## 1.13.1
1718

roots/test-python-colors/conf.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import sys
5+
from pathlib import Path
6+
7+
sys.path.insert(0, str(Path(__file__).parent))
8+
os.environ["PYTHON_COLORS"] = "1"
9+
extensions = ["sphinx_argparse_cli"]
10+
nitpicky = True

roots/test-python-colors/index.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.. sphinx_argparse_cli::
2+
:module: parser
3+
:func: make

roots/test-python-colors/parser.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from __future__ import annotations
2+
3+
from argparse import ArgumentParser
4+
5+
6+
def make() -> ArgumentParser:
7+
parser = ArgumentParser(prog="prog", add_help=False)
8+
parser.add_argument("--flag", help="a flag")
9+
parser.add_subparsers().add_parser("run", add_help=False).add_argument("--magic", help="magic")
10+
return parser

src/sphinx_argparse_cli/_logic.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,8 @@ def _mk_usage(self, parser: ArgumentParser) -> literal_block:
399399

400400
@contextmanager
401401
def _no_color(self) -> Iterator[None]:
402-
with patch.dict(os.environ, {"NO_COLOR": "1"}, clear=False):
402+
# PYTHON_COLORS=1 outranks NO_COLOR in argparse, so both must be set to get plain text
403+
with patch.dict(os.environ, {"NO_COLOR": "1", "PYTHON_COLORS": "0"}, clear=False):
403404
yield
404405

405406

tests/test_logic.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,36 @@ def test_option_role_as_html(build_outcome: str, warning: StringIO) -> None:
273273
assert not warning.getvalue()
274274

275275

276+
@pytest.mark.sphinx(buildername="text", testroot="python-colors")
277+
def test_usage_ignores_python_colors(build_outcome: str) -> None:
278+
assert (
279+
build_outcome
280+
== """prog - CLI interface
281+
********************
282+
283+
prog [--flag FLAG] {run} ...
284+
285+
286+
prog options
287+
============
288+
289+
* **"--flag"** "FLAG" - a flag
290+
291+
292+
prog run
293+
========
294+
295+
prog run [--magic MAGIC]
296+
297+
298+
prog run options
299+
----------------
300+
301+
* **"--magic"** "MAGIC" - magic
302+
"""
303+
)
304+
305+
276306
@pytest.mark.sphinx(buildername="text", testroot="ref-duplicate-label")
277307
def test_ref_duplicate_label(build_outcome: tuple[str, str], warning: StringIO) -> None:
278308
assert build_outcome

0 commit comments

Comments
 (0)