devops_utils.core.encoding module
Make CLI output survive non-UTF-8 stdio (Windows cp1252 in particular).
Work-item titles, previews, and this CLI’s own messages contain non-ASCII
characters (→, —). On Windows a redirected stdout defaults to the
ANSI code page (cp1252), where writing one of those raises
UnicodeEncodeError and kills the command mid-write:
UnicodeEncodeError: 'charmap' codec can't encode character '\u2190'
configure_stdio() upgrades such streams to UTF-8 and, whatever the
encoding ends up being, switches the error handler to backslashreplace so
an unencodable character degrades to an escape sequence instead of an
exception.
It is called from every entry point — the devops-utils console script (from
its main wrapper, before Click parses argv, so eager options like
--help are covered too), the root Click group (covering python -m and
embedded use), and the devops-utils-mcp server via configure_stderr().
The point is that none of this depends on the caller exporting PYTHONUTF8 /
PYTHONIOENCODING first.
stdout is the stream that matters: CPython already defaults sys.stderr to
backslashreplace, so Click’s own error output was never the crash path.
Note that chcp 65001 was never part of the fix: since PEP 528 a process
attached to a real Windows console writes through WriteConsoleW whatever
the code page is. The crash only ever happened on a redirected stream,
which falls back to the ANSI code page.
- devops_utils.core.encoding.ERRORS = 'backslashreplace'
never raise, escape what can’t be encoded (
←) so the text stays readable and diagnosable.- Type:
Error handler used for all CLI output
- devops_utils.core.encoding.configure_stderr() None[source]
Harden
sys.stderronly — for hosts that own stdout.The MCP stdio transport frames JSON-RPC over stdout and stdin; those must stay byte-exact UTF-8, and
backslashreplaceat the stream layer would silently mangle a frame rather than let the serializer escape it. Its log channel is stderr, though, which is exposed to the same cp1252 crash as the CLI’s.
- devops_utils.core.encoding.configure_stdio() None[source]
Apply UTF-8 +
backslashreplacetosys.stdoutandsys.stderr.Idempotent, so it is safe to call from more than one entry point.
stdinis deliberately left alone: the only readers areclick.confirmanddevops_utils.core.io.input_confirm(), both of which takey/n. A future “read the description from stdin” feature should reconfigure at its own call site rather than globally.