Skip to content

Commit 68c33be

Browse files
committed
Keep the file's newline style when refactoring
tokenize.open translates newlines while reading and write_text wrote \n, so refactoring a CRLF file rewrote every line to LF. read() now also returns the newline it saw, and the file is written back with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019P9bvuGwAyVCNUdsAYPB1V
1 parent 3325092 commit 68c33be

3 files changed

Lines changed: 24 additions & 6 deletions

File tree

‎src/unexport/session.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def get_source(self, path: Path) -> Iterator[tuple[str | None, Path, str | None]
2020
"""Yield (source, path, error) for each file; source is None when the file can't be read."""
2121
for py_path in utils.list_paths(path, include=self.config.include, exclude=self.config.exclude):
2222
try:
23-
source, _ = utils.read(py_path)
23+
source, _, _ = utils.read(py_path)
2424
except utils.READ_ERRORS as exc:
2525
yield None, py_path, str(exc)
2626
else:
@@ -36,9 +36,10 @@ def get_expected_all(source: str) -> tuple[bool, list[str]]:
3636

3737
@classmethod
3838
def refactor(cls, path: Path, apply: bool = False) -> str:
39-
source, encoding = utils.read(path)
39+
source, encoding, newline = utils.read(path)
4040
_, expected_all = cls.get_expected_all(source)
4141
new_source = refactor_source(source, expected_all)
4242
if apply and new_source != source:
43-
path.write_text(new_source, encoding=encoding)
43+
with path.open("w", encoding=encoding, newline=newline) as file:
44+
file.write(new_source)
4445
return new_source

‎src/unexport/utils.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515
READ_ERRORS = (OSError, SyntaxError, UnicodeDecodeError)
1616

1717

18-
def read(path: Path) -> tuple[str, str]:
18+
def read(path: Path) -> tuple[str, str, str]:
19+
"""Return (source with ``\\n`` newlines, encoding, the file's newline) so the file can be written back as it was."""
1920
with tokenize.open(path) as stream:
2021
source = stream.read()
2122
encoding = stream.encoding
22-
return source, encoding
23+
newlines = stream.newlines # the newline(s) seen while reading: None, a str or a tuple of them
24+
newline = newlines if isinstance(newlines, str) else (newlines or ("\n",))[0]
25+
return source, encoding, newline
2326

2427

2528
def list_paths(

‎tests/test_main.py‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
from unexport import utils
66
from unexport.main import main
77

8-
__all__ = ["test_errors_are_reported", "test_read_errors", "test_several_statements_are_not_refactored"]
8+
__all__ = [
9+
"test_errors_are_reported",
10+
"test_read_errors",
11+
"test_refactor_keeps_crlf_newlines",
12+
"test_several_statements_are_not_refactored",
13+
]
914

1015

1116
def test_errors_are_reported(tmp_path: Path, capsys):
@@ -54,3 +59,12 @@ def test_several_statements_are_not_refactored(tmp_path: Path, capsys):
5459
assert "Refactoring" not in output
5560
assert path.read_text() == source
5661
assert exit_code == 1
62+
63+
64+
def test_refactor_keeps_crlf_newlines(tmp_path: Path):
65+
path = tmp_path / "crlf.py"
66+
path.write_bytes(b"import os\r\n\r\nclass Public: ...\r\n")
67+
68+
main(["--refactor", path.as_posix()])
69+
70+
assert path.read_bytes() == b'import os\r\n\r\n__all__ = ["Public"]\r\n\r\nclass Public: ...\r\n'

0 commit comments

Comments
 (0)