Skip to content

Commit 8bfde1d

Browse files
authored
Update XML writing (#599)
* write xml to file normally and test * remove filepath check
1 parent 7baeabe commit 8bfde1d

File tree

2 files changed

+91
-36
lines changed

2 files changed

+91
-36
lines changed

src/codemodder/codemods/xml_transformer.py

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import mmap
21
from dataclasses import dataclass, field
32
from tempfile import TemporaryFile
4-
from xml.sax import SAXParseException, handler
3+
from xml.sax import handler
54
from xml.sax.handler import LexicalHandler
65
from xml.sax.saxutils import XMLGenerator
76
from xml.sax.xmlreader import AttributesImpl, Locator
@@ -13,6 +12,7 @@
1312
from codemodder.context import CodemodExecutionContext
1413
from codemodder.diff import create_diff
1514
from codemodder.file_context import FileContext
15+
from codemodder.logging import logger
1616
from codemodder.result import Result
1717

1818

@@ -171,12 +171,7 @@ def apply(
171171
file_context: FileContext,
172172
results: list[Result] | None,
173173
) -> ChangeSet | None:
174-
if file_context.file_path.suffix.lower() not in (".config", ".xml"):
175-
return None
176-
177-
changes = []
178174
with TemporaryFile("w+") as output_file:
179-
180175
# this will fail fast for files that are not XML
181176
try:
182177
transformer_instance = self.xml_transformer(
@@ -187,42 +182,33 @@ def apply(
187182
parser.setProperty(
188183
handler.property_lexical_handler, transformer_instance
189184
)
190-
parser.parse(file_context.file_path)
185+
parser.parse(file_path := file_context.file_path)
191186
changes = transformer_instance.changes
192187
output_file.seek(0)
188+
except Exception:
189+
file_context.add_failure(
190+
file_path, reason := "Failed to parse XML file"
191+
)
192+
logger.exception("%s %s", reason, file_path)
193+
return None
193194

194-
except SAXParseException:
195+
if not changes:
195196
return None
196197

197-
diff = ""
198-
# don't calculate diff if no changes were reported
199-
if changes:
200-
with open(file_context.file_path, "r") as original:
201-
# TODO there's a failure potential here for very large files
202-
diff = create_diff(
203-
original.readlines(),
204-
output_file.readlines(),
205-
)
206-
207-
# don't write anything if no changes were issued
208-
# avoids simply formatting the file
209-
if changes and not context.dry_run:
210-
with open(file_context.file_path, "w+b") as original:
211-
# mmap can't map empty files, write something first
212-
original.write(b"a")
213-
# copy contents of result into original file
214-
# the snippet below preserves the original file metadata and accounts for large files.
215-
output_file.seek(0)
216-
output_mmap = mmap.mmap(output_file.fileno(), 0)
217-
218-
original.truncate()
219-
original_mmap = mmap.mmap(original.fileno(), 0)
220-
original_mmap.resize(len(output_mmap))
221-
original_mmap[:] = output_mmap
222-
original_mmap.flush()
198+
new_lines = output_file.readlines()
199+
with open(file_path, "r") as original:
200+
# TODO there's a failure potential here for very large files
201+
diff = create_diff(
202+
original.readlines(),
203+
new_lines,
204+
)
205+
206+
if not context.dry_run:
207+
with open(file_path, "w+") as original:
208+
original.writelines(new_lines)
223209

224210
return ChangeSet(
225-
path=str(file_context.file_path.relative_to(context.directory)),
211+
path=str(file_path.relative_to(context.directory)),
226212
diff=diff,
227213
changes=changes,
228214
)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from pathlib import Path
2+
3+
from codemodder.codemods.xml_transformer import (
4+
ElementAttributeXMLTransformer,
5+
XMLTransformerPipeline,
6+
)
7+
from codemodder.context import CodemodExecutionContext
8+
from codemodder.file_context import FileContext
9+
10+
11+
def test_transformer_failure(mocker, caplog):
12+
mocker.patch(
13+
"defusedxml.expatreader.DefusedExpatParser.parse",
14+
side_effect=Exception,
15+
)
16+
file_context = FileContext(
17+
"home",
18+
Path("test.xml"),
19+
)
20+
execution_context = CodemodExecutionContext(
21+
directory=mocker.MagicMock(),
22+
dry_run=True,
23+
verbose=False,
24+
registry=mocker.MagicMock(),
25+
repo_manager=mocker.MagicMock(),
26+
path_include=[],
27+
path_exclude=[],
28+
)
29+
transformer = mocker.MagicMock(spec=ElementAttributeXMLTransformer)
30+
pipeline = XMLTransformerPipeline(transformer)
31+
32+
changeset = pipeline.apply(
33+
context=execution_context,
34+
file_context=file_context,
35+
results=None,
36+
)
37+
assert changeset is None
38+
assert "Failed to parse XML file" in caplog.text
39+
40+
41+
def test_transformer(mocker):
42+
mocker.patch(
43+
"defusedxml.expatreader.DefusedExpatParser.parse",
44+
)
45+
mocker.patch("builtins.open")
46+
mocker.patch("codemodder.codemods.xml_transformer.create_diff", return_value="diff")
47+
file_context = FileContext(
48+
parent_dir := Path("home"),
49+
parent_dir / Path("test.xml"),
50+
)
51+
execution_context = CodemodExecutionContext(
52+
directory=parent_dir,
53+
dry_run=True,
54+
verbose=False,
55+
registry=mocker.MagicMock(),
56+
repo_manager=mocker.MagicMock(),
57+
path_include=[],
58+
path_exclude=[],
59+
)
60+
transformer = mocker.MagicMock(spec=ElementAttributeXMLTransformer)
61+
transformer.changes = ["change1", "change2"]
62+
pipeline = XMLTransformerPipeline(transformer)
63+
64+
changeset = pipeline.apply(
65+
context=execution_context,
66+
file_context=file_context,
67+
results=None,
68+
)
69+
assert changeset is not None

0 commit comments

Comments
 (0)