Repository: Shoobx/xmldiff

Issue:
Patching doesn't account for namespaces in xpaths and breaks
Xmldiff patching breaks when there are namespace prefixes in xpaths within xmldiff actions. e.g.: `ns:` in`MoveNode(node='/ns:a/ns:b/ns:x', target='/ns:a/ns:c', position=0)`.

When I attempt to apply the patch. I get the following:
```
Traceback (most recent call last):
  File "[REDACTED]", line [REDACTED], in [REDACTED]
    [REDACTED]
  File "xmldiff/xmldiff/main.py", line 156, in patch_tree
    return patcher.patch(actions, tree)
  File "xmldiff/xmldiff/patch.py", line 14, in patch
    self.handle_action(action, result)
  File "xmldiff/xmldiff/patch.py", line 21, in handle_action
    method(action, tree)
  File "xmldiff/xmldiff/patch.py", line 36, in _handle_MoveNode
    node = tree.xpath(action.node)[0]
  File "src/lxml/etree.pyx", line 1582, in lxml.etree._Element.xpath
  File "src/lxml/xpath.pxi", line 305, in lxml.etree.XPathElementEvaluator.__call__
  File "src/lxml/xpath.pxi", line 225, in lxml.etree._XPathEvaluatorBase._handle_result
lxml.etree.XPathEvalError: Undefined namespace prefix
```
I'd suggest a simple change to all calls to the `tree.xpath()` method in patch.py where `tree.xpath(action.<field>)` is replaced by `tree.xpath(action.<field>, namespaces=tree.nsmap)`.

i.e.:
```
diff --git a/xmldiff/patch.py b/xmldiff/patch.py
index 872275d..a3bd38d 100644
--- a/xmldiff/patch.py
+++ b/xmldiff/patch.py
@@ -21,53 +21,53 @@ class Patcher:
         method(action, tree)
 
     def _handle_DeleteNode(self, action, tree):
-        node = tree.xpath(action.node)[0]
+        node = tree.xpath(action.node, namespaces=tree.nsmap)[0]
         node.getparent().remove(node)
 
     def _handle_InsertNode(self, action, tree):
-        target = tree.xpath(action.target)[0]
+        target = tree.xpath(action.target, namespaces=tree.nsmap)[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
+        tree.xpath(action.node, namespaces=tree.nsmap)[0].tag = action.tag
 
     def _handle_MoveNode(self, action, tree):
-        node = tree.xpath(action.node)[0]
+        node = tree.xpath(action.node, namespaces=tree.nsmap)[0]
         node.getparent().remove(node)
-        target = tree.xpath(action.target)[0]
+        target = tree.xpath(action.target, namespaces=tree.nsmap)[0]
         target.insert(action.position, node)
 
     def _handle_UpdateTextIn(self, action, tree):
-        tree.xpath(action.node)[0].text = action.text
+        tree.xpath(action.node, namespaces=tree.nsmap)[0].text = action.text
 
     def _handle_UpdateTextAfter(self, action, tree):
-        tree.xpath(action.node)[0].tail = action.text
+        tree.xpath(action.node, namespaces=tree.nsmap)[0].tail = action.text
 
     def _handle_UpdateAttrib(self, action, tree):
-        node = tree.xpath(action.node)[0]
+        node = tree.xpath(action.node, namespaces=tree.nsmap)[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]
+        del tree.xpath(action.node, namespaces=tree.nsmap)[0].attrib[action.name]
 
     def _handle_InsertAttrib(self, action, tree):
-        node = tree.xpath(action.node)[0]
+        node = tree.xpath(action.node, namespaces=tree.nsmap)[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]
+        node = tree.xpath(action.node, namespaces=tree.nsmap)[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 = tree.xpath(action.target, namespaces=tree.nsmap)[0]
         target.insert(action.position, etree.Comment(action.text))
```
I haven't tested this beyond my single use case (which works for me), so probably need some testing before being adopted (assuming it is adopted).   


Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index c780fb0..9d65f6a 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -15,6 +15,8 @@ Changes
 
 - Added a parameter for ignoring attributes in comparison.
 
+- Solved a bug in xmlpatch in certain namespace situations.
+
 - Added a --diff-encoding parameter to xmlpatch, to support diff-files that are
   not in your system default encoding.
 
diff --git a/xmldiff/patch.py b/xmldiff/patch.py
index 872275d..8c6d128 100644
--- a/xmldiff/patch.py
+++ b/xmldiff/patch.py
@@ -7,6 +7,9 @@ from xmldiff import actions
 
 class Patcher:
     def patch(self, actions, tree):
+        if isinstance(tree, etree._ElementTree):
+            tree = tree.getroot()
+
         # Copy the tree so we don't modify the original
         result = deepcopy(tree)
 
@@ -21,46 +24,46 @@ class Patcher:
         method(action, tree)
 
     def _handle_DeleteNode(self, action, tree):
-        node = tree.xpath(action.node)[0]
+        node = tree.xpath(action.node, namespaces=tree.nsmap)[0]
         node.getparent().remove(node)
 
     def _handle_InsertNode(self, action, tree):
-        target = tree.xpath(action.target)[0]
+        target = tree.xpath(action.target, namespaces=tree.nsmap)[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
+        tree.xpath(action.node, namespaces=tree.nsmap)[0].tag = action.tag
 
     def _handle_MoveNode(self, action, tree):
-        node = tree.xpath(action.node)[0]
+        node = tree.xpath(action.node, namespaces=tree.nsmap)[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
+        tree.xpath(action.node, namespaces=tree.nsmap)[0].text = action.text
 
     def _handle_UpdateTextAfter(self, action, tree):
-        tree.xpath(action.node)[0].tail = action.text
+        tree.xpath(action.node, namespaces=tree.nsmap)[0].tail = action.text
 
     def _handle_UpdateAttrib(self, action, tree):
-        node = tree.xpath(action.node)[0]
+        node = tree.xpath(action.node, namespaces=tree.nsmap)[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]
+        del tree.xpath(action.node, namespaces=tree.nsmap)[0].attrib[action.name]
 
     def _handle_InsertAttrib(self, action, tree):
-        node = tree.xpath(action.node)[0]
+        node = tree.xpath(action.node, namespaces=tree.nsmap)[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]
+        node = tree.xpath(action.node, namespaces=tree.nsmap)[0]
         assert action.oldname in node.attrib
         assert action.newname not in node.attrib
         node.attrib[action.newname] = node.attrib[action.oldname]


Test patch:
diff --git a/tests/test_patch.py b/tests/test_patch.py
index 7be0c9f..670167e 100644
--- a/tests/test_patch.py
+++ b/tests/test_patch.py
@@ -119,7 +119,7 @@ class DiffPatch(unittest.TestCase):
         # 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())
+        compare_elements(result, right.getroot())
 
 
 TEST_DIFF = """[delete, node]