Skip to content

Commit c699c47

Browse files
nightcitybladenightcityblade
andauthored
fix: support legacy encodings in parallel reporter (#1160)
* fix: support legacy encodings in parallel reporter * fix: fall back for legacy-encoded spinners --------- Co-authored-by: nightcityblade <nightcityblade@gmail.com>
1 parent f85d050 commit c699c47

2 files changed

Lines changed: 51 additions & 2 deletions

File tree

nox/_parallel.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,19 @@
6767
from nox.sessions import SessionRunner
6868

6969
_SPINNER = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
70+
_ASCII_SPINNER = "|/-\\"
7071
_SYMBOLS = {
7172
Status.SUCCESS: "✓",
7273
Status.SKIPPED: "⊘",
7374
Status.FAILED: "✗",
7475
Status.ABORTED: "↯",
7576
}
77+
_ASCII_SYMBOLS = {
78+
Status.SUCCESS: "+",
79+
Status.SKIPPED: "-",
80+
Status.FAILED: "x",
81+
Status.ABORTED: "!",
82+
}
7683
_ANSI = re.compile(r"\x1b\[[0-9;?]*[a-zA-Z]")
7784

7885
# How long a child gets to exit after SIGTERM before it is SIGKILLed.
@@ -114,6 +121,24 @@ def _preview_text(line: str) -> str:
114121
return _ANSI.sub("", line.rstrip("\r\n").rsplit("\r", 1)[-1]).strip()
115122

116123

124+
def _status_symbol(status: Status, encoding: str | None) -> str:
125+
symbol = _SYMBOLS[status]
126+
try:
127+
symbol.encode(encoding or "utf-8")
128+
except UnicodeEncodeError:
129+
return _ASCII_SYMBOLS[status]
130+
return symbol
131+
132+
133+
def _spinner_frame(spin: int, encoding: str | None) -> str:
134+
frame = _SPINNER[spin % len(_SPINNER)]
135+
try:
136+
frame.encode(encoding or "utf-8")
137+
except UnicodeEncodeError:
138+
return _ASCII_SPINNER[spin % len(_ASCII_SPINNER)]
139+
return frame
140+
141+
117142
@dataclasses.dataclass(kw_only=True)
118143
class _Reporter:
119144
"""Buffers per-session output and renders progress.
@@ -191,7 +216,7 @@ def _render(self, now: float, width: int) -> list[str]:
191216
header = plain_header[: width - 1]
192217
lines = [self._banner(width), header]
193218

194-
frame = _SPINNER[self._spin % len(_SPINNER)]
219+
frame = _spinner_frame(self._spin, self.stream.encoding)
195220
for name, start in self._active.items():
196221
# Plain and colored renderings are built from the same segments so
197222
# the width math can't drift from what is actually displayed.
@@ -237,7 +262,7 @@ def _clear_board(self) -> None:
237262
self._board_lines = 0
238263

239264
def _emit_block(self, name: str, result: Result, output: str) -> None:
240-
symbol = _SYMBOLS[result.status]
265+
symbol = _status_symbol(result.status, self.stream.encoding)
241266
duration = _duration_str(result.duration, ", {time}")
242267
rule = "=" * 10
243268
self.stream.write(

tests/test_parallel.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from __future__ import annotations
1616

17+
import io
1718
import json
1819
import os
1920
import signal
@@ -612,6 +613,15 @@ def test_reporter_render_truncates_to_width() -> None:
612613
assert "\x1b" not in line
613614

614615

616+
def test_reporter_render_uses_ascii_spinner_for_legacy_encoding() -> None:
617+
buffer = io.BytesIO()
618+
with io.TextIOWrapper(buffer, encoding="cp1252") as stream:
619+
reporter = _parallel._Reporter(color=False, tty=True, total=1)
620+
reporter.stream = stream
621+
reporter._active = {"a": 100.0}
622+
assert reporter._render(105.0, width=0)[2] == "| a (5s)"
623+
624+
615625
def test_reporter_render_color() -> None:
616626
reporter = _parallel._Reporter(color=True, tty=False, total=1)
617627
reporter._active = {"a": 100.0}
@@ -711,6 +721,20 @@ def test_reporter_block_without_output_or_reason(
711721
assert "✓ x: success" in out
712722

713723

724+
def test_reporter_block_uses_ascii_symbol_for_legacy_encoding() -> None:
725+
buffer = io.BytesIO()
726+
with io.TextIOWrapper(buffer, encoding="cp1252") as stream:
727+
reporter = _parallel._Reporter(color=False, tty=False)
728+
reporter.stream = stream
729+
reporter._emit_block(
730+
"x",
731+
Result(_fake_runner(FakeSession("x")), Status.SUCCESS, duration=0),
732+
"",
733+
)
734+
stream.flush()
735+
assert "+ x: success" in buffer.getvalue().decode("cp1252")
736+
737+
714738
def test_run_session_spawns_subprocess(
715739
monkeypatch: pytest.MonkeyPatch,
716740
) -> None:

0 commit comments

Comments
 (0)