From 58e5712bd89abe9d7ab3f7223f97d6166109f917 Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Fri, 13 Nov 2026 04:57:33 -0400 Subject: [PATCH] fix: open audit log and tee/once files as UTF-8 These files were opened with the locale default encoding, so writing non-ASCII query output failed under non-UTF-8 locales (Windows cp1252, LANG=C). The config and history files already pass encoding='utf-8'. --- changelog.md | 8 ++++++++ mycli/client.py | 2 +- mycli/packages/special_commands/io_commands.py | 6 +++--- mycli_test/pytests/test_client.py | 1 + mycli_test/pytests/test_special_io_commands.py | 17 +++++++++++++++++ 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index 20e64d3b1..4e5608816 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,11 @@ +Upcoming (TBD) +============== + +Bug Fixes +-------- +* Open the audit log and the `\T`/`\o` output files as UTF-8, so non-ASCII output no longer fails under non-UTF-8 locales. + + v2.30.4 (2026/11/12) ============== diff --git a/mycli/client.py b/mycli/client.py index 5d5e4b8aa..8cb92e379 100644 --- a/mycli/client.py +++ b/mycli/client.py @@ -180,7 +180,7 @@ def __init__( # audit log if self.logfile is None and "audit_log" in c["main"]: try: - self.logfile = open(os.path.expanduser(c["main"]["audit_log"]), "a") + self.logfile = open(os.path.expanduser(c["main"]["audit_log"]), "a", encoding="utf-8") except (IOError, OSError): self.echo("Error: Unable to open the audit log file. Your queries will not be logged.", err=True, fg="red") self.logfile = False diff --git a/mycli/packages/special_commands/io_commands.py b/mycli/packages/special_commands/io_commands.py index 26c901428..0839797d1 100644 --- a/mycli/packages/special_commands/io_commands.py +++ b/mycli/packages/special_commands/io_commands.py @@ -899,7 +899,7 @@ def set_tee(arg: str, **_) -> list[SQLResult]: global tee_file try: - tee_file = open(*parseargfile(arg)) + tee_file = open(*parseargfile(arg), encoding='utf-8') except (IOError, OSError) as e: raise OSError(f"Cannot write to file '{e.filename}': {e.strerror}") from e @@ -949,7 +949,7 @@ def _set_once_file(filename: str, mode: str) -> list[SQLResult]: global once_file, written_to_once_file try: - once_file = open(os.path.expanduser(filename), mode) + once_file = open(os.path.expanduser(filename), mode, encoding='utf-8') except (IOError, OSError) as e: raise OSError(f"Cannot write to file '{e.filename}': {e.strerror}") from e written_to_once_file = False @@ -1096,7 +1096,7 @@ def flush_pipe_once_if_written( returncode = PIPE_ONCE['process'].returncode if PIPE_ONCE['stdout_file']: if not timed_out and not returncode and (stdout_data or force): - with open(PIPE_ONCE['stdout_file'], PIPE_ONCE['stdout_mode']) as f: + with open(PIPE_ONCE['stdout_file'], PIPE_ONCE['stdout_mode'], encoding='utf-8') as f: if stdout_data: print(stdout_data, file=f) _run_post_redirect_hook(post_redirect_command, PIPE_ONCE['stdout_file']) diff --git a/mycli_test/pytests/test_client.py b/mycli_test/pytests/test_client.py index ba606066e..b695e9da3 100644 --- a/mycli_test/pytests/test_client.py +++ b/mycli_test/pytests/test_client.py @@ -423,6 +423,7 @@ def test_init_opens_audit_log_from_config(monkeypatch: pytest.MonkeyPatch, tmp_p try: assert isinstance(cli.logfile, TextIOWrapper) assert Path(cli.logfile.name) == audit_log + assert cli.logfile.encoding.lower().replace('_', '-') == 'utf-8' finally: if isinstance(cli.logfile, TextIOWrapper): cli.logfile.close() diff --git a/mycli_test/pytests/test_special_io_commands.py b/mycli_test/pytests/test_special_io_commands.py index 8c68b9121..b68c88c73 100644 --- a/mycli_test/pytests/test_special_io_commands.py +++ b/mycli_test/pytests/test_special_io_commands.py @@ -274,6 +274,23 @@ def test_tee_command(): print(f"An error occurred while attempting to delete the file: {e}") +def test_tee_and_once_files_are_opened_as_utf8(tmp_path: Path) -> None: + tee_path = tmp_path / 'tee.out' + once_path = tmp_path / 'once.out' + try: + mycli.packages.special_commands.execute(None, f'tee {tee_path}') + assert io_commands.tee_file.encoding.lower().replace('_', '-') == 'utf-8' + io_commands.write_tee('日本語 🙂') + mycli.packages.special_commands.execute(None, 'notee') + + io_commands._set_once_file(str(once_path), 'w') + assert io_commands.once_file.encoding.lower().replace('_', '-') == 'utf-8' + finally: + io_commands.unset_once_if_written('') + io_commands.close_tee() + assert '日本語 🙂' in tee_path.read_text(encoding='utf-8') + + @pytest.mark.skipif('wsl2' in platform.uname().release.lower(), reason='todo: unknown') def test_tee_command_error(): with pytest.raises(TypeError):