From 06f0c6eb92f217ad4c6bffd2e66cabe67b4d1ad0 Mon Sep 17 00:00:00 2001 From: revernomad17 Date: Thu, 9 Apr 2026 10:10:13 +0700 Subject: [PATCH] fix: guard against None stdout in fix_console_encoding for pythonw.exe When launched via pythonw.exe (no console), sys.stdout/stderr are None. Accessing .encoding on None raised AttributeError, caught by the GUI's pipeline import guard and shown as 'Failed to load pipeline'. Added None check before the encoding check in fix_console_encoding(), added a test, and documented the pitfall in CLAUDE.md. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 4 ++++ arma_modlist_tools/compat.py | 2 ++ test_suite.py | 13 +++++++++++++ 3 files changed, 19 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 7df6cf8..f71299e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -147,6 +147,10 @@ get_language() # → "vi" Minimum is Python **3.9**. All files that use `X | Y` union type annotations **must** have `from __future__ import annotations` as the first import. Without it, the `|` syntax raises `TypeError` at runtime on Python < 3.10. Every module in `arma_modlist_tools/` already has it; any new CLI script you add must include it too. +### `fix_console_encoding` — `None` stdout guard + +When the GUI is launched via `pythonw.exe` (no console window), Python sets `sys.stdout` and `sys.stderr` to `None`. `fix_console_encoding()` must check `if sys.stdout is None or sys.stderr is None: return` **before** accessing `.encoding`, otherwise it raises `AttributeError: 'NoneType' object has no attribute 'encoding'`. This error surfaces in the GUI as *"Failed to load pipeline"* because `run.py` calls `fix_console_encoding()` at module level and the exception is caught by the pipeline import guard. + ## Test Suite `test_suite.py` uses a custom harness (no pytest/unittest dependency). Structure: diff --git a/arma_modlist_tools/compat.py b/arma_modlist_tools/compat.py index 5aadb93..f7b8499 100644 --- a/arma_modlist_tools/compat.py +++ b/arma_modlist_tools/compat.py @@ -102,6 +102,8 @@ def fix_console_encoding() -> None: """ if not is_windows(): return + if sys.stdout is None or sys.stderr is None: + return if sys.stdout.encoding and sys.stdout.encoding.lower() == "utf-8": return sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") diff --git a/test_suite.py b/test_suite.py index dedea7c..b387397 100644 --- a/test_suite.py +++ b/test_suite.py @@ -2076,8 +2076,21 @@ test("compat: _read_os_release parses key=value pairs", _test_r test("compat: _read_os_release returns {} when file missing", _test_read_os_release_handles_missing_file) test("compat: _is_headless returns False when DISPLAY is set", _test_is_headless_with_display) test("compat: _is_headless returns True when no display env vars", _test_is_headless_without_display) +def _test_fix_console_encoding_none_stdout(): + """fix_console_encoding is a no-op when sys.stdout is None (pythonw.exe).""" + original_stdout = sys.stdout + try: + with _patch("arma_modlist_tools.compat.is_windows", return_value=True): + sys.stdout = None + _compat_mod.fix_console_encoding() # must not raise + assert sys.stdout is None + finally: + sys.stdout = original_stdout + + test("compat: fix_console_encoding is no-op on non-Windows", _test_fix_console_encoding_non_windows_noop) test("compat: fix_console_encoding skips when stdout already UTF-8", _test_fix_console_encoding_already_utf8) +test("compat: fix_console_encoding is no-op when stdout is None", _test_fix_console_encoding_none_stdout) # ---------------------------------------------------------------------------