From 5adda512e326433e93a5e62901767bd04ea34a15 Mon Sep 17 00:00:00 2001 From: Sreekant Baheti <60787859+Sreekant13@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:10:57 -0700 Subject: [PATCH 1/2] Write generated docs as UTF-8 `typer utils docs --output FILE` wrote the Markdown file using the platform's default encoding, so non-ASCII help (for example emojis, which are common in Typer/Rich CLIs) raised UnicodeEncodeError on interpreters where the locale encoding is not UTF-8, such as cp1252 on Windows. Write the file as UTF-8, matching how the docs are read back in the tests, and add a regression test that forces a non-UTF-8 locale so it fails on the old behavior on any platform. --- tests/test_cli/test_doc.py | 50 ++++++++++++++++++++++++++++++++++++++ typer/cli.py | 2 +- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/test_cli/test_doc.py b/tests/test_cli/test_doc.py index 1da8bb73ef..f12b68d853 100644 --- a/tests/test_cli/test_doc.py +++ b/tests/test_cli/test_doc.py @@ -86,6 +86,56 @@ def test_doc_title_output(tmp_path: Path): assert "Docs saved to:" in result.stdout +def test_doc_output_non_ascii(tmp_path: Path): + # Docs must be written as UTF-8 regardless of the platform's default + # encoding, otherwise non-ASCII help (e.g. emojis) crashes with a + # UnicodeEncodeError on interpreters where the locale encoding is not UTF-8 + # (e.g. cp1252 on Windows). + app_file: Path = tmp_path / "emoji_app.py" + app_file.write_text( + "import typer\n" + "app = typer.Typer()\n\n\n" + "@app.command()\n" + "def hello(name: str):\n" + ' """Say hello 👋 to someone."""\n' + " print(name)\n", + encoding="utf-8", + ) + out_file: Path = tmp_path / "out.md" + result = subprocess.run( + [ + sys.executable, + "-m", + "coverage", + "run", + "-m", + "typer", + str(app_file), + "utils", + "docs", + "--name", + "emoji", + "--output", + str(out_file), + ], + capture_output=True, + encoding="utf-8", + # Force a non-UTF-8 locale so the write path fails on the old behavior + # on any platform (not only Windows). + env={ + **os.environ, + "PYTHONUTF8": "0", + "LC_ALL": "C", + "LANG": "C", + "PYTHONIOENCODING": "utf-8", + }, + ) + assert result.returncode == 0, result.stderr + written_docs = out_file.read_text(encoding="utf-8") + assert "👋" in written_docs + assert "Docs saved to:" in result.stdout + + def test_doc_no_rich(): result = subprocess.run( [ diff --git a/typer/cli.py b/typer/cli.py index 665bcf5a59..56d82c4446 100644 --- a/typer/cli.py +++ b/typer/cli.py @@ -307,7 +307,7 @@ def docs( docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name, title=title) clean_docs = f"{docs.strip()}\n" if output: - output.write_text(clean_docs) + output.write_text(clean_docs, encoding="utf-8") typer.echo(f"Docs saved to: {output}") else: typer.echo(clean_docs) From 3e540c5b55a7586e013e48d566c08b1643357a67 Mon Sep 17 00:00:00 2001 From: Sreekant Baheti <60787859+Sreekant13@users.noreply.github.com> Date: Sat, 4 Jul 2026 10:33:51 -0700 Subject: [PATCH 2/2] Simplify regression test env: drop redundant LANG=C LC_ALL=C already overrides LANG, so LANG=C was redundant. Keep LC_ALL=C (forces a non-UTF-8 locale) and PYTHONUTF8=0 (keeps it non-UTF-8 on 3.15+ where UTF-8 mode is on by default), with a comment explaining why each is needed for the test to fail on the old behavior on any platform. --- tests/test_cli/test_doc.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_cli/test_doc.py b/tests/test_cli/test_doc.py index f12b68d853..c257b478b9 100644 --- a/tests/test_cli/test_doc.py +++ b/tests/test_cli/test_doc.py @@ -124,9 +124,12 @@ def test_doc_output_non_ascii(tmp_path: Path): # on any platform (not only Windows). env={ **os.environ, - "PYTHONUTF8": "0", + # LC_ALL forces a non-UTF-8 locale (the write path would otherwise + # use UTF-8 on most CI); PYTHONUTF8=0 keeps it non-UTF-8 on 3.15+ + # where UTF-8 mode is on by default; PYTHONIOENCODING lets us capture + # the subprocess output cleanly when it fails. "LC_ALL": "C", - "LANG": "C", + "PYTHONUTF8": "0", "PYTHONIOENCODING": "utf-8", }, )