Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/6761.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed `reflex rename` corrupting or failing on source files on non-UTF-8 platform locales while preserving declared Python source encodings and line endings.
11 changes: 9 additions & 2 deletions reflex/utils/rename.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import re
import sys
import tokenize
from pathlib import Path

from reflex_base import constants
Expand Down Expand Up @@ -97,7 +98,13 @@ def rename_imports_and_app_name(file_path: str | Path, old_name: str, new_name:
new_name: The new name to use.
"""
file_path = Path(file_path)
content = file_path.read_text()
if file_path.suffix == constants.Ext.PY:
with file_path.open("rb") as source:
encoding = tokenize.detect_encoding(source.readline)[0]
else:
encoding = "utf-8"
with file_path.open("r", encoding=encoding, newline="") as source:
content = source.read()

# Replace `from old_name.` or `from old_name` with `from new_name`
content = re.sub(
Expand Down Expand Up @@ -127,7 +134,7 @@ def rename_imports_and_app_name(file_path: str | Path, old_name: str, new_name:
content,
)

file_path.write_text(content)
file_path.write_text(content, encoding=encoding, newline="")


def process_directory(
Expand Down
79 changes: 79 additions & 0 deletions tests/units/test_prerequisites.py
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,85 @@ def test_regex_edge_cases(temp_directory):
assert updated_content == expected_content


def test_rename_imports_and_app_name_preserves_utf8(
temp_directory, monkeypatch: pytest.MonkeyPatch
):
"""UTF-8 source is preserved even when the platform default encoding is not UTF-8.

Simulates a Western Windows locale (cp1252) where ``Path.read_text`` /
``write_text`` without an explicit ``encoding`` would mis-decode UTF-8 source,
silently corrupting or aborting on non-ASCII characters (curly quotes, accents).

Args:
temp_directory: A temporary directory fixture.
monkeypatch: The pytest monkeypatch fixture.
"""
original_open = Path.open

def open_file(
self, mode="r", buffering=-1, encoding=None, errors=None, newline=None
):
return original_open(
self,
mode=mode,
buffering=buffering,
encoding=(encoding if encoding is not None or "b" in mode else "cp1252"),
errors=errors,
newline=newline,
)

monkeypatch.setattr(Path, "open", open_file)

source = "import old_name # \u201cquoted\u201d caf\u00e9 na\u00efve r\u00e9sum\u00e9\n" # codespell:ignore
file_path = temp_directory / "example.py"
file_path.write_bytes(source.encode("utf-8"))

rename_imports_and_app_name(file_path, "old_name", "new_name")

expected = "import new_name # \u201cquoted\u201d caf\u00e9 na\u00efve r\u00e9sum\u00e9\n" # codespell:ignore
assert file_path.read_bytes() == expected.encode("utf-8")


def test_rename_imports_and_app_name_preserves_declared_encoding(temp_directory):
"""Python source encoding cookies are honored during rename.

Args:
temp_directory: A temporary directory fixture.
"""
source = (
"# -*- coding: cp1252 -*-\nimport old_name # caf\u00e9\n" # codespell:ignore
)
file_path = temp_directory / "example.py"
file_path.write_bytes(source.encode("cp1252"))

rename_imports_and_app_name(file_path, "old_name", "new_name")

expected = (
"# -*- coding: cp1252 -*-\nimport new_name # caf\u00e9\n" # codespell:ignore
)
assert file_path.read_bytes() == expected.encode("cp1252")


@pytest.mark.parametrize("line_ending", ["\n", "\r\n"])
def test_rename_imports_and_app_name_preserves_line_endings(
temp_directory, line_ending
):
"""Source line endings are preserved during rename.

Args:
temp_directory: A temporary directory fixture.
line_ending: The line ending used in the source file.
"""
source = line_ending.join(("import old_name", "# comment", ""))
file_path = temp_directory / "example.py"
file_path.write_bytes(source.encode())

rename_imports_and_app_name(file_path, "old_name", "new_name")

expected = line_ending.join(("import new_name", "# comment", ""))
assert file_path.read_bytes() == expected.encode()


def test_cli_rename_command(temp_directory):
foo_dir = temp_directory / "foo"
foo_dir.mkdir()
Expand Down
Loading