Skip to content

Commit

Permalink
Strip ANSI control codes when writing to the warnings file (#11624)
Browse files Browse the repository at this point in the history
Co-authored-by: Adam Turner <[email protected]>
  • Loading branch information
picnixz and AA-Turner authored Sep 21, 2023
1 parent b935915 commit 5fe0bd4
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 3 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ Bugs fixed
Patch by Vinay Sajip.
* #11622: Ensure that the order of keys in ``searchindex.js`` is deterministic.
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
-------
Expand Down
4 changes: 4 additions & 0 deletions doc/man/sphinx-build.rst
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ Options

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

Turn warnings into errors. This means that the build stops at the first
Expand Down
14 changes: 12 additions & 2 deletions sphinx/cmd/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from sphinx.application import Sphinx
from sphinx.errors import SphinxError, SphinxParallelError
from sphinx.locale import __
from sphinx.util import Tee
from sphinx.util._io import TeeStripANSI
from sphinx.util.console import ( # type: ignore[attr-defined]
color_terminal,
nocolor,
Expand All @@ -34,6 +34,11 @@

if TYPE_CHECKING:
from collections.abc import Sequence
from typing import Protocol

class SupportsWrite(Protocol):
def write(self, text: str, /) -> int | None:
...


def handle_exception(
Expand Down Expand Up @@ -266,11 +271,12 @@ def _parse_logging(
try:
warnfile = path.abspath(warnfile)
ensuredir(path.dirname(warnfile))
# the caller is responsible for closing this file descriptor
warnfp = open(warnfile, 'w', encoding="utf-8") # NoQA: SIM115
except Exception as exc:
parser.error(__('cannot open warning file %r: %s') % (
warnfile, exc))
warning = Tee(warning, warnfp) # type: ignore[assignment]
warning = TeeStripANSI(warning, warnfp) # type: ignore[assignment]
error = warning

return status, warning, error, warnfp
Expand Down Expand Up @@ -334,6 +340,10 @@ def build_main(argv: Sequence[str]) -> int:
except (Exception, KeyboardInterrupt) as exc:
handle_exception(app, args, exc, args.error)
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:
Expand Down
4 changes: 3 additions & 1 deletion sphinx/util/_io.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from sphinx.util.console import _strip_escape_sequences
Expand All @@ -6,7 +8,7 @@
from typing import Protocol

class SupportsWrite(Protocol):
def write(self, text: str, /) -> None:
def write(self, text: str, /) -> int | None:
...


Expand Down
29 changes: 29 additions & 0 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import os
import shutil
from contextlib import contextmanager
from pathlib import Path
from unittest import mock

import pytest
from docutils import nodes

from sphinx.cmd.build import build_main
from sphinx.errors import SphinxError


Expand Down Expand Up @@ -133,3 +136,29 @@ def test_image_glob(app, status, warning):
assert doctree[0][3][0]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf',
'image/svg+xml': 'subdir/svgimg.svg'}
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

0 comments on commit 5fe0bd4

Please sign in to comment.