Skip to content

Commit 0cc9953

Browse files
authored
Transformers should read and write bytes (#765)
* fix regex transformer to handle windows style endings * change test utils * change read/write to read_bytes
1 parent ae174d9 commit 0cc9953

File tree

8 files changed

+66
-35
lines changed

8 files changed

+66
-35
lines changed

src/codemodder/codemodder.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,6 @@
2323
from codemodder.semgrep import run as run_semgrep
2424

2525

26-
def update_code(file_path, new_code):
27-
"""
28-
Write the `new_code` to the `file_path`
29-
"""
30-
with open(file_path, "w", encoding="utf-8") as f:
31-
f.write(new_code)
32-
33-
3426
def find_semgrep_results(
3527
context: CodemodExecutionContext,
3628
codemods: Sequence[BaseCodemod],

src/codemodder/codemods/libcst_transformer.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ def update_code(file_path, new_code):
2424
"""
2525
Write the `new_code` to the `file_path`
2626
"""
27-
with open(file_path, "w", encoding="utf-8") as f:
28-
f.write(new_code)
27+
file_path.write_bytes(new_code.encode("utf-8"))
2928

3029

3130
class LibcstResultTransformer(BaseTransformer):
@@ -261,8 +260,7 @@ def apply(
261260

262261
try:
263262
with file_context.timer.measure("parse"):
264-
with open(file_path, "r", encoding="utf-8") as f:
265-
source_tree = cst.parse_module(f.read())
263+
source_tree = cst.parse_module(file_path.read_bytes().decode("utf-8"))
266264
except Exception:
267265
file_context.add_failure(file_path, reason := "Failed to parse file")
268266
logger.exception("%s %s", reason, file_path)

src/codemodder/codemods/regex_transformer.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ def apply(
3434
changes = []
3535
updated_lines = []
3636

37-
with open(file_context.file_path, "r", encoding="utf-8") as f:
38-
original_lines = f.readlines()
37+
original_lines = (
38+
file_context.file_path.read_bytes()
39+
.decode("utf-8")
40+
.splitlines(keepends=True)
41+
)
3942

4043
for lineno, line in enumerate(original_lines):
4144
# TODO: use results to filter out which lines to change
@@ -57,8 +60,7 @@ def apply(
5760
diff = create_diff(original_lines, updated_lines)
5861

5962
if not context.dry_run:
60-
with open(file_context.file_path, "w+", encoding="utf-8") as original:
61-
original.writelines(updated_lines)
63+
file_context.file_path.write_bytes("".join(updated_lines).encode("utf-8"))
6264

6365
return ChangeSet(
6466
path=str(file_context.file_path.relative_to(context.directory)),

src/codemodder/codemods/test/utils.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,7 @@ def assert_changes(self, root, file_path, input_code, expected, changes):
129129
except AssertionError:
130130
raise DiffError(expected_diff, changes.diff)
131131

132-
with open(file_path, "r", encoding="utf-8") as tmp_file:
133-
output_code = tmp_file.read()
132+
output_code = file_path.read_bytes().decode("utf-8")
134133

135134
try:
136135
assert output_code == (format_expected := dedent(expected))

src/codemodder/codemods/xml_transformer.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,16 +218,19 @@ def apply(
218218
return None
219219

220220
new_lines = output_file.readlines()
221-
with open(file_path, "r") as original:
222-
# TODO there's a failure potential here for very large files
223-
diff = create_diff(
224-
original.readlines(),
225-
new_lines,
226-
)
221+
# TODO there's a failure potential here for very large files
222+
original_lines = (
223+
file_context.file_path.read_bytes()
224+
.decode("utf-8")
225+
.splitlines(keepends=True)
226+
)
227+
diff = create_diff(
228+
original_lines,
229+
new_lines,
230+
)
227231

228232
if not context.dry_run:
229-
with open(file_path, "w+") as original:
230-
original.writelines(new_lines)
233+
file_context.file_path.write_bytes("".join(new_lines).encode("utf-8"))
231234

232235
return ChangeSet(
233236
path=str(file_path.relative_to(context.directory)),

tests/codemods/test_xml_transformer.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
from pathlib import Path
22

3+
import mock
4+
35
from codemodder.codemods.xml_transformer import (
46
ElementAttributeXMLTransformer,
57
XMLTransformerPipeline,
68
)
79
from codemodder.context import CodemodExecutionContext
810
from codemodder.file_context import FileContext
911

12+
FILE_PATH = mock.MagicMock()
13+
FILE_PATH.read_bytes.return_value = b"""<?xml version="1.0" encoding="utf-8"?>"""
14+
1015

1116
def test_transformer_failure(mocker, caplog):
1217
mocker.patch(
1318
"defusedxml.expatreader.DefusedExpatParser.parse",
1419
side_effect=Exception,
1520
)
16-
file_context = FileContext(
17-
Path("home"),
18-
Path("test.xml"),
19-
)
21+
file_context = FileContext(parent_dir := Path("home"), FILE_PATH)
22+
2023
execution_context = CodemodExecutionContext(
21-
directory=mocker.MagicMock(),
24+
directory=parent_dir,
2225
dry_run=True,
2326
verbose=False,
2427
registry=mocker.MagicMock(),
@@ -45,10 +48,7 @@ def test_transformer(mocker):
4548
)
4649
mocker.patch("builtins.open")
4750
mocker.patch("codemodder.codemods.xml_transformer.create_diff", return_value="diff")
48-
file_context = FileContext(
49-
parent_dir := Path("home"),
50-
parent_dir / Path("test.xml"),
51-
)
51+
file_context = FileContext(parent_dir := Path("home"), FILE_PATH)
5252
execution_context = CodemodExecutionContext(
5353
directory=parent_dir,
5454
dry_run=True,

tests/test_libcst_transformer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from core_codemods.sonar.results import SonarResult
1414

1515
FILE_PATH = mock.MagicMock()
16+
FILE_PATH.read_bytes.return_value = b"# some code to codemod"
1617
DEFECTDOJO_RESULTS = [
1718
DefectDojoResult.from_result(
1819
{

tests/test_regex_transformer.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,39 @@ def test_transformer(mocker, tmp_path_factory):
7070
assert changeset is not None
7171
assert code.read_text() == text.replace("hello", "bye")
7272
assert changeset.changes[0].lineNumber == 1
73+
74+
75+
def test_transformer_windows_carriage(mocker, tmp_path_factory):
76+
base_dir = tmp_path_factory.mktemp("foo")
77+
code = base_dir / "code.py"
78+
text = (
79+
b"Hello, world!\r\nThis is a test string with Windows-style line endings.\r\n"
80+
)
81+
code.write_bytes(text)
82+
83+
file_context = FileContext(
84+
base_dir,
85+
code,
86+
)
87+
execution_context = CodemodExecutionContext(
88+
directory=base_dir,
89+
dry_run=False,
90+
verbose=False,
91+
registry=mocker.MagicMock(),
92+
providers=None,
93+
repo_manager=mocker.MagicMock(),
94+
path_include=[],
95+
path_exclude=[],
96+
)
97+
pipeline = RegexTransformerPipeline(
98+
pattern=r"world", replacement="Earth", change_description="testing"
99+
)
100+
101+
changeset = pipeline.apply(
102+
context=execution_context,
103+
file_context=file_context,
104+
results=None,
105+
)
106+
assert changeset is not None
107+
assert code.read_bytes() == text.replace(b"world", b"Earth")
108+
assert changeset.changes[0].lineNumber == 1

0 commit comments

Comments
 (0)