diff --git a/parallel_web_tools/cli/commands.py b/parallel_web_tools/cli/commands.py index 0e4eabe..542937c 100644 --- a/parallel_web_tools/cli/commands.py +++ b/parallel_web_tools/cli/commands.py @@ -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__) diff --git a/tests/test_cli.py b/tests/test_cli.py index b4c37f2..2def037 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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