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) # ---------------------------------------------------------------------------