Keep the startup banners from crashing on a non-UTF-8 stdout (#3050)

This commit is contained in:
Nguyen Thanh Dat
2026-08-31 10:31:28 +02:00
committed by GitHub
parent 6cab6b9f98
commit 3df91b4d7a
2 changed files with 72 additions and 2 deletions
+23 -2
View File
@@ -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))
+49
View File
@@ -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}"
)