Commit f3188d86 authored by Tatuya Kamada's avatar Tatuya Kamada

- replace 4SuiteXML with lxml

- replace distutill with setuptool 
- append test-cases using docstring
- append buildout setting
- append interface
- eggify
parent 1ba251e9
......@@ -20,19 +20,7 @@
#
##############################################################################
try:
from Ft.Xml import Parse as parse
from Ft.Xml.Domlette import NonvalidatingReader, PrettyPrint
parseString = NonvalidatingReader.parseString
from Ft.Xml.Domlette import implementation
from Ft.Xml import EMPTY_NAMESPACE
def getDOMImplementation():
return implementation
except ImportError:
from xml.dom.minidom import parse, parseString
from xml.dom.minidom import getDOMImplementation
from xml.dom.ext import PrettyPrint
EMPTY_NAMESPACE = None
from lxml import etree
import sys
import getopt
......@@ -40,6 +28,9 @@ import os
from StringIO import StringIO
import re
import codecs
from copy import deepcopy
from interfaces.erp5diff import IERP5Diff
import zope.interface
class ERP5Diff:
"""
......@@ -56,6 +47,10 @@ class ERP5Diff:
5. Ignore some types of nodes, such as EntityReference and Comment, because they are not
used in ERP5 XML documents.
"""
# Declarative interfaces
zope.interface.implements(IERP5Diff,)
def __init__(self):
"""
Initialize itself.
......@@ -84,9 +79,10 @@ class ERP5Diff:
doc_list = []
for a in args:
if type(a) == type(''):
doc_list.append(parseString(a))
doc_list.append(etree.XML(a))
else:
doc_list.append(parse(a))
element_tree = etree.parse(a)
doc_list.append(element_tree.getroot())
return doc_list
def _concatPath(self, p1, p2, separator='/'):
......@@ -102,151 +98,148 @@ class ERP5Diff:
"""
Return the root element of the result document.
"""
return self._result.documentElement
return self._result
#return self._result.getroottree()
def _hasChildren(self, element):
"""
Check whether the element has any children
"""
if len(element) == 0:
return False
return True
def _xupdateAppendAttributes(self, dict, path):
"""
Append attributes to the element at 'path'.
Append attrib to the element at 'path'.
"""
root = self._getResultRoot()
createElement = self._result.createElementNS
createTextNode = self._result.createTextNode
append_element = createElement(self._ns, 'xupdate:append')
append_element.setAttributeNS(EMPTY_NAMESPACE, 'select', path)
append_element = etree.Element('{%s}append' % self._ns, nsmap=root.nsmap)
append_element.attrib['select'] = path
for name, val in dict.iteritems():
attr_element = createElement(self._ns, 'xupdate:attribute')
attr_element.setAttributeNS(EMPTY_NAMESPACE, 'name', name)
text_node = createTextNode(val)
attr_element.appendChild(text_node)
append_element.appendChild(attr_element)
root.appendChild(append_element)
attr_element = etree.Element('{%s}attribute' % self._ns, nsmap=root.nsmap)
attr_element.attrib['name'] = name
attr_element.text = val
append_element.append(attr_element)
root.append(append_element)
def _xupdateRemoveAttribute(self, name, path):
"""
Remove an attribute from the element at 'path'.
"""
root = self._getResultRoot()
createElement = self._result.createElementNS
remove_element = createElement(self._ns, 'xupdate:remove')
remove_element.setAttributeNS(EMPTY_NAMESPACE, 'select', self._concatPath(path, 'attribute::' + name))
root.appendChild(remove_element)
remove_element = etree.Element('{%s}remove' % self._ns, nsmap=root.nsmap)
remove_element.attrib['select'] = self._concatPath(path, 'attribute::' + name)
root.append(remove_element)
def _xupdateUpdateAttribute(self, name, val, path):
"""
Update the value of an attribute of the element at 'path'.
"""
root = self._getResultRoot()
createElement = self._result.createElementNS
createTextNode = self._result.createTextNode
update_element = createElement(self._ns, 'xupdate:update')
update_element.setAttributeNS(EMPTY_NAMESPACE, 'select', self._concatPath(path, 'attribute::' + name))
text_node = createTextNode(val)
update_element.appendChild(text_node)
root.appendChild(update_element)
update_element = etree.Element('{%s}update' % self._ns, nsmap=root.nsmap)
update_element.attrib['select'] = self._concatPath(path, 'attribute::' + name)
update_element.text = val
root.append(update_element)
def _xupdateRenameElement(self, name, path):
"""
Rename an existing element at 'path'.
"""
root = self._getResultRoot()
createElement = self._result.createElementNS
createTextNode = self._result.createTextNode
rename_element = createElement(self._ns, 'xupdate:rename')
rename_element.setAttributeNS(EMPTY_NAMESPACE, 'select', path)
text_node = createTextNode(name)
rename_element.appendChild(text_node)
root.appendChild(rename_element)
rename_element = etree.Element('{%s}rename' % self._ns, nsmap=root.nsmap)
rename_element.attrib['select'] = path
rename_element.text = name
root.append(rename_element)
def _xupdateUpdateElement(self, element, path):
"""
Update the contents of an element at 'path' to that of 'element'.
"""
root = self._getResultRoot()
createElement = self._result.createElementNS
update_element = createElement(self._ns, 'xupdate:update')
update_element.setAttributeNS(EMPTY_NAMESPACE, 'select', path)
for node in element.childNodes:
#self._p("node is %s" % repr(node))
clone_node = node.cloneNode(1)
update_element.appendChild(clone_node)
root.appendChild(update_element)
update_element = etree.Element('{%s}update' % self._ns, nsmap=root.nsmap)
update_element.attrib['select'] = path
if self._hasChildren(element):
for child in element:
clone_node = deepcopy(child)
update_element.append(clone_node)
else:
update_element.text = element.text
root.append(update_element)
def _xupdateRemoveElement(self, path):
"""
Remove an element at 'path'.
"""
root = self._getResultRoot()
createElement = self._result.createElementNS
remove_element = createElement(self._ns, 'xupdate:remove')
remove_element.setAttributeNS(EMPTY_NAMESPACE, 'select', path)
root.appendChild(remove_element)
remove_element = etree.Element('{%s}remove' % self._ns, nsmap=root.nsmap)
remove_element.attrib['select'] = path
root.append(remove_element)
def _xupdateInsertBefore(self, element_list, path):
"""
Insert elements before the element at 'path'.
"""
root = self._getResultRoot()
createElement = self._result.createElementNS
createTextNode = self._result.createTextNode
insert_element = createElement(self._ns, 'xupdate:insert-before')
insert_element.setAttributeNS(EMPTY_NAMESPACE, 'select', path)
insert_element = etree.Element('{%s}insert-before' % self._ns, nsmap=root.nsmap)
insert_element.attrib['select'] = path
for element in element_list:
child_element = createElement(self._ns, 'xupdate:element')
child_element.setAttributeNS(EMPTY_NAMESPACE, 'name', element.tagName)
attr_map = element.attributes
for attr in attr_map.values():
attr_element = createElement(self._ns, 'xupdate:attribute')
attr_element.setAttributeNS(EMPTY_NAMESPACE, 'name', attr.name)
text_node = createTextNode(attr.nodeValue)
attr_element.appendChild(text_node)
child_element.appendChild(attr_element)
for child in element.childNodes:
clone_node = child.cloneNode(1)
child_element.appendChild(clone_node)
insert_element.appendChild(child_element)
root.appendChild(insert_element)
child_element = etree.Element('{%s}element' % self._ns, nsmap=root.nsmap)
child_element.attrib['name'] = element.tag
attr_map = element.attrib
for name, value in attr_map.items():
attr_element = etree.Element('{%s}attribute' % self._ns, nsmap=root.nsmap)
attr_element.attrib['name'] = name
attr_element.text = value
child_element.append(attr_element)
for child in element:
clone_node = deepcopy(child)
child_element.append(clone_node)
if self._hasChildren(child_element):
child_element[-1].tail = element.text
insert_element.append(child_element)
root.append(insert_element)
def _xupdateAppendElements(self, element_list, path):
"""
Append elements to the element at 'path'.
"""
root = self._getResultRoot()
createElement = self._result.createElementNS
createTextNode = self._result.createTextNode
append_element = createElement(self._ns, 'xupdate:append')
append_element.setAttributeNS(EMPTY_NAMESPACE, 'select', path)
append_element = etree.Element('{%s}append' % self._ns, nsmap=root.nsmap)
append_element.attrib['select'] = path
for element in element_list:
child_element = createElement(self._ns, 'xupdate:element')
child_element.setAttributeNS(EMPTY_NAMESPACE, 'name', element.tagName)
attr_map = element.attributes
for attr in attr_map.values():
attr_element = createElement(self._ns, 'xupdate:attribute')
attr_element.setAttributeNS(EMPTY_NAMESPACE, 'name', attr.name)
text_node = createTextNode(attr.nodeValue)
attr_element.appendChild(text_node)
child_element.appendChild(attr_element)
for child in element.childNodes:
clone_node = child.cloneNode(1)
child_element.appendChild(clone_node)
append_element.appendChild(child_element)
root.appendChild(append_element)
child_element = etree.Element('{%s}element' % self._ns, nsmap=root.nsmap)
child_element.attrib['name'] = element.tag
attr_map = element.attrib
for name, value in attr_map.items():
attr_element = etree.Element('{%s}attribute' % self._ns, nsmap=root.nsmap)
attr_element.attrib['name'] = name
attr_element.text = value
child_element.append(attr_element)
for child in element:
clone_node = deepcopy(child)
child_element.append(clone_node)
if self._hasChildren(child_element):
child_element[-1].tail = element.text
append_element.append(child_element)
root.append(append_element)
def _testElements(self, element1, element2):
"""
Test if two given elements are matching. Matching does not mean that they are identical.
"""
# Make sure that they are elements.
if element1.nodeType != element2.nodeType or element1.nodeType != element1.ELEMENT_NODE:
if type(element1) != type(element2) or type(element1) != etree._Element:
return 0
if element1.tagName != element2.tagName:
if element1.tag != element2.tag:
return 0
id_list = []
for attr_map in (element1.attributes, element2.attributes):
for attr in attr_map.values():
if attr.name == 'id':
id_list.append(attr.nodeValue)
for attr_map in (element1.attrib, element2.attrib):
for name, value in attr_map.items():
if name == 'id':
id_list.append(value)
break
if len(id_list) == 0:
......@@ -257,18 +250,18 @@ class ERP5Diff:
def _testAttributes(self, element1, element2, path):
"""
Test attributes of two given elements. Add differences, if any.
Test attrib of two given elements. Add differences, if any.
"""
# Make a list of dictionaries of the attributes.
dict_list = []
for attr_map in (element1.attributes, element2.attributes):
dict = {}
for attr in attr_map.values():
dict[attr.name] = attr.nodeValue
dict_list.append(dict)
for attr_map in (element1.attrib, element2.attrib):
d = {}
for name, value in attr_map.items():
d[name] = value
dict_list.append(d)
dict1, dict2 = dict_list
# Find all added or removed or changed attributes.
# Find all added or removed or changed attrib.
for name1, val1 in dict1.iteritems():
if name1 in dict2:
if val1 != dict2[name1]:
......@@ -279,21 +272,23 @@ class ERP5Diff:
else:
# This attribute is removed.
self._xupdateRemoveAttribute(name1, path)
dict = {}
d = {}
for name2, val2 in dict2.iteritems():
if val2 is not None:
# This attribute is added.
dict[name2] = val2
if dict != {}:
self._xupdateAppendAttributes(dict, path)
d[name2] = val2
if d != {}:
self._xupdateAppendAttributes(d, path)
def _checkEmptiness(self, element):
"""
Check if an element has child values.
"""
for child in element.childNodes:
if child.nodeType == child.ELEMENT_NODE or child.nodeType == child.TEXT_NODE:
for child in element:
if type(child) == etree._Element:
return 0
if element.text is not None:
return 0
return 1
def _checkIgnoreText(self, element):
......@@ -301,8 +296,8 @@ class ERP5Diff:
Determine if text should be ignored by heuristics,
because ERP5 does not define any schema at the moment.
"""
for child in element.childNodes:
if child.nodeType == child.ELEMENT_NODE:
for child in element:
if type(child) == etree._Element:
return 1
return 0
......@@ -313,33 +308,33 @@ class ERP5Diff:
num_map = {}
count_map = {}
for element in element_list:
if element.tagName in num_map:
num_map[element.tagName] += 1
if element.tag in num_map:
num_map[element.tag] += 1
else:
num_map[element.tagName] = 1
count_map[element.tagName] = 0
num_map[element.tag] = 1
count_map[element.tag] = 0
path_list = []
for element in element_list:
# Check if this element has an attribute 'id'.
# Check if this element has an attribute 'id'.s
id_val = None
attr_map = element.attributes
for attr in attr_map.values():
if attr.name == 'id':
id_val = attr.nodeValue
attr_map = element.attrib
for name, value in attr_map.items():
if name == 'id':
id_val = value
break
if id_val is not None:
# If an attribute 'id' is present, uses the attribute for convenience.
path_list.append("%s[@id='%s']" % (element.tagName, id_val))
path_list.append("%s[@id='%s']" % (element.tag, id_val))
# Increase the count, for a case where other elements with the same tag name do not have
# 'id' attributes.
count_map[element.tagName] += 1
elif num_map[element.tagName] > 1:
path_list.append('%s[%d]' % (element.tagName, count_map[element.tagName]))
count_map[element.tagName] += 1
# 'id' attrib.
count_map[element.tag] += 1
elif num_map[element.tag] > 1:
path_list.append('%s[%d]' % (element.tag, count_map[element.tag]))
count_map[element.tag] += 1
else:
path_list.append(element.tagName)
path_list.append(element.tag)
return path_list
......@@ -348,8 +343,8 @@ class ERP5Diff:
Aggregate child elements of an element into a list.
"""
element_list = []
for child in element.childNodes:
if child.nodeType == child.ELEMENT_NODE:
for child in element:
if type(child) == etree._Element:
element_list.append(child)
return element_list
......@@ -358,9 +353,11 @@ class ERP5Diff:
Aggregate child text nodes of an element into a single string.
"""
text = ''
for child in element.childNodes:
if child.nodeType == child.TEXT_NODE:
text += child.nodeValue
if not self._hasChildren(element):
return element.text
for child in element:
if type(child) == etree._Element:
text += child.text
return text
def _compareChildNodes(self, old_element, new_element, path):
......@@ -433,40 +430,36 @@ class ERP5Diff:
Otherwise, it is assumed to be a file object which contains a XML document.
"""
old_doc, new_doc = self._makeDocList(old_xml, new_xml)
old_root_element = old_doc.documentElement
new_root_element = new_doc.documentElement
old_root_element = old_doc #.getroottree() #old_doc.documentElement
new_root_element = new_doc #.getroottree() #new_doc.documentElement
try:
impl = getDOMImplementation()
# XXX this namespace argument won't be handled correctly in minidom.
# XXX So work around that problem when outputting the result.
if self._result is not None:
self._result.close()
self._result = impl.createDocument(self._ns, 'xupdate:modifications', None)
attr_version = self._result.createAttributeNS(EMPTY_NAMESPACE, 'version')
attr_version.value = '1.0'
self._result.documentElement.setAttributeNodeNS(attr_version)
self._result = None
self._result = etree.Element('{%s}modifications' % self._ns, nsmap={'xupdate':self._ns})
self._result.set('version', '1.0')
if self._testElements(old_root_element, new_root_element):
self._testAttributes(old_root_element, new_root_element, '/')
self._compareChildNodes(old_root_element, new_root_element, '/')
else:
# These XML documents seem to be completely different...
if old_root_element.tagName != new_root_element.tagName:
self._xupdateRenameElement(new_root_element.tagName, '/')
if old_root_element.tag != new_root_element.tag:
self._xupdateRenameElement(new_root_element.tag, '/')
self._testAttributes(old_root_element, new_root_element, '/')
self._xupdateUpdateElement(new_root_element, '/')
finally:
del old_doc
del new_doc
def output(self, file=None):
def output(self, output_file=None):
"""
Output the result of parsing XML documents to 'file'.
Output the result of parsing XML documents to 'output_file'.
If it is not specified, stdout is assumed.
"""
if file is None:
file = sys.stdout
PrettyPrint(self._result.documentElement, stream=file, encoding='UTF-8')
if output_file is None:
output_file = sys.stdout
# stream
xml = etree.tostring(self._result, encoding='utf-8', pretty_print=True)
output_file.write(xml)
def outputString(self):
"""
......
This is a XUpdate Generator for ERP5.
See <http://www.xmldb.org/xupdate/index.html> for information on
See <http://xmldb-org.sourceforge.net/xupdate/> for information on
XUpdate.
See <http://erp5.org/> for information on ERP5.
......@@ -14,4 +14,673 @@ See the manpage erp5diff(1) or "erp5diff --help" for more information.
Also, you can use the module ERP5Diff from your Python script.
Do "pydoc ERP5Diff" for more information.
ERP5Diff Usage and its output example
=====================================
1. update the texts of the three elements
>>> from ERP5Diff import ERP5Diff
>>> erp5diff = ERP5Diff()
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <description type="text">description1 --- $sdfr&#231;_sdfs&#231;df_oisfsopf</description>
... <first_name type="string">Kamada</first_name>
... <last_name type="string">Kamada</last_name>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:24.700 GMT+9</time>
... </workflow_action>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <description type="text">description3 &#231;sdf__sdf&#231;&#231;&#231;_df___&amp;amp;&amp;amp;&#233;]]]&#176;&#176;&#176;&#176;&#176;&#176;</description>
... <first_name type="string">Tatuya</first_name>
... <last_name type="string">Kamada</last_name>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:24.703 GMT+9</time>
... </workflow_action>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:update select="/object[@id='313730']/description">description3 çsdf__sdfççç_df___&amp;amp;&amp;amp;é]]]°°°°°°</xupdate:update>
<xupdate:update select="/object[@id='313730']/first_name">Tatuya</xupdate:update>
<xupdate:update select="/object[@id='313730']/workflow_action[@id='edit_workflow']/time">2009/08/28 19:12:24.703 GMT+9</xupdate:update>
</xupdate:modifications>
2. update one element
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <description type="text">description2&#233;&#224;@ $*&amp;lt; &amp;lt; -----</description>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <description type="text">description3&#233;&#224;@ $*&amp;lt; &amp;lt; -----</description>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:update select="/object[@id='313730']/description">description3éà@ $*&amp;lt; &amp;lt; -----</xupdate:update>
</xupdate:modifications>
3. same
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <title type="string">Tatuya Kamada</title>
... <subject_list type="lines">&lt;?xml version="1.0"?&gt;&lt;marshal&gt;&lt;list id="i2"&gt;&lt;/list&gt;&lt;/marshal&gt;</subject_list>
... <first_name type="string">Kamada</first_name>
... <last_name type="string">Tatuya</last_name>
... <workflow_action id="edit_workflow">
... <actor type="string">tatuya</actor>
... <time type="date">2009/08/28 19:12:26.631 GMT+9</time>
... </workflow_action>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <title type="string">Tatuya Kamada</title>
... <subject_list type="lines">&lt;?xml version="1.0"?&gt;&lt;marshal&gt;&lt;list id="i2"&gt;&lt;/list&gt;&lt;/marshal&gt;</subject_list>
... <first_name type="string">Kamada</first_name>
... <last_name type="string">Tatuya</last_name>
... <workflow_action id="edit_workflow">
... <actor type="string">tatuya</actor>
... <time type="date">2009/08/28 19:12:26.631 GMT+9</time>
... </workflow_action>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0"/>
4. update the texts of the elements and remove an element
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <description type="text">description2&#233;&#224;@ $*&amp;lt; &amp;lt;&amp;lt;&amp;lt; -----</description>
... <language type="string">en</language>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.424 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.432 GMT+9</time>
... </workflow_action>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <description type="text">description1 --- $sdfr&#231;_sdfs&#231;df_oisfsopf</description>
... <language type="None"/>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.424 GMT+9</time>
... </workflow_action>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:update select="/object[@id='313730']/description">description1 --- $sdfrç_sdfsçdf_oisfsopf</xupdate:update>
<xupdate:update select="/object[@id='313730']/language/attribute::type">None</xupdate:update>
<xupdate:update select="/object[@id='313730']/language"/>
<xupdate:remove select="/object[@id='313730']/workflow_action[@id='edit_workflow']"/>
</xupdate:modifications>
5. update two elements includes some symbols
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <description type="text">description2&#233;&#224;@ $*&amp;lt;&amp;lt;-----&amp;gt;&amp;gt;</description>
... <language type="string">jp</language>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <description type="text">description4 sdflkmooo^^^^]]]]]{{{{{{{</description>
... <language type="string">ca</language>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:update select="/object[@id='313730']/description">description4 sdflkmooo^^^^]]]]]{{{{{{{</xupdate:update>
<xupdate:update select="/object[@id='313730']/language">ca</xupdate:update>
</xupdate:modifications>
6. update two date element which have same id
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:40.550 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:40.903 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:40.907 GMT+9</time>
... </workflow_action>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:40.550 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:40.905 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:40.910 GMT+9</time>
... </workflow_action>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:update select="/object[@id='313730']/workflow_action[@id='edit_workflow']/time">2009/08/28 19:12:40.905 GMT+9</xupdate:update>
<xupdate:update select="/object[@id='313730']/workflow_action[@id='edit_workflow']/time">2009/08/28 19:12:40.910 GMT+9</xupdate:update>
</xupdate:modifications>
7. insert and remove elements
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313731">
... <local_role type="tokens" id="tk">&lt;?xml version="1.0"?&gt;&lt;marshal&gt;&lt;tuple&gt;&lt;string&gt;Manager&lt;/string&gt;&lt;string&gt;Owner&lt;/string&gt;&lt;/tuple&gt;&lt;/marshal&gt;</local_role>
... <local_permission type="tokens" id="Access contents information">&lt;?xml version="1.0"?&gt;</local_permission>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313731">
... <local_role type="tokens" id="tatuya">&lt;?xml version="1.0"?&gt;&lt;marshal&gt;&lt;tuple&gt;&lt;string&gt;Owner&lt;/string&gt;&lt;/tuple&gt;&lt;/marshal&gt;</local_role>
... <local_permission type="tokens" id="Access contents information">&lt;?xml version="1.0"?&gt;</local_permission>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:remove select="/object[@id='313731']/local_role[@id='tk']"/>
<xupdate:insert-before select="/object[@id='313731']/local_permission[@id='Access contents information']">
<xupdate:element name="local_role"><xupdate:attribute name="type">tokens</xupdate:attribute><xupdate:attribute name="id">tatuya</xupdate:attribute>&lt;?xml version="1.0"?&gt;&lt;marshal&gt;&lt;tuple&gt;&lt;string&gt;Owner&lt;/string&gt;&lt;/tuple&gt;&lt;/marshal&gt;</xupdate:element>
</xupdate:insert-before>
</xupdate:modifications>
8. update xml in xml
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313731">
... <local_permission type="tokens" id="View">&lt;?xml version="1.0"?&gt;&lt;marshal&gt;&lt;tuple&gt;&lt;string&gt;Manager&lt;/string&gt;&lt;string&gt;Owner&lt;/string&gt;&lt;/tuple&gt;&lt;/marshal&gt;</local_permission>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313731">
... <local_permission type="tokens" id="View">&lt;?xml version="1.0"?&gt;&lt;marshal&gt;&lt;tuple&gt;&lt;string&gt;Assignee&lt;/string&gt;&lt;string&gt;Assignor&lt;/string&gt;&lt;string&gt;Associate&lt;/string&gt;&lt;string&gt;Auditor&lt;/string&gt;&lt;string&gt;Author&lt;/string&gt;&lt;string&gt;Manager&lt;/string&gt;&lt;string&gt;Owner&lt;/string&gt;&lt;/tuple&gt;&lt;/marshal&gt;</local_permission>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:update select="/object[@id='313731']/local_permission[@id='View']">&lt;?xml version="1.0"?&gt;&lt;marshal&gt;&lt;tuple&gt;&lt;string&gt;Assignee&lt;/string&gt;&lt;string&gt;Assignor&lt;/string&gt;&lt;string&gt;Associate&lt;/string&gt;&lt;string&gt;Auditor&lt;/string&gt;&lt;string&gt;Author&lt;/string&gt;&lt;string&gt;Manager&lt;/string&gt;&lt;string&gt;Owner&lt;/string&gt;&lt;/tuple&gt;&lt;/marshal&gt;</xupdate:update>
</xupdate:modifications>
9. rename element
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <first_name type="string">Tatuya</first_name>
... <last_name type="string">Kamada</last_name>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <given_name type="string">Tatuya</given_name>
... <family_name type="string">Kamada</family_name>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:remove select="/object[@id='313730']/first_name"/>
<xupdate:remove select="/object[@id='313730']/last_name"/>
<xupdate:append select="/object[@id='313730']">
<xupdate:element name="given_name"><xupdate:attribute name="type">string</xupdate:attribute>Tatuya</xupdate:element>
<xupdate:element name="family_name"><xupdate:attribute name="type">string</xupdate:attribute>Kamada</xupdate:element>
</xupdate:append>
</xupdate:modifications>
10. rename root element
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <id type="string">313730</id>
... <title type="string">Tatuya Kamada</title>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp6>
... <object portal_type="Person" id="313730">
... <id type="string">313730</id>
... <title type="string">Tatuya Kamada</title>
... </object>
... </erp6>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:rename select="/">erp6</xupdate:rename>
<xupdate:update select="/"><object portal_type="Person" id="313730">
<id type="string">313730</id>
<title type="string">Tatuya Kamada</title>
</object>
</xupdate:update>
</xupdate:modifications>
11. Update one attribute
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <local_role type="tokens" id="fab">&lt;?xml version="1.0"?&gt;&lt;marshal&gt;&lt;tuple&gt;&lt;string&gt;Owner&lt;/string&gt;&lt;/tuple&gt;&lt;/marshal&gt;</local_role>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <local_role type="ccc" id="fab">&lt;?xml version="1.0"?&gt;&lt;marshal&gt;&lt;tuple&gt;&lt;string&gt;Owner&lt;/string&gt;&lt;/tuple&gt;&lt;/marshal&gt;</local_role>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:update select="/object[@id='313730']/local_role[@id='fab']/attribute::type">ccc</xupdate:update>
</xupdate:modifications>
12. Update two attribute
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <local_permission attr_a='aaa' type="tokens" id="View">&lt;?xml version="1.0"?&gt;&lt;marshal&gt;&lt;tuple&gt;&lt;string&gt;Assignee&lt;/string&gt;&lt;string&gt;Assignor&lt;/string&gt;&lt;string&gt;Associate&lt;/string&gt;&lt;string&gt;Auditor&lt;/string&gt;&lt;string&gt;Author&lt;/string&gt;&lt;string&gt;Manager&lt;/string&gt;&lt;string&gt;Owner&lt;/string&gt;&lt;/tuple&gt;&lt;/marshal&gt;</local_permission>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <local_permission attr_a='ccc' type="ccc" id="View">&lt;?xml version="1.0"?&gt;&lt;marshal&gt;&lt;tuple&gt;&lt;string&gt;Assignee&lt;/string&gt;&lt;string&gt;Assignor&lt;/string&gt;&lt;string&gt;Associate&lt;/string&gt;&lt;string&gt;Auditor&lt;/string&gt;&lt;string&gt;Author&lt;/string&gt;&lt;string&gt;Manager&lt;/string&gt;&lt;string&gt;Owner&lt;/string&gt;&lt;/tuple&gt;&lt;/marshal&gt;</local_permission>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:update select="/object[@id='313730']/local_permission[@id='View']/attribute::attr_a">ccc</xupdate:update>
<xupdate:update select="/object[@id='313730']/local_permission[@id='View']/attribute::type">ccc</xupdate:update>
</xupdate:modifications>
13. Update three attribute
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <title attribute_a="aaa" attribute_b="bbb" attribute_c="ccc" type="string">Tatuya Kamada</title>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <title attribute_a="nnn" attribute_b="nnn" attribute_c="nnn" type="string">Tatuya Kamada</title>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:update select="/object[@id='313730']/title/attribute::attribute_b">nnn</xupdate:update>
<xupdate:update select="/object[@id='313730']/title/attribute::attribute_c">nnn</xupdate:update>
<xupdate:update select="/object[@id='313730']/title/attribute::attribute_a">nnn</xupdate:update>
</xupdate:modifications>
14. Remove one attribute
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <first_name attribute_a="aaa" attribute_b="bbb" attribute_c="ccc" type="string">Tatuya</first_name>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <first_name attribute_a="aaa" attribute_b="bbb" type="string">Tatuya</first_name>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:remove select="/object[@id='313730']/first_name/attribute::attribute_c"/>
</xupdate:modifications>
15. Remove two attribute
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <first_name attribute_a="aaa" attribute_b="bbb" attribute_c="ccc" type="string">Tatuya</first_name>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <first_name attribute_a="aaa" type="string">Tatuya</first_name>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:remove select="/object[@id='313730']/first_name/attribute::attribute_b"/>
<xupdate:remove select="/object[@id='313730']/first_name/attribute::attribute_c"/>
</xupdate:modifications>
16. Remove three attribute
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <first_name attribute_a="aaa" attribute_b="bbb" attribute_c="ccc" type="string">Tatuya</first_name>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <first_name type="string">Tatuya</first_name>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:remove select="/object[@id='313730']/first_name/attribute::attribute_b"/>
<xupdate:remove select="/object[@id='313730']/first_name/attribute::attribute_c"/>
<xupdate:remove select="/object[@id='313730']/first_name/attribute::attribute_a"/>
</xupdate:modifications>
17. Append one attribute
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <last_name type="string">Kamada</last_name>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <last_name attribute_a="aaa" type="string">Kamada</last_name>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:append select="/object[@id='313730']/last_name">
<xupdate:attribute name="attribute_a">aaa</xupdate:attribute>
</xupdate:append>
</xupdate:modifications>
18. Append two attribute
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <last_name type="string">Kamada</last_name>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <last_name attribute_a="aaa" attribute_b="bbb" type="string">Kamada</last_name>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:append select="/object[@id='313730']/last_name">
<xupdate:attribute name="attribute_b">bbb</xupdate:attribute>
<xupdate:attribute name="attribute_a">aaa</xupdate:attribute>
</xupdate:append>
</xupdate:modifications>
19. Append three attribute
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <last_name type="string">Kamada</last_name>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <last_name attribute_a="aaa" attribute_b="bbb" attribute_c="ccc" type="string">Kamada</last_name>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:append select="/object[@id='313730']/last_name">
<xupdate:attribute name="attribute_b">bbb</xupdate:attribute>
<xupdate:attribute name="attribute_c">ccc</xupdate:attribute>
<xupdate:attribute name="attribute_a">aaa</xupdate:attribute>
</xupdate:append>
</xupdate:modifications>
20. Remove some elements that have same id
This is an unexpected case for current ERP5Diff alogrithm. So current ERP5Diff
does not work as bellow example. This is a known bug.
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.424 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.432 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.434 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.432 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.430 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.428 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.426 GMT+9</time>
... </workflow_action>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.424 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.430 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.428 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.426 GMT+9</time>
... </workflow_action>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:remove select="/object[@id='313730']/workflow_action[2]"/>
<xupdate:remove select="/object[@id='313730']/workflow_action[3]"/>
<xupdate:remove select="/object[@id='313730']/workflow_action[4]"/>
</xupdate:modifications>
21. Modify two elements that have same id
As well as No.20. This a known bug, too.
>>> old_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.424 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.432 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.434 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.436 GMT+9</time>
... </workflow_action>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Person" id="313730">
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/28 19:12:34.424 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/29 19:12:34.432 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/30 19:12:34.434 GMT+9</time>
... </workflow_action>
... <workflow_action id="edit_workflow">
... <time type="date">2009/08/31 19:12:34.436 GMT+9</time>
... </workflow_action>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:update select="/object[@id='313730']/workflow_action[2]/time">2009/08/29 19:12:34.432 GMT+9</xupdate:update>
<xupdate:update select="/object[@id='313730']/workflow_action[3]/time">2009/08/30 19:12:34.434 GMT+9</xupdate:update>
<xupdate:update select="/object[@id='313730']/workflow_action[4]/time">2009/08/31 19:12:34.436 GMT+9</xupdate:update>
</xupdate:modifications>
22. Modify attributes of sequencial objects
ERP5Diff creates target index from 0 as a XPath string, but according to the
definition of the XPath specification <http://www.w3.org/TR/xpath>, it is wrong.
It should be start from 1. This is a known problem.
>>> old_xml = """
... <erp5>
... <object portal_type="Test">
... <title>A</title>
... </object>
... <object portal_type="Test">
... <title>A</title>
... </object>
... <object portal_type="Test">
... <title>A</title>
... </object>
... </erp5>
... """
>>> new_xml = """
... <erp5>
... <object portal_type="Test">
... <title>A</title>
... </object>
... <object portal_type="Test">
... <title>B</title>
... </object>
... <object portal_type="Test">
... <title>C</title>
... </object>
... </erp5>
... """
>>> erp5diff.compare(old_xml, new_xml)
>>> erp5diff.output()
<xupdate:modifications xmlns:xupdate="http://www.xmldb.org/xupdate" version="1.0">
<xupdate:update select="/object[2]/title">B</xupdate:update>
<xupdate:update select="/object[3]/title">C</xupdate:update>
</xupdate:modifications>
- 2003-12-04, Yoshinori OKUJI <yo@nexedi.com>
- 2009-09-15, Tatuya Kamada <tatuya@nexedi.com>
##############################################################################
#
# Copyright (c) 2006 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Bootstrap a buildout-based project
Simply run this script in a directory containing a buildout.cfg.
The script accepts buildout command-line options, so you can
use the -c option to specify an alternate configuration file.
$Id: bootstrap.py 77225 2007-06-29 09:20:13Z dobe $
"""
import os, shutil, sys, tempfile, urllib2
tmpeggs = tempfile.mkdtemp()
try:
import pkg_resources
except ImportError:
ez = {}
exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
).read() in ez
ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
import pkg_resources
cmd = 'from setuptools.command.easy_install import main; main()'
if sys.platform == 'win32':
cmd = '"%s"' % cmd # work around spawn lamosity on windows
ws = pkg_resources.working_set
assert os.spawnle(
os.P_WAIT, sys.executable, sys.executable,
'-c', cmd, '-mqNxd', tmpeggs, 'zc.buildout',
dict(os.environ,
PYTHONPATH=
ws.find(pkg_resources.Requirement.parse('setuptools')).location
),
) == 0
ws.add_entry(tmpeggs)
ws.require('zc.buildout')
import zc.buildout.buildout
zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
shutil.rmtree(tmpeggs)
[buildout]
develop = .
parts = test
[test]
recipe = zc.recipe.testrunner
eggs = erp5diff
from zope.interface import Interface
class IERP5Diff(Interface):
"""
Make a difference between two XML documents using XUpdate.
Use some assumptions in ERP5's data representation.
The strategy is:
1. Find a matching element among elements of the other XML document at the same depth.
2. Use the first matching element, even if there can be other better elements.
3. Assume that two elements are matching, if the tag names are identical. If either of
them has an attribute 'id', the values of the attrib 'id' also must be identical.
4. Don't use xupdate:rename for elements. It should be quite rare to rename tag names
in ERP5, and it is too complicated to support this renaming.
5. Ignore some types of nodes, such as EntityReference and Comment, because they are not
used in ERP5 XML documents.
"""
def compare(self, old_xml, new_xml):
"""
Compare two given XML documents.
If an argument is a string, it is assumed to be a XML document itself.
Otherwise, it is assumed to be a file object which contains a XML document.
"""
def output(self, output_file=None):
"""
Output the result of parsing XML documents to 'output_file'.
If it is not specified, stdout is assumed.
"""
def outputString(self):
"""
Return the result as a string object.
"""
def main():
"""
The main routine of ERP5Diff.
"""
\ No newline at end of file
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
#! /usr/bin/env python
from distutils.core import setup
from setuptools import setup, find_packages
setup(name="erp5diff",
version="0.1",
......@@ -9,7 +9,11 @@ setup(name="erp5diff",
author_email="yo@nexedi.com",
url="http://nexedi.com",
license="GPL",
packages=find_packages(),
py_modules=["ERP5Diff"],
scripts=["erp5diff"],
data_files=[('share/man/man1', ['erp5diff.1'])]
data_files=[('share/man/man1', ['erp5diff.1'])],
install_requires=[ 'zope.interface', 'lxml'],
include_package_data=True,
zip_safe=False,
)
from zope import interface
import zope.testing
import unittest
OPTIONFLAGS = (zope.testing.doctest.ELLIPSIS |
zope.testing.doctest.NORMALIZE_WHITESPACE)
def test_suite():
doctests = ('README',)
globs = dict(interface=interface)
return unittest.TestSuite((
zope.testing.doctest.DocFileSuite(doctest,
optionflags=OPTIONFLAGS,
globs=globs,
) for doctest in doctests
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment