Read JSON and write XML as UTF-8 regardless of locale - #655
Read JSON and write XML as UTF-8 regardless of locale#655davidpavlovschi wants to merge 2 commits into
Conversation
Fixes elapouya#516 docxtpl opened the only two text files it handles without an explicit encoding, so both fell back to locale.getpreferredencoding(False) : - docxtpl/__main__.py : `python -m docxtpl` decoded the json data with the locale code page. Under cp1252 the UTF-8 bytes silently become mojibake in the generated docx, which is what elapouya#516 reports ; under cp936 or a C locale they raise UnicodeDecodeError instead. JSON is UTF-8 (RFC 8259), so the encoding is not a guess. "utf-8-sig" is used rather than "utf-8" to also skip the BOM that PowerShell and Windows editors write. A file that really is not UTF-8 is now reported like any other bad input instead of escaping the command line error handling. - docxtpl/template.py : write_xml() encoded the document xml with the same locale code page and raised UnicodeEncodeError as soon as the document contained a character the code page cannot represent (any CJK or Cyrillic text under cp1252, any non-ASCII text under a C locale). This is also why the bug looked unreproducible : on Linux and macOS the default encoding already is UTF-8. tests/utf8_locale.py re-runs itself in a child interpreter forced off UTF-8 (PYTHONUTF8=0 + C locale) so the regression reproduces on Linux and macOS too. Reverting either fix on its own makes it fail.
JackSpiece
left a comment
There was a problem hiding this comment.
The production cases pass for me, including the forced non-UTF locale under Python 3.8, and all 37 scripts pass under Python 3.12. I found one supported-version problem in the new test harness.
From the repository root, PYTHONPATH=. python3.8 tests/utf8_locale.py fails before starting the child interpreter with FileNotFoundError: .../tests/tests. Python 3.8 keeps __file__ relative here. The main block changes into tests, then rerun_with_non_utf8_locale() recomputes abspath(__file__) from that new directory and adds a second tests component. The same command passes under Python 3.12 because __file__ is absolute there.
Please capture the absolute tests directory once before changing directories and reuse it for both os.chdir() and the child cwd, or otherwise avoid recomputing it after the directory change. Python 3.7 and 3.8 are both listed as supported, so the regression script should work from this normal invocation on those versions too.
tests/utf8_locale.py computed os.path.dirname(os.path.abspath(__file__)) twice : once to chdir into tests, then again inside rerun_with_non_utf8_locale() to give the child its cwd. Up to Python 3.8 __file__ stays relative to the directory the interpreter was started from, so the second call resolved against the new directory and produced tests/tests. `PYTHONPATH=. python3.8 tests/utf8_locale.py` from the repository root died with FileNotFoundError before the child even started, while Python 3.9+ passed because __file__ is absolute there. Resolve the directory and the script name once, at import time, and reuse them for both os.chdir() and the child cwd, so neither depends on the current directory any more. The PYTHONPATH handed to the child had the same problem : the child runs in the tests directory, so a relative entry such as "." no longer pointed at the repository root and docxtpl was not importable. Make its entries absolute against the startup directory.
|
Thanks for the careful review, and for pinning down the exact mechanism — that made it quick to reproduce. Fixed in 1be323e. The tests directory and the script name are now resolved once, at import time, into Chasing your invocation turned up a second instance of the same relative-path problem, one step further along. The child inherits Verified locally on CPython 3.8.20, 3.9.25 and 3.12.13:
The full I do not have a 3.7 interpreter to hand, so I have not run it there and would rather not claim otherwise. The path no longer depends on the working directory at all, though, so the 3.7 behaviour of |
1be323e to
312499d
Compare
Summary
utf-8-siginstead of the host locale, fixing Wrong encoding when runned via module #516 while accepting Windows-style UTF-8 BOMs.UnicodeDecodeErrortraceback.DocxTemplate.write_xml()output as UTF-8 instead of the host locale.The
write_xml()half has no linked issue; I found it while tracing the same locale-dependent text-I/O pattern for #516.Verification
I independently ran the locale regression, all 37 script tests, Flake8, and Black on macOS with Python 3.9.6:
The test forces Python out of UTF-8 mode under a
Clocale. It also verifies that invalid Latin-1 JSON raises the CLI's normalRuntimeError, and thatwrite_xml()emits bytes that decode as UTF-8.Current CI does not run these scripts because
test.ymlinvokestests/runtests.pyfrom the repository root. PR #652 already addresses that runner problem, so this PR does not overlap it.Related: withdrawn PR #640 proposed the CLI half in June and closed without comments or review. This change was developed independently and also covers BOM input, the CLI error path, and
write_xml().AI disclosure
Claude Opus 5 implemented and tested this change. OpenAI Codex independently reviewed the diff and reran the regression, full script suite, and linters. David authorized this automated contribution workflow and owns the submission. The commit keeps an explicit
Co-Authored-Bytrailer for Claude.