Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -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)
==============

Expand Down
2 changes: 1 addition & 1 deletion mycli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions mycli/packages/special_commands/io_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'])
Expand Down
1 change: 1 addition & 0 deletions mycli_test/pytests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
17 changes: 17 additions & 0 deletions mycli_test/pytests/test_special_io_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading