Skip to content

Commit

Permalink
A Patcher class that can apply patches
Browse files Browse the repository at this point in the history
  • Loading branch information
regebro committed Feb 21, 2019
1 parent 084f26e commit 1a2b218
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 9 deletions.
14 changes: 7 additions & 7 deletions tests/test_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import unittest

from lxml import etree
from xmldiff import diff, formatting, main, actions
from xmldiff import formatting, main, actions

from .testing import generate_filebased_cases

Expand Down Expand Up @@ -298,7 +298,7 @@ def test_update_text_in(self):

left = u'<document><node>This is a bit of text, right' + END
action = actions.UpdateTextIn('/document/node',
'Also a bit of text, rick')
'Also a bit of text, rick')
expected = START + u'><diff:delete>This is</diff:delete><diff:insert>'\
u'Also</diff:insert> a bit of text, ri<diff:delete>ght'\
u'</diff:delete><diff:insert>ck</diff:insert>' + END
Expand All @@ -316,7 +316,7 @@ def test_update_text_after_1(self):
def test_update_text_after_2(self):
left = u'<document><node/>This is a bit of text, right</document>'
action = actions.UpdateTextAfter('/document/node',
'Also a bit of text, rick')
'Also a bit of text, rick')
expected = START + u'/><diff:delete>This is</diff:delete>'\
u'<diff:insert>Also</diff:insert> a bit of text, ri<diff:delete>'\
u'ght</diff:delete><diff:insert>ck</diff:insert></document>'
Expand Down Expand Up @@ -396,7 +396,7 @@ def test_update_text_in(self):
self._format_test(action, expected)

action = actions.UpdateTextIn('/document/node',
'Also a bit of text, "rick"')
'Also a bit of text, "rick"')
expected = '[update-text, /document/node, '\
u'"Also a bit of text, \\"rick\\""]'
self._format_test(action, expected)
Expand All @@ -408,7 +408,7 @@ def test_update_text_after_1(self):

def test_update_text_after_2(self):
action = actions.UpdateTextAfter('/document/node',
'Also a bit of text, rick')
'Also a bit of text, rick')
expected = '[update-text-after, /document/node, '\
u'"Also a bit of text, rick"]'
self._format_test(action, expected)
Expand Down Expand Up @@ -476,7 +476,7 @@ def test_update_text_in(self):
self._format_test(action, expected)

action = actions.UpdateTextIn('/document/node',
'Also a bit of text, "rick"')
'Also a bit of text, "rick"')
expected = '[update, /document/node/text()[1], '\
u'"Also a bit of text, \\"rick\\""]'
self._format_test(action, expected)
Expand All @@ -488,7 +488,7 @@ def test_update_text_after_1(self):

def test_update_text_after_2(self):
action = actions.UpdateTextAfter('/document/node',
'Also a bit of text, rick')
'Also a bit of text, rick')
expected = '[update, /document/node/text()[2], '\
u'"Also a bit of text, rick"]'
self._format_test(action, expected)
Expand Down
95 changes: 95 additions & 0 deletions tests/test_patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import os
import unittest

from lxml import etree
from xmldiff.main import diff_trees
from xmldiff.patch import Patcher
from xmldiff.actions import (UpdateTextIn, InsertNode, MoveNode,
DeleteNode, UpdateAttrib, InsertAttrib,
RenameAttrib, DeleteAttrib, UpdateTextAfter,
RenameNode, InsertComment)

from .testing import compare_elements


class PatcherTests(unittest.TestCase):

patcher = Patcher()

def _test(self, start, action, end):
tree = etree.fromstring(start)
self.patcher.handle_action(action, tree)
self.assertEqual(etree.tounicode(tree), end)

def test_delete_node(self):
self._test('<root><deleteme/></root>',
DeleteNode('/root/deleteme'),
'<root/>')

def test_insert_node(self):
self._test('<root><anode/></root>',
InsertNode('/root/anode', 'newnode', 0),
'<root><anode><newnode/></anode></root>')

def test_rename_node(self):
self._test('<root><oldname/></root>',
RenameNode('/root/oldname', 'newname'),
'<root><newname/></root>')

def test_move_node(self):
self._test('<root><anode><moveme/></anode></root>',
MoveNode('/root/anode/moveme', '/root', 1),
'<root><anode/><moveme/></root>')

def test_update_text_in(self):
self._test('<root><anode/></root>',
UpdateTextIn('/root/anode', 'New text'),
'<root><anode>New text</anode></root>')

def test_update_text_after(self):
self._test('<root><anode/></root>',
UpdateTextAfter('/root/anode', 'New text'),
'<root><anode/>New text</root>')

def test_update_attrib(self):
self._test('<root><anode attrib="oldvalue" /></root>',
UpdateAttrib('/root/anode', 'attrib', 'newvalue'),
'<root><anode attrib="newvalue"/></root>')

def test_delete_attrib(self):
self._test('<root><anode attrib="oldvalue" /></root>',
DeleteAttrib('/root/anode', 'attrib'),
'<root><anode/></root>')

def test_insert_attrib(self):
self._test('<root><anode/></root>',
InsertAttrib('/root/anode', 'attrib', 'value'),
'<root><anode attrib="value"/></root>')

def test_rename_attrib(self):
self._test('<root><anode oldname="value"/></root>',
RenameAttrib('/root/anode', 'oldname', 'newname'),
'<root><anode newname="value"/></root>')

def test_insert_comment(self):
self._test('<root><anode/></root>',
InsertComment('/root', 1, "This is a new comment"),
'<root><anode/><!--This is a new comment--></root>')


class DiffPatch(unittest.TestCase):

def test_diff_patch(self):
here = os.path.split(__file__)[0]
lfile = os.path.join(here, 'test_data', 'all_actions.left.xml')
rfile = os.path.join(here, 'test_data', 'all_actions.right.xml')

left = etree.parse(lfile)
right = etree.parse(rfile)
diff = diff_trees(left, right)
result = Patcher().patch(diff, left)

# This example has top level comments, and lxml doesn't deal well
# with that, so the trees are not EXACTLY the same, the trailing
# top level comment differs, but that's OK.
compare_elements(result.getroot(), right.getroot())
4 changes: 2 additions & 2 deletions xmldiff/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,8 @@ def diff(self, left=None, right=None):
pos = self.find_pos(rnode)
# (ii)
if rnode.tag is etree.Comment:
yield actions.InsertComment(utils.getpath(ltarget, ltree), pos,
rnode.text)
yield actions.InsertComment(
utils.getpath(ltarget, ltree), pos, rnode.text)
lnode = etree.Comment(rnode.text)
else:
yield actions.InsertNode(utils.getpath(ltarget, ltree),
Expand Down
69 changes: 69 additions & 0 deletions xmldiff/patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from copy import deepcopy
from lxml import etree


class Patcher(object):

def patch(self, actions, tree):
# Copy the tree so we don't modify the original
result = deepcopy(tree)

for action in actions:
self.handle_action(action, result)

return result

def handle_action(self, action, tree):
action_type = type(action)
method = getattr(self, '_handle_' + action_type.__name__)
method(action, tree)

def _handle_DeleteNode(self, action, tree):
node = tree.xpath(action.node)[0]
node.getparent().remove(node)

def _handle_InsertNode(self, action, tree):
target = tree.xpath(action.target)[0]
node = target.makeelement(action.tag)
target.insert(action.position, node)

def _handle_RenameNode(self, action, tree):
tree.xpath(action.node)[0].tag = action.tag

def _handle_MoveNode(self, action, tree):
node = tree.xpath(action.node)[0]
node.getparent().remove(node)
target = tree.xpath(action.target)[0]
target.insert(action.position, node)

def _handle_UpdateTextIn(self, action, tree):
tree.xpath(action.node)[0].text = action.text

def _handle_UpdateTextAfter(self, action, tree):
tree.xpath(action.node)[0].tail = action.text

def _handle_UpdateAttrib(self, action, tree):
node = tree.xpath(action.node)[0]
# This should not be used to insert new attributes.
assert action.name in node.attrib
node.attrib[action.name] = action.value

def _handle_DeleteAttrib(self, action, tree):
del tree.xpath(action.node)[0].attrib[action.name]

def _handle_InsertAttrib(self, action, tree):
node = tree.xpath(action.node)[0]
# This should not be used to update existing attributes.
assert action.name not in node.attrib
node.attrib[action.name] = action.value

def _handle_RenameAttrib(self, action, tree):
node = tree.xpath(action.node)[0]
assert action.oldname in node.attrib
assert action.newname not in node.attrib
node.attrib[action.newname] = node.attrib[action.oldname]
del node.attrib[action.oldname]

def _handle_InsertComment(self, action, tree):
target = tree.xpath(action.target)[0]
target.insert(action.position, etree.Comment(action.text))

0 comments on commit 1a2b218

Please sign in to comment.