Strip ANSI control codes when writing to the warnings file (#11624)

Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
This commit is contained in:
Bénédikt Tran 2023-09-21 11:01:07 +02:00 committed by GitHub
parent b935915c57
commit 5fe0bd41eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 51 additions and 3 deletions

View File

@ -20,6 +20,9 @@ Bugs fixed
Patch by Vinay Sajip. Patch by Vinay Sajip.
* #11622: Ensure that the order of keys in ``searchindex.js`` is deterministic. * #11622: Ensure that the order of keys in ``searchindex.js`` is deterministic.
Patch by Pietro Albini. Patch by Pietro Albini.
* #11617: ANSI control sequences are stripped from the output when writing to
a warnings file with :option:`-w <sphinx-build -w>`.
Patch by Bénédikt Tran.
Testing Testing
------- -------

View File

@ -195,6 +195,10 @@ Options
Write warnings (and errors) to the given file, in addition to standard error. Write warnings (and errors) to the given file, in addition to standard error.
.. versionchanged:: 7.3
ANSI control sequences are stripped when writing to *file*.
.. option:: -W .. option:: -W
Turn warnings into errors. This means that the build stops at the first Turn warnings into errors. This means that the build stops at the first

View File

@ -21,7 +21,7 @@ from sphinx import __display_version__
from sphinx.application import Sphinx from sphinx.application import Sphinx
from sphinx.errors import SphinxError, SphinxParallelError from sphinx.errors import SphinxError, SphinxParallelError
from sphinx.locale import __ from sphinx.locale import __
from sphinx.util import Tee from sphinx.util._io import TeeStripANSI
from sphinx.util.console import ( # type: ignore[attr-defined] from sphinx.util.console import ( # type: ignore[attr-defined]
color_terminal, color_terminal,
nocolor, nocolor,
@ -34,6 +34,11 @@ from sphinx.util.osutil import ensuredir
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Sequence from collections.abc import Sequence
from typing import Protocol
class SupportsWrite(Protocol):
def write(self, text: str, /) -> int | None:
...
def handle_exception( def handle_exception(
@ -266,11 +271,12 @@ def _parse_logging(
try: try:
warnfile = path.abspath(warnfile) warnfile = path.abspath(warnfile)
ensuredir(path.dirname(warnfile)) ensuredir(path.dirname(warnfile))
# the caller is responsible for closing this file descriptor
warnfp = open(warnfile, 'w', encoding="utf-8") # NoQA: SIM115 warnfp = open(warnfile, 'w', encoding="utf-8") # NoQA: SIM115
except Exception as exc: except Exception as exc:
parser.error(__('cannot open warning file %r: %s') % ( parser.error(__('cannot open warning file %r: %s') % (
warnfile, exc)) warnfile, exc))
warning = Tee(warning, warnfp) # type: ignore[assignment] warning = TeeStripANSI(warning, warnfp) # type: ignore[assignment]
error = warning error = warning
return status, warning, error, warnfp return status, warning, error, warnfp
@ -334,6 +340,10 @@ def build_main(argv: Sequence[str]) -> int:
except (Exception, KeyboardInterrupt) as exc: except (Exception, KeyboardInterrupt) as exc:
handle_exception(app, args, exc, args.error) handle_exception(app, args, exc, args.error)
return 2 return 2
finally:
if warnfp is not None:
# close the file descriptor for the warnings file opened by Sphinx
warnfp.close()
def _bug_report_info() -> int: def _bug_report_info() -> int:

View File

@ -1,3 +1,5 @@
from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from sphinx.util.console import _strip_escape_sequences from sphinx.util.console import _strip_escape_sequences
@ -6,7 +8,7 @@ if TYPE_CHECKING:
from typing import Protocol from typing import Protocol
class SupportsWrite(Protocol): class SupportsWrite(Protocol):
def write(self, text: str, /) -> None: def write(self, text: str, /) -> int | None:
... ...

View File

@ -2,11 +2,14 @@
import os import os
import shutil import shutil
from contextlib import contextmanager
from pathlib import Path
from unittest import mock from unittest import mock
import pytest import pytest
from docutils import nodes from docutils import nodes
from sphinx.cmd.build import build_main
from sphinx.errors import SphinxError from sphinx.errors import SphinxError
@ -133,3 +136,29 @@ def test_image_glob(app, status, warning):
assert doctree[0][3][0]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf', assert doctree[0][3][0]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf',
'image/svg+xml': 'subdir/svgimg.svg'} 'image/svg+xml': 'subdir/svgimg.svg'}
assert doctree[0][3][0]['uri'] == 'subdir/svgimg.*' assert doctree[0][3][0]['uri'] == 'subdir/svgimg.*'
@contextmanager
def force_colors():
forcecolor = os.environ.get('FORCE_COLOR', None)
try:
os.environ['FORCE_COLOR'] = '1'
yield
finally:
if forcecolor is None:
os.environ.pop('FORCE_COLOR', None)
else:
os.environ['FORCE_COLOR'] = forcecolor
def test_log_no_ansi_colors(tmp_path):
with force_colors():
wfile = tmp_path / 'warnings.txt'
srcdir = Path(__file__).parent / 'roots/test-nitpicky-warnings'
argv = list(map(str, ['-b', 'html', srcdir, tmp_path, '-n', '-w', wfile]))
retcode = build_main(argv)
assert retcode == 0
content = wfile.read_text(encoding='utf8')
assert '\x1b[91m' not in content