|
27 | 27 |
|
28 | 28 | import os |
29 | 29 | import re |
| 30 | +from collections.abc import Iterator |
30 | 31 | from configparser import RawConfigParser |
| 32 | +from contextlib import contextmanager |
31 | 33 | from dataclasses import dataclass, field, fields |
32 | 34 | from enum import Enum |
33 | 35 | from functools import lru_cache |
@@ -120,6 +122,45 @@ def _nearest_256(r: int, g: int, b: int) -> int: |
120 | 122 | """ |
121 | 123 |
|
122 | 124 |
|
| 125 | +@contextmanager |
| 126 | +def forced_color() -> Iterator[None]: |
| 127 | + """Force ANSI color while Click Extra captures CLI text for documentation. |
| 128 | +
|
| 129 | + Click Extra renders CLI help and output into docs from both the MkDocs plugin |
| 130 | + (:mod:`click_extra.mkdocs`) and the Sphinx directives |
| 131 | + (:mod:`click_extra.sphinx.click`). During a build that output is a pipe, not a TTY, |
| 132 | + so the underlying renderers strip their escape codes. Two independent color systems |
| 133 | + have to be defeated: |
| 134 | +
|
| 135 | + - Click's, gated by ``should_strip_ansi`` / ``ctx.color`` (what ``click.echo`` and |
| 136 | + the Click and Click Extra help formatters consult). Sphinx's runner additionally |
| 137 | + flips this one with ``click.testing.CliRunner(color=True)``. |
| 138 | + - Rich's, gated by ``rich.console.Console.is_terminal``, which ignores the above and |
| 139 | + reads ``FORCE_COLOR`` (https://force-color.org). This is the system ``rich-click`` |
| 140 | + uses, and ``color=True`` never reaches it. |
| 141 | +
|
| 142 | + ``FORCE_COLOR`` is the only signal common to both systems (Rich reads it directly; |
| 143 | + Click Extra recognizes it through :data:`color_envvars`), so it is the lever we set |
| 144 | + here. We also clear the color-disabling variables Click Extra recognizes |
| 145 | + (``NO_COLOR``, ``LLM``, …) so an opt-out in the build environment cannot suppress the |
| 146 | + rendering. The previous environment is restored on exit, so the override never leaks |
| 147 | + beyond a single capture. |
| 148 | + """ |
| 149 | + disabling = [var for var, enables in color_envvars.items() if not enables] |
| 150 | + saved = {var: os.environ.get(var) for var in ("FORCE_COLOR", *disabling)} |
| 151 | + os.environ["FORCE_COLOR"] = "1" |
| 152 | + for var in disabling: |
| 153 | + os.environ.pop(var, None) |
| 154 | + try: |
| 155 | + yield |
| 156 | + finally: |
| 157 | + for var, value in saved.items(): |
| 158 | + if value is None: |
| 159 | + os.environ.pop(var, None) |
| 160 | + else: |
| 161 | + os.environ[var] = value |
| 162 | + |
| 163 | + |
123 | 164 | class ColorOption(ExtraOption): |
124 | 165 | """A pre-configured option that is adding a ``--color``/``--no-color`` (aliased by |
125 | 166 | ``--ansi``/``--no-ansi``) to keep or strip colors and ANSI codes from CLI output. |
|
0 commit comments