From 2ea5e942d7ccafb8f0cf2170b23717f126c064a7 Mon Sep 17 00:00:00 2001 From: b84330808 Date: Mon, 26 Oct 2026 22:36:20 +0900 Subject: [PATCH] Read and write reflex init's AGENTS.md, CLAUDE.md and .gitignore as UTF-8 initialize_gitignore() and the AGENTS.md/CLAUDE.md helpers used Path.read_text()/write_text() without an encoding, so they used the locale encoding. On Windows with a non-UTF-8 code page this made `reflex init` crash: the canonical AGENTS.md contains an em dash, which cp932 cannot encode, and existing UTF-8 files with CJK text could not be decoded. On cp1252 the files were silently written in the ANSI code page. Co-Authored-By: Claude Opus 5.5 --- news/+init-files-utf8.bugfix.md | 1 + reflex/utils/frontend_skeleton.py | 12 +++--- tests/units/utils/test_utils.py | 62 +++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 news/+init-files-utf8.bugfix.md diff --git a/news/+init-files-utf8.bugfix.md b/news/+init-files-utf8.bugfix.md new file mode 100644 index 00000000000..c68afacd971 --- /dev/null +++ b/news/+init-files-utf8.bugfix.md @@ -0,0 +1 @@ +`reflex init` now reads and writes `AGENTS.md`, `CLAUDE.md` and `.gitignore` as UTF-8, so it no longer fails or writes them in the Windows ANSI code page on non-UTF-8 locales. diff --git a/reflex/utils/frontend_skeleton.py b/reflex/utils/frontend_skeleton.py index 6d7565d7848..f9a8548e3a3 100644 --- a/reflex/utils/frontend_skeleton.py +++ b/reflex/utils/frontend_skeleton.py @@ -33,7 +33,9 @@ def initialize_gitignore( # Combine with the current ignored files. current_ignore: list[str] = [] if gitignore_file.exists(): - current_ignore = [ln.strip() for ln in gitignore_file.read_text().splitlines()] + current_ignore = [ + ln.strip() for ln in gitignore_file.read_text(encoding="utf-8").splitlines() + ] if files_to_ignore == current_ignore: logger.debug(f"{gitignore_file} already up to date.") @@ -44,7 +46,7 @@ def initialize_gitignore( # Write files to the .gitignore file. gitignore_file.touch(exist_ok=True) logger.debug(f"Creating {gitignore_file}") - gitignore_file.write_text("\n".join(files_to_ignore) + "\n") + gitignore_file.write_text("\n".join(files_to_ignore) + "\n", encoding="utf-8") AgentsMdAction = Literal["managed", "bridge"] @@ -76,7 +78,7 @@ def _plan_agents_md( return plan if ( agents_file.exists() and claude_file.samefile(agents_file) - ) or constants.AgentsMd.CLAUDE_IMPORT in claude_file.read_text(): + ) or constants.AgentsMd.CLAUDE_IMPORT in claude_file.read_text(encoding="utf-8"): return [(agents_file, "managed")] if agents_file.exists(): return [(agents_file, "managed"), (claude_file, "managed")] @@ -104,7 +106,7 @@ def _apply_agents_md_action( elif not file.exists(): content = managed_agents_md_text + "\n" else: - existing = file.read_text() + existing = file.read_text(encoding="utf-8") begin_idx = existing.find(begin) end_idx = existing.find(end, begin_idx + len(begin)) if begin_idx != -1 else -1 if end_idx != -1: @@ -120,7 +122,7 @@ def _apply_agents_md_action( f"\n\n{remainder}\n" if remainder else "\n" ) logger.debug(f"Creating {file}") - file.write_text(content) + file.write_text(content, encoding="utf-8") def initialize_agents_md( diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index 4ff41dac4cf..6ace983f256 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -380,6 +380,25 @@ def test_initialize_non_existent_gitignore( assert set(file_content) - expected == set() +def test_initialize_gitignore_keeps_utf8_entries(tmp_path): + """Test that an existing .gitignore is read and written as UTF-8 on any locale. + + Args: + tmp_path: The root test path. + """ + gitignore_file = tmp_path / ".gitignore" + gitignore_file.write_text("下書き/\n", encoding="utf-8") + + frontend_skeleton.initialize_gitignore( + gitignore_file=gitignore_file, files_to_ignore=["*.db"] + ) + + assert gitignore_file.read_text(encoding="utf-8").splitlines() == [ + "*.db", + "下書き/", + ] + + def test_initialize_agents_md_fetches_canonical(tmp_path, mocker): """Test that AGENTS.md is fetched and a CLAUDE.md bridge is created when absent.""" agents_file = tmp_path / "AGENTS.md" @@ -596,6 +615,49 @@ def test_initialize_agents_md_handles_symlinked_claude(tmp_path, mocker): assert claude_file.read_text() == agents_file.read_text() +def test_initialize_agents_md_writes_utf8(tmp_path, mocker): + """Test that a new AGENTS.md is written as UTF-8 on any locale.""" + agents_file = tmp_path / "AGENTS.md" + response = mocker.Mock() + response.text = "Reflex agent skills — 使い方" + mocker.patch("reflex.utils.net.get", return_value=response) + + frontend_skeleton.initialize_agents_md( + agents_file=agents_file, claude_file=tmp_path / "CLAUDE.md" + ) + + assert agents_file.read_text(encoding="utf-8") == ( + f"{constants.AgentsMd.BEGIN_MARKER}\n" + "Reflex agent skills — 使い方\n" + f"{constants.AgentsMd.END_MARKER}\n" + ) + + +def test_initialize_agents_md_keeps_utf8_user_content(tmp_path, mocker): + """Test that existing AGENTS.md and CLAUDE.md files are read as UTF-8 on any locale.""" + agents_file = tmp_path / "AGENTS.md" + agents_file.write_text("# 下書きのメモ\n", encoding="utf-8") + claude_file = tmp_path / "CLAUDE.md" + claude_file.write_text("# Claude 用のメモ\n", encoding="utf-8") + response = mocker.Mock() + response.text = "Reflex agent skills — 使い方" + mocker.patch("reflex.utils.net.get", return_value=response) + + frontend_skeleton.initialize_agents_md( + agents_file=agents_file, claude_file=claude_file + ) + + managed = ( + f"{constants.AgentsMd.BEGIN_MARKER}\n" + "Reflex agent skills — 使い方\n" + f"{constants.AgentsMd.END_MARKER}" + ) + assert agents_file.read_text(encoding="utf-8") == f"{managed}\n\n# 下書きのメモ\n" + assert ( + claude_file.read_text(encoding="utf-8") == f"{managed}\n\n# Claude 用のメモ\n" + ) + + def test_initialize_requirements_txt_skips_when_pyproject_exists(tmp_path): """Test that pyproject-based apps do not get a requirements.txt file.""" pyproject_file = tmp_path / "pyproject.toml"