Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions parallel_web_tools/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,25 @@
# CLI extras not installed (pyyaml, questionary)
pass


def _make_output_streams_unicode_safe() -> None:
"""Prevent UnicodeEncodeError crashes on legacy consoles (e.g. Windows cp1252).

Help text and other CLI output can contain non-ASCII characters (arrows,
box-drawing characters). On consoles using legacy code pages these cannot
be encoded, which would crash instead of printing. Reconfigure the streams
so unencodable characters are replaced with escape sequences instead.
"""
for stream in (sys.stdout, sys.stderr):
if stream is not None and hasattr(stream, "reconfigure"):
try:
stream.reconfigure(errors="backslashreplace")
except Exception: # noqa: BLE001 - never fail startup over this
pass


_make_output_streams_unicode_safe()

logging.basicConfig(format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)

Expand Down
21 changes: 21 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2984,3 +2984,24 @@ def test_service_api_error_exits_api_error(self, runner):

assert result.exit_code == EXIT_API_ERROR
assert "Balance API error" in result.output


class TestUnicodeSafeStreams:
"""Output streams must not crash on unencodable characters (e.g. Windows cp1252)."""

def test_helper_replaces_unencodable_characters(self):
import io

from parallel_web_tools.cli.commands import _make_output_streams_unicode_safe

raw = io.BytesIO()
stream = io.TextIOWrapper(raw, encoding="cp1252", errors="strict")
with mock.patch("sys.stdout", stream):
# Before the fix this raises UnicodeEncodeError on cp1252.
_make_output_streams_unicode_safe()
print("arrow -> \u2192")
stream.flush()

output = raw.getvalue().decode("cp1252")
assert "\u2192" not in output # cannot be encoded in cp1252
assert "arrow" in output # text itself still printed