diff --git a/core/log.py b/core/log.py index ac9f7f3c..1f78b7e5 100644 --- a/core/log.py +++ b/core/log.py @@ -120,6 +120,19 @@ def _paint(level: str, text: str) -> str: return f"{code}{text}\x1b[0m" if code and _colour_wanted() else text +def _say(text: str) -> None: + """Print to the console, never raising. A console that is not UTF-8 (a rental's ASCII locale, a redirected pipe) + cannot print a Japanese file name; that must cost the characters, not the generation that was only mentioning it.""" + try: + print(text, file=sys.stderr, flush=True) + except UnicodeEncodeError: + enc = getattr(sys.stderr, "encoding", None) or "ascii" + try: + print(text.encode(enc, errors="replace").decode(enc, errors="replace"), file=sys.stderr, flush=True) + except Exception: # noqa: BLE001 -- the Log window still has the record + pass + + def record(level: str, source: str, message: str) -> dict: # Imported here rather than at module level: `run` starts a run by clearing # what this module holds, so a top-level import would be circular. @@ -128,7 +141,7 @@ def record(level: str, source: str, message: str) -> dict: "message": message, "run": run_mod.current()} _records.append(entry) _keep(entry) - print(_paint(level, line(level, source, message)), file=sys.stderr, flush=True) + _say(_paint(level, line(level, source, message))) return entry diff --git a/core/tests/test_log.py b/core/tests/test_log.py index 08c1ae31..1affa1c7 100644 --- a/core/tests/test_log.py +++ b/core/tests/test_log.py @@ -227,3 +227,12 @@ def test_colour_codes_never_reach_the_panel(monkeypatch): monkeypatch.setattr(backend_log, "_memory", lambda: deque([{"m": painted + "\n"}])) answer = backend_log.recent() assert answer["lines"] == ["[Log] Warning: x | y"] and answer["tones"] == ["warn"] + + +def test_a_console_that_cannot_encode_a_name_does_not_raise(monkeypatch): + import io + import sys + from core import log + monkeypatch.setattr(sys, "stderr", io.TextIOWrapper(io.BytesIO(), encoding="ascii")) + entry = log.info("FunUI Load Media", "loaded 日本語.mp4") + assert "日本語" in entry["message"] # the Log window keeps the real characters diff --git a/modules/media/load/tests/test_load_media.py b/modules/media/load/tests/test_load_media.py index 1efb64b2..e362d949 100644 --- a/modules/media/load/tests/test_load_media.py +++ b/modules/media/load/tests/test_load_media.py @@ -164,3 +164,16 @@ def test_a_cut_that_leaves_nothing_is_explained_and_a_picture_input_reads_one_fr FunUILoadMedia.execute(mid, full=True, prepare=json.dumps({"skip_first": 100})).result image, _, _ = FunUILoadMedia.execute(mid).result assert image.shape == (1, 48, 64, 3) + + +def test_a_japanese_file_name_loads_even_when_the_console_cannot_print_it(store, monkeypatch): + """A rental whose console is ASCII must not fail a generation because a clip is called 日本語.mp4.""" + import io + import sys + from modules.media.load.nodes import FunUILoadMedia + mid = _long_clip(store) + media.rename(mid, "日本語の動画 カタカナ.mp4") + console = io.TextIOWrapper(io.BytesIO(), encoding="ascii") + monkeypatch.setattr(sys, "stderr", console) + image, frames, _ = FunUILoadMedia.execute(mid, full=True, prepare='{"frame_cap": 3}').result + assert frames.shape[0] == 3 and image.shape[0] == 1