From 3df91b4d7a7e2c35b4093767f9c7c2a618c0045c Mon Sep 17 00:00:00 2001 From: Nguyen Thanh Dat Date: Mon, 31 Aug 2026 15:31:28 +0700 Subject: [PATCH] Keep the startup banners from crashing on a non-UTF-8 stdout (#3050) --- maigret/notify.py | 25 ++++++++++++++++++++-- tests/test_notify.py | 49 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/maigret/notify.py b/maigret/notify.py index 43bd3315..e1672474 100644 --- a/maigret/notify.py +++ b/maigret/notify.py @@ -271,6 +271,27 @@ PATREON_URL = "https://www.patreon.com/soxoj" INTRO_TEXT = "MAIGRET - collect a dossier by username from 3000+ sites" +def _print_encodable(text: str) -> None: + """Print text the active stdout encoding may not be able to represent. + + On Windows, Python takes stdout's encoding from the process ANSI + codepage, which is cp1252 on a default install and has no U+2665. The + banners below carry one, so printing them raised UnicodeEncodeError + before a single site was checked, and --no-color did not help because + the character sits in that branch too. PYTHONIOENCODING cannot rescue + the PyInstaller build, which ignores PYTHON* environment variables. + + Catching the write rather than reconfiguring the stream is deliberate: + colorama's init() replaces sys.stdout with a wrapper that has no + reconfigure(), so a stream-level fix would depend on running first. + """ + try: + print(text) + except UnicodeEncodeError: + encoding = getattr(sys.stdout, "encoding", None) or "ascii" + print(text.encode(encoding, errors="replace").decode(encoding, errors="replace")) + + def _format_intro(use_color: bool) -> str: if not use_color: return f"[+] {INTRO_TEXT}" @@ -283,7 +304,7 @@ def print_intro_banner(no_color: bool = False, silent: bool = False) -> None: """Print the Maigret intro tagline. Skipped only in silent (--ai) mode.""" if silent: return - print(_format_intro(use_color=not no_color)) + _print_encodable(_format_intro(use_color=not no_color)) def _format_donate_banner(use_color: bool) -> str: @@ -304,4 +325,4 @@ def print_donate_banner(no_color: bool = False, silent: bool = False) -> None: """Print a colored donation banner. Skipped only in silent (--ai) mode.""" if silent: return - print(_format_donate_banner(use_color=not no_color)) + _print_encodable(_format_donate_banner(use_color=not no_color)) diff --git a/tests/test_notify.py b/tests/test_notify.py index a4c6b993..8824895f 100644 --- a/tests/test_notify.py +++ b/tests/test_notify.py @@ -204,3 +204,52 @@ def test_update_shows_extractor_field_when_verbose(monkeypatch): out = "\n".join(captured) assert "uid: 42" in out assert "_extractor: SomeSchemeAPI" in out + + +def test_banners_survive_a_stdout_that_cannot_encode_them(tmp_path): + """The banners carry '♥', which the Windows ANSI codepage (cp1252 on a + default install) cannot encode, so printing them raised UnicodeEncodeError + before a single site was checked. --no-color did not help: the character is + in that branch too, and the PyInstaller build ignores PYTHONIOENCODING, so + there was no way around it from outside. + + Run the banners in a child process whose stdout really is cp1252 rather + than mocking the failure, so this fails on any machine if the guard goes. + """ + import os + import subprocess + import sys + import textwrap + + probe = tmp_path / "banner_probe.py" + probe.write_text( + textwrap.dedent( + """ + from maigret.notify import print_intro_banner, print_donate_banner + + print_intro_banner(no_color=True) + print_donate_banner(no_color=True) + print_intro_banner(no_color=False) + print_donate_banner(no_color=False) + """ + ), + encoding="utf-8", + ) + + env = dict(os.environ) + env["PYTHONIOENCODING"] = "cp1252" + result = subprocess.run( + [sys.executable, str(probe)], + capture_output=True, + encoding="cp1252", + errors="replace", + env=env, + cwd=str(tmp_path), + ) + + assert result.returncode == 0, ( + f"the banner must not crash the run: stderr={result.stderr!r}" + ) + assert "Support Maigret" in result.stdout, ( + f"the banner must still be printed: stdout={result.stdout!r}" + )