Skip to content

fix(cli): set UTF-8 stdout in the console entry point - #2096

Merged
jmartin-tech merged 3 commits into
NVIDIA:mainfrom
MohammedAlkindi:fix/console-entrypoint-utf8-stdout
Sep 3, 2026
Merged

fix(cli): set UTF-8 stdout in the console entry point#2096
jmartin-tech merged 3 commits into
NVIDIA:mainfrom
MohammedAlkindi:fix/console-entrypoint-utf8-stdout

Conversation

@MohammedAlkindi

Copy link
Copy Markdown
Contributor

On Windows, garak <anything> > file.txt silently truncates its output at the first emoji and still exits 0.

[project.scripts] declares the console script as garak.__main__:main, so it calls main() directly and never runs the if __name__ == "__main__" block — which is where sys.stdout.reconfigure(encoding="utf-8") lived. python -m garak does run that block, so the two entry points behave differently. When stdout is a pipe or a file, Python falls back to the ANSI code page (cp1252 here, cp936/cp932 elsewhere), and the first 🌟 in the plugin listing kills the write.

CI does not see this because test_windows.yml sets PYTHONUTF8: 1 and PYTHONIOENCODING: utf-8. Those are not mentioned anywhere in the README or docs, so users do not have them set.

This moves the one line into main() so both entry points behave the same. It follows what garak/analyze/* already does — 11 of those scripts call sys.stdout.reconfigure(encoding="utf-8") themselves.

Reproduction

Windows 11, Python 3.13.13, clean venv, no PYTHONUTF8 / PYTHONIOENCODING set.

Before:

> garak --list_probes > probes.txt        # exit code 0, stderr empty
> wc -l probes.txt
2

probes.txt ends mid-line, with the codec error written into stdout rather than raised:

garak LLM vulnerability scanner v0.16.1.pre1 ( https://github.com/NVIDIA/garak ) at 2026-08-21T03:21:42
probes: adaptive_attacks'charmap' codec can't encode character '\U0001f31f' in position 1: character maps to <undefined>

After: 234 lines, 560 non-ASCII bytes, no codec error — byte-identical to what python -m garak --list_probes already produced.

Verification

  • python -m pytest tests/test_main_entrypoint.py — 1 passed, and fails on main with UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f31f'
  • python -m pytest tests/cli tests/test_config.py tests/test_configurable.py — 119 passed
  • python -m pytest tests/cli tests/test_config.py tests/test_main_entrypoint.py — 110 passed, new test passes in company
  • New test ran 3× individually, passed each time
  • Verify the thing does what it should — console script now writes all 234 lines with emoji intact
  • Verify the thing does not do what it should not — python -m garak --list_probes output is unchanged; the reconfigure just moved, so the -m path still gets it exactly once

The new test forces PYTHONIOENCODING=cp1252 in a subprocess, so it reproduces the failure on any OS rather than only on Windows.

I did not run the full tests/ suite — several suites pull models and remote resources that I could not fetch here. I ran the CLI and config suites, which are the ones this change can affect.

Not a duplicate

I checked open PRs for this symptom (reconfigure, PYTHONUTF8, PYTHONIOENCODING, charmap, UnicodeEncodeError) and found none. #1790 does touch garak/__main__.py, but it wraps the call as sys.exit(cli.main(...)) for exit codes and does not change the encoding behaviour — a different defect, and adjacent lines in the same small function, so the two may need sequencing.

Worth noting the exit-code angle separately: the failure here surfaced as exit 0 with the error text on stdout, which is arguably its own bug. I have deliberately left that alone to keep this PR to one concern.

AI assistance

AI assistance was used for this change. I reviewed every changed line and ran the test commands listed above myself; the before/after output quoted here is from my own machine.

One deviation from AGENTS.md to flag rather than leave silent: I have not added a Co-authored-by: commit trailer. My personal attribution policy is that commits carry only my identity and AI involvement is disclosed in prose, as it is here. Happy to add the trailer if you would rather the disclosure be in the commit metadata too.

The garak console script is declared as garak.__main__:main, so it calls main()
directly and never runs the `if __name__ == "__main__"` block that reconfigured
stdout to UTF-8. Redirected or piped output then truncated at the first emoji.

Signed-off-by: Mohammed Alkindi <alkndymhmd692@gmail.com>
@leondz

leondz commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

thanks, this seems reasonable. will take a look.

@jmartin-tech jmartin-tech left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the reasoning for this, since this is in __main__.py I think it is reasonable

I can see a concern with changing sys.stdout configuration in ways that might be triggered during library based import as an unexpected side-effect for the caller, though I think that is highly unlikely for this particular entry point as access to sys.argv should clearly signal to an integrator that this entry point should not be called directly by a wrapper.

Due to a recent upstream dependency lock need please rebase this PR on commit 3845757 or newer from main to enable CI/CD execution to pass.

Signed-off-by: Mohammed Alkindi <alkndymhmd692@gmail.com>
@MohammedAlkindi

Copy link
Copy Markdown
Contributor Author

Picked up 3845757, though as a merge rather than a rebase — force-pushes are blocked in my setup, so the branch carries it as a merge commit instead of linear history. Say the word if you need it rebased properly and I'll sort that out.

On the stdout concern: the reconfigure now sits inside main() rather than at module scope, so importing garak, garak.cli or even garak.__main__ never triggers it — only actually calling the entry point does.

@MohammedAlkindi

Copy link
Copy Markdown
Contributor Author

CI is green and the rebase request is satisfied. The branch contains 3845757 (compare/3845757...fb453ca reports behind_by: 0), and all 17 checks on fb453ca pass, DCO included.

On the import side-effect: the reconfigure call sits inside main(), so import garak.__main__ does not run it. Only invoking main() does, and main() already consumes sys.argv, which is the signal you described. Nothing runs at module scope either before or after this change.

It landed as a merge commit rather than a rebase because force-pushing is blocked on my side. If you want linear history before this merges, say so and I will send a replacement PR from a fresh branch.

@jmartin-tech jmartin-tech self-assigned this Sep 1, 2026
@jmartin-tech
jmartin-tech merged commit 4d1b228 into NVIDIA:main Sep 3, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants