diff --git a/tests/test_cli/test_doc.py b/tests/test_cli/test_doc.py index 1da8bb73ef..c257b478b9 100644 --- a/tests/test_cli/test_doc.py +++ b/tests/test_cli/test_doc.py @@ -86,6 +86,59 @@ 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, + # 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", + "PYTHONUTF8": "0", + "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)