[Erp5-report] r29905 - in /erp5/trunk/utils/xml_marshaller: ./ xml_marshaller/
nobody at svn.erp5.org
nobody at svn.erp5.org
Thu Oct 22 10:42:41 CEST 2009
Author: nicolas
Date: Thu Oct 22 10:42:38 2009
New Revision: 29905
URL: http://svn.erp5.org?rev=29905&view=rev
Log:
Initial import of xml_marshaller module implemented in lxml.
Major part of code come from no longer maintained pyxml library.
Fully compatible with PyXML implementation, enable namespace support for
XML Input/Output.
Added:
erp5/trunk/utils/xml_marshaller/
erp5/trunk/utils/xml_marshaller/setup.py
erp5/trunk/utils/xml_marshaller/xml_marshaller/
erp5/trunk/utils/xml_marshaller/xml_marshaller/__init__.py
erp5/trunk/utils/xml_marshaller/xml_marshaller/xml_marshaller.py
Added: erp5/trunk/utils/xml_marshaller/setup.py
URL: http://svn.erp5.org/erp5/trunk/utils/xml_marshaller/setup.py?rev=29905&view=auto
==============================================================================
--- erp5/trunk/utils/xml_marshaller/setup.py (added)
+++ erp5/trunk/utils/xml_marshaller/setup.py [utf8] Thu Oct 22 10:42:38 2009
@@ -1,0 +1,30 @@
+from setuptools import setup, find_packages
+import sys, os
+
+version = '0.9'
+
+setup(name='xml_marshaller',
+ version=version,
+ description="Converting Python objects to XML and back again.",
+ long_description="""
+Marshals simple Python data types into a custom XML format.
+The Marshaller and Unmarshaller classes can be subclassed in order
+to implement marshalling into a different XML DTD.""",
+classifiers=['Development Status :: 4 - Beta',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: Python License (CNRI Python License)',
+ 'Operating System :: OS Independent',
+ 'Topic :: Text Processing :: Markup :: XML'],
+ keywords='XML marshaller',
+ author='XML-SIG',
+ author_email='xml-sig at python.org',
+ url='http://www.python.org/community/sigs/current/xml-sig/',
+ license='Python License (CNRI Python License)',
+ packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=['lxml',],
+ entry_points="""
+ # -*- Entry points: -*-
+ """,
+ )
Added: erp5/trunk/utils/xml_marshaller/xml_marshaller/__init__.py
URL: http://svn.erp5.org/erp5/trunk/utils/xml_marshaller/xml_marshaller/__init__.py?rev=29905&view=auto
==============================================================================
--- erp5/trunk/utils/xml_marshaller/xml_marshaller/__init__.py (added)
+++ erp5/trunk/utils/xml_marshaller/xml_marshaller/__init__.py [utf8] Thu Oct 22 10:42:38 2009
@@ -1,0 +1,1 @@
+#
Added: erp5/trunk/utils/xml_marshaller/xml_marshaller/xml_marshaller.py
URL: http://svn.erp5.org/erp5/trunk/utils/xml_marshaller/xml_marshaller/xml_marshaller.py?rev=29905&view=auto
==============================================================================
--- erp5/trunk/utils/xml_marshaller/xml_marshaller/xml_marshaller.py (added)
+++ erp5/trunk/utils/xml_marshaller/xml_marshaller/xml_marshaller.py [utf8] Thu Oct 22 10:42:38 2009
@@ -1,0 +1,636 @@
+# -*- coding: utf-8 -*-
+"""Originals Authors http://sourceforge.net/projects/pyxml/
+Under Python License (CNRI Python License)
+Patched by Nicolas Delaby nicolas at nexedi.com to support namespaces
+"""
+
+# Generic class for marshalling simple Python data types into an XML-based
+# format. The interface is the same as the built-in module of the
+# same name, with four functions:
+# dump(value, file), load(file)
+# dumps(value), loads(string)
+from types import ClassType
+import sys
+from xml.sax.saxutils import escape, unescape
+import lxml.sax
+from lxml.sax import ElementTreeContentHandler
+from lxml import etree
+from lxml.builder import ElementMaker
+from cStringIO import StringIO
+
+# Basic marshaller class, customizable by overriding it and
+# changing various attributes and methods.
+# It's also used as a SAX handler, which may be a good idea but may
+# also be a stupid hack.
+MARSHAL_PREFIX = 'marshal'
+
+def version_independent_cmp(a, b):
+ ta = type(a)
+ tb = type(b)
+ if ta is not tb:
+ return cmp(ta.__name__, tb.__name__)
+ return cmp(a, b)
+
+class Marshaller(object):
+
+ # Names of elements. These are specified as class attributes
+ # because simple things like integers are often handled in the
+ # same way, and only the element names change.
+ def __init__(self, prefix=MARSHAL_PREFIX, namespace_uri=None, as_tree=False):
+ self.as_tree = as_tree
+ if namespace_uri:
+ nsmap = {prefix: namespace_uri}
+ else:
+ nsmap = {}
+ E = ElementMaker(namespace=namespace_uri, nsmap=nsmap)
+ self.tag_root = E.marshal
+ self.tag_int = E.int
+ self.tag_float = E.float
+ self.tag_long = E.long
+ self.tag_string = E.string
+ self.tag_tuple = E.tuple
+ self.tag_list = E.list
+ self.tag_dictionary = E.dictionary
+ self.tag_complex = E.complex
+ self.tag_reference = E.reference
+ self.tag_code = E.code
+ self.tag_none = E.none
+ self.tag_instance = E.object
+
+ # The four basic functions that form the caller's interface
+ def dump(self, value, file):
+ "Write the value on the open file"
+ kw = {'id': 1}
+ xml_tree = self.m_root(value, kw)
+
+ # XXX should this just loop through the L and call file.write
+ # for each item?
+ file.write(etree.tostring(xml_tree))
+
+ def dumps(self, value):
+ "Marshal value, returning the resulting string"
+ kw = {'id': 1}
+ # now uses m_root for proper root element handling
+ xml_tree = self.m_root(value, kw)
+ if self.as_tree:
+ return xml_tree
+ else:
+ return etree.tostring(xml_tree)
+
+ # IMPORTANT NOTE: The proper entry point to marshal
+ # an object is m_root; the public marshalling
+ # methods dump and dumps use m_root().
+ #
+ # This function gets the name of the
+ # type of the object being marshalled, and calls the
+ # m_<typename> method. This method must return a list of strings,
+ # which will be returned to the caller.
+ #
+ # (This function can be called recursively, so it shouldn't
+ # return just a single. The top-level caller will perform a
+ # single string.join to get the resulting XML document.
+ #
+ # dict is a dictionary whose keys are used to store the IDs of
+ # objects that have already been marshalled, in order to allow
+ # writing a reference to them.
+ #
+ # XXX there should be some way to disable the automatic generation of
+ # references to already-marshalled objects
+
+ def _marshal(self, value, kw):
+ t = type(value)
+ i = str(id(value))
+ if kw.has_key(i):
+ return self.m_reference(value, kw)
+ else:
+ method_id = 'm_%s' % (type(value).__name__,)
+ callable_method = getattr(self, method_id, None)
+ if callable_method is not None:
+ return callable_method(value, kw)
+ elif object in value.__class__.mro():
+ #Fallback to instance for new style Classes
+ #http://www.pdb.org/doc/newstyle/
+ return self.m_instance(value, kw)
+ else:
+ return self.m_unimplemented(value, kw)
+
+ # Utility function, used for types that aren't implemented
+ def m_unimplemented(self, value, kw):
+ raise ValueError, ("Marshalling of object %r unimplemented or not supported in this DTD" % value)
+
+ # The real entry point for marshalling, to handle properly
+ # and cleanly any root tag or tags necessary for the marshalled
+ # output.
+ def m_root(self, value, kw):
+ return self.tag_root(self._marshal(value, kw))
+
+ #
+ # All the generic marshalling functions for various Python types
+ #
+ def m_reference(self, value, kw):
+ # This object has already been marshalled, so
+ # emit a reference element.
+ i = kw[str(id(value))]
+ return self.tag_reference(id='i%s' % i)
+
+ def m_string(self, value, kw):
+ return self.tag_string(escape(str(value)))
+
+ # Since Python 2.2, the string type has a name of 'str'
+ # To avoid having to rewrite all classes that implement m_string
+ # we delegate m_str to m_string.
+ def m_str(self, value, kw):
+ return self.m_string(value, kw)
+
+ def m_int(self, value, kw):
+ return self.tag_int(str(value))
+
+ def m_float(self, value, kw):
+ return self.tag_float(str(value))
+
+ def m_long(self, value, kw):
+ value = str(value)
+ if value[-1] == 'L':
+ # some Python versions append and 'L'
+ value = value[:-1]
+ return self.tag_long(value)
+
+ def m_tuple(self, value, kw):
+ xml_tree = self.tag_tuple()
+ for elem in value:
+ xml_tree.append(self._marshal(elem, kw))
+ return xml_tree
+
+ def m_list(self, value, kw):
+ kw['id'] += 1
+ i = str(kw['id'])
+ kw[str(id(value))] = i
+ kw[i] = value
+ xml_tree = self.tag_list(id='i%s' % i)
+ for elem in value:
+ xml_tree.append(self._marshal(elem, kw))
+ return xml_tree
+
+ def m_dictionary(self, value, kw):
+ kw['id'] += 1
+ i = str(kw['id'])
+ kw[str(id(value))] = i
+ kw[i] = value
+ xml_tree = self.tag_dictionary(id='i%s' % i)
+ item_list = value.items()
+ # Sort the items to allow reproducable results across Python
+ # versions
+ item_list.sort(version_independent_cmp)
+ for key, v in item_list:
+ xml_tree.append(self._marshal(key, kw))
+ xml_tree.append(self._marshal(v, kw))
+ return xml_tree
+
+ # Python 2.2 renames dictionary to dict.
+ def m_dict(self, value, kw):
+ return self.m_dictionary(value, kw)
+
+ def m_None(self, value, kw):
+ return self.tag_none()
+
+ # Python 2.2 renamed the type of None to NoneTye
+ def m_NoneType(self, value, kw):
+ return self.m_None(value, kw)
+
+ def m_complex(self, value, kw):
+ return self.tag_complex('%s %s' % (value.real, value.imag))
+
+ def m_code(self, value, kw):
+ # The full information about code objects is only available
+ # from the C level, so we'll use the built-in marshal module
+ # to convert the code object into a string, and include it in
+ # the HTML.
+ import marshal, base64
+ encoded_value = base64.encodestring(marshal.dumps(value))
+ return self.tag_code(encoded_value)
+
+ def m_instance(self, value, kw):
+ kw['id'] += 1
+ i = str(kw['id'])
+ kw[str(id(value))] = i
+ kw[i] = value
+ cls = value.__class__
+ xml_tree = self.tag_instance(id='i%s' % i, module=cls.__module__)
+ xml_tree.attrib.update({'class': cls.__name__})
+
+ # Check for pickle's __getinitargs__
+ if hasattr(value, '__getinitargs__'):
+ args = value.__getinitargs__()
+ len(args) # XXX Assert it's a sequence
+ else:
+ args = ()
+
+ xml_tree.append(self._marshal(args, kw))
+
+ # Check for pickle's __getstate__ function
+ try:
+ getstate = value.__getstate__
+ except AttributeError:
+ stuff = value.__dict__
+ else:
+ stuff = getstate()
+ xml_tree.append(self._marshal(stuff, kw))
+ return xml_tree
+
+# These values are used as markers in the stack when unmarshalling
+# one of the structures below. When a <tuple> tag is encountered, for
+# example, the TUPLE object is pushed onto the stack, and further
+# objects are processed. When the </tuple> tag is found, the code
+# looks back into the stack until TUPLE is found; all the higher
+# objects are then collected into a tuple. Ditto for lists...
+
+TUPLE = {}
+LIST = {}
+DICT = {}
+
+class Unmarshaller(ElementTreeContentHandler):
+ # This dictionary maps element names to the names of starting and ending
+ # functions to call when unmarshalling them. My convention is to
+ # name them um_start_foo and um_end_foo, but do whatever you like.
+
+ unmarshal_meth = {
+ 'marshal': ('um_start_root', None),
+ 'int': ('um_start_int', 'um_end_int'),
+ 'float': ('um_start_float', 'um_end_float'),
+ 'long': ('um_start_long', 'um_end_long'),
+ 'string': ('um_start_string', 'um_end_string'),
+ 'tuple': ('um_start_tuple', 'um_end_tuple'),
+ 'list': ('um_start_list', 'um_end_list'),
+ 'dictionary': ('um_start_dictionary', 'um_end_dictionary'),
+ 'complex': ('um_start_complex', 'um_end_complex'),
+ 'reference': ('um_start_reference', None),
+ 'code': ('um_start_code', 'um_end_code'),
+ 'none': ('um_start_none', 'um_end_none'),
+ 'object': ('um_start_instance', 'um_end_instance')
+ }
+
+ def __init__(self):
+ # Find the named methods, and convert them to the actual
+ # method object.
+
+ d = {}
+ for key, (sm, em) in self.unmarshal_meth.items():
+ if sm is not None:
+ sm = getattr(self, sm)
+ if em is not None:
+ em = getattr(self, em)
+ d[key] = sm, em
+ self.unmarshal_meth = d
+ self._clear()
+ ElementTreeContentHandler.__init__(self)
+
+ def _clear(self):
+ """
+ Protected method to (re)initialize the object into
+ a steady state. Performed by __init__ and _load.
+ """
+ self.data_stack = []
+ self.kw = {}
+ self.accumulating_chars = 0
+
+ def load(self, file):
+ "Unmarshal one value, reading it from a file-like object"
+ # Instantiate a new object; unmarshalling isn't thread-safe
+ # because it modifies attributes on the object.
+ m = self.__class__()
+ return m._load(file)
+
+ def loads(self, string):
+ "Unmarshal one value from a string"
+ # Instantiate a new object; unmarshalling isn't thread-safe
+ # because it modifies attributes on the object.
+ m = self.__class__()
+ file = StringIO(string)
+ return m._load(file)
+
+ # Basic unmarshalling routine; it creates a SAX XML parser,
+ # registers self as the SAX handler, parses it, and returns
+ # the only thing on the data stack.
+
+ def _load(self, file):
+ "Read one value from the open file"
+ lxml.sax.saxify(lxml.etree.parse(file), self)
+ #p = saxexts.make_parser()
+ #p.setDocumentHandler(self)
+ #p.parseFile(file)
+ assert len(self.data_stack) == 1
+ # leave the instance in a steady state
+ result = self.data_stack[0]
+ self._clear()
+ return result
+
+ # find_class() is copied from pickle.py
+ def find_class(self, module, name):
+ module = __import__(module, globals(), locals(), [''])
+ return getattr(module, name)
+
+
+ # SAXlib handler methods.
+ #
+ # Unmarshalling is done by creating a stack (a Python list) on
+ # starting the root element. When the .character() method may be
+ # called, the last item on the stack must be a list; the
+ # characters will be appended to that list.
+ #
+ # The starting methods must, at minimum, push a single list onto
+ # the stack, as um_start_generic does.
+ #
+ # The ending methods can then do string.join() on the list on the
+ # top of the stack, and convert it to whatever Python type is
+ # required. The resulting Python object then replaces the list on
+ # the top of the stack.
+ #
+
+ def startElement(self, name, attrs):
+ # Call the start unmarshalling method, if specified
+ sm, em = self.unmarshal_meth[name]
+ if sm is not None:
+ return sm(name, attrs)
+
+ def startElementNS(self, ns_name, name, attrs):
+ # Call the start unmarshalling method, if specified
+ ns_uri, local_name = ns_name
+ sm, em = self.unmarshal_meth[local_name]
+ if sm is not None:
+ attrib = {}
+ [attrib.update({k[1]: v}) for k, v in attrs.items()]
+ return sm(local_name, attrib)
+
+ def characters(self, data):
+ if self.accumulating_chars:
+ self.data_stack[-1].append(data)
+
+ def endElement(self, name):
+ # Call the ending method
+ sm, em = self.unmarshal_meth[name]
+ if em is not None:
+ em(name)
+
+ def endElementNS(self, ns_name, name):
+ # Call the ending method
+ ns_uri, local_name = ns_name
+ sm, em = self.unmarshal_meth[local_name]
+ if em is not None:
+ em(local_name)
+
+ # um_start_root is really a "sentinel" method
+ # which ensures that the unmarshaller is in a steady,
+ # "empty" state.
+ def um_start_root(self, name, attrs):
+ if self.kw or self.data_stack:
+ raise ValueError, "root element %s found elsewhere than root" \
+ % repr(name)
+
+ def um_start_reference(self, name, attrs):
+ assert attrs.has_key('id')
+ id = attrs['id']
+ assert self.kw.has_key(id)
+ self.data_stack.append(self.kw[id])
+
+ def um_start_generic(self, name, attrs):
+ self.data_stack.append([])
+ self.accumulating_chars = 1
+
+ um_start_float = um_start_long = um_start_string = um_start_generic
+ um_start_complex = um_start_code = um_start_none = um_start_generic
+ um_start_int = um_start_generic
+
+ def um_end_string(self, name):
+ ds = self.data_stack
+ # might need to convert unicode string to byte string
+ ds[-1] = unescape(''.join(ds[-1]))
+ self.accumulating_chars = 0
+
+ def um_end_int(self, name):
+ ds = self.data_stack
+ ds[-1] = ''.join(ds[-1])
+ ds[-1] = int(ds[-1])
+ self.accumulating_chars = 0
+
+ def um_end_long(self, name):
+ ds = self.data_stack
+ ds[-1] = ''.join(ds[-1])
+ ds[-1] = long(ds[-1])
+ self.accumulating_chars = 0
+
+ def um_end_float(self, name):
+ ds = self.data_stack
+ ds[-1] = ''.join(ds[-1])
+ ds[-1] = float(ds[-1])
+ self.accumulating_chars = 0
+
+ def um_end_none(self, name):
+ ds = self.data_stack
+ ds[-1] = None
+ self.accumulating_chars = 0
+
+ def um_end_complex(self, name):
+ ds = self.data_stack
+ c = ''.join(ds[-1])
+ c = c.split()
+ c = float(c[0]) + float(c[1])*1j
+ ds[-1:] = [c]
+ self.accumulating_chars = 0
+
+ def um_end_code(self, name):
+ import marshal, base64
+ ds = self.data_stack
+ s = ''.join(ds[-1])
+ s = base64.decodestring(s)
+ ds[-1] = marshal.loads(s)
+ self.accumulating_chars = 0
+
+ # Trickier stuff: dictionaries, lists, tuples.
+ def um_start_list(self, name, attrs):
+ self.data_stack.append(LIST)
+ L = []
+ if attrs.has_key('id'):
+ id = attrs['id']
+ self.kw[id] = L
+ self.data_stack.append(L)
+
+ def um_end_list(self, name):
+ ds = self.data_stack
+ for index in range(len(ds)-1, -1, -1):
+ if ds[index] is LIST:
+ break
+ assert index != -1
+ L = ds[index + 1]
+ L[:] = ds[index + 2:len(ds)]
+ ds[index:] = [L]
+
+ def um_start_tuple(self, name, attrs):
+ self.data_stack.append(TUPLE)
+
+ def um_end_tuple(self, name):
+ ds = self.data_stack
+ for index in range(len(ds) - 1, -1, -1):
+ if ds[index] is TUPLE:
+ break
+ assert index != -1
+ t = tuple(ds[index+1:len(ds)])
+ ds[index:] = [t]
+
+ # Dictionary elements, in the generic format, must always have an
+ # even number of objects contained inside them. These objects are
+ # treated as alternating keys and values.
+ def um_start_dictionary(self, name, attrs):
+ self.data_stack.append(DICT)
+ d = {}
+ if attrs.has_key('id'):
+ id = attrs['id']
+ self.kw[id] = d
+ self.data_stack.append(d)
+
+ def um_end_dictionary(self, name):
+ ds = self.data_stack
+ for index in range(len(ds) - 1, -1, -1):
+ if ds[index] is DICT:
+ break
+ assert index != -1
+ d = ds[index + 1]
+ for i in range(index + 2, len(ds), 2):
+ key = ds[i]
+ value = ds[i+1]
+ d[key] = value
+ ds[index:] = [d]
+
+ def um_start_instance(self, name, attrs):
+ module = attrs['module']
+ classname = attrs['class']
+ value = _EmptyClass()
+ if attrs.has_key('id'):
+ id = attrs['id']
+ self.kw[id] = value
+ self.data_stack.append(value)
+ self.data_stack.append(module)
+ self.data_stack.append(classname)
+
+ def um_end_instance(self, name):
+ value, module, classname, initargs, kw = self.data_stack[-5:]
+ klass = self.find_class(module, classname)
+ instantiated = 0
+ if (not initargs and isinstance(klass, ClassType) and
+ not hasattr(klass, '__getinitargs__')):
+ value.__class__ = klass
+ instantiated = 1
+
+ if not instantiated:
+ try:
+ # Uh oh... we need to call the constructor with the initial
+ # arguments, but we also have to preserve the identity of
+ # the object, to keep recursive objects right.
+ v2 = apply(klass, initargs)
+ except TypeError, err:
+ raise TypeError, 'in constructor for %s: %s' % (
+ klass.__name__, str(err)), sys.exc_info()[2]
+ else:
+ for k, v in v2.__dict__.items():
+ setattr(value, k, v)
+
+ # Now set the object's attributes from the marshalled dictionary
+ for k, v in kw.items():
+ setattr(value, k, v)
+ self.data_stack[-5:] = [value]
+
+# Helper class for instance unmarshalling
+class _EmptyClass:
+ pass
+
+# module functions for procedural use of module
+_m = Marshaller()
+_m_ns = Marshaller(namespace_uri='http://www.erp5.org/namespaces/marshaller')
+dump = _m.dump
+dumps = _m.dumps
+dump_ns = _m_ns.dump
+dumps_ns = _m_ns.dumps
+_um = Unmarshaller()
+load = _um.load
+loads = _um.loads
+
+del _m, _um, _m_ns
+
+def test(load, loads, dump, dumps, test_values,
+ do_assert=1):
+ # Try all the above bits of data
+ for item in test_values:
+ s = dumps(item)
+ print s
+ output = loads(s)
+ # Try it from a file
+ file = StringIO()
+ dump(item, file)
+ file.seek(0)
+ output2 = load(file)
+ if do_assert:
+ assert item == output and item == output2 and output == output2
+
+
+# Classes used in the test suite
+class _A:
+ def __repr__(self):
+ return '<A instance>'
+class _B(object):
+ def __repr__(self):
+ return '<B instance>'
+
+def runtests(namespace_uri=None):
+ print "Testing XML marshalling..."
+
+ L = [None, 1, pow(2, 123L), 19.72, 1+5j,
+ "here is a string & a <fake tag>",
+ (1, 2, 3),
+ ['alpha', 'beta', 'gamma'],
+ {'key': 'value', 1: 2}
+ ]
+ if namespace_uri:
+ test(load, loads, dump_ns, dumps_ns, L)
+ test(load, loads, dump, dumps, L)
+
+ instance = _A() ; instance.subobject = _B()
+ instance.subobject.list=[None, 1, pow(2, 123L), 19.72, 1+5j,
+ "here is a string & a <fake tag>"]
+ instance.self = instance
+ L = [instance]
+
+ if namespace_uri:
+ test(load, loads, dump_ns, dumps_ns, L, do_assert=0)
+ test(load, loads, dump, dumps, L, do_assert=0)
+
+ recursive_list = [None, 1, pow(3, 65L), {1: 'spam', 2: 'eggs'},
+ '<fake tag>', 1+5j]
+ recursive_list.append(recursive_list)
+ if namespace_uri:
+ test(load, loads, dump_ns, dumps_ns, [recursive_list], do_assert=0)
+ test(load, loads, dump, dumps, [recursive_list], do_assert=0)
+
+ # Try unmarshalling XML with extra harmless whitespace (as if it was
+ # pretty-printed)
+ output = loads("""<?xml version="1.0"?>
+<marshal>
+ <tuple>
+ <float> 1.0 </float>
+ <string>abc</string>
+ <list id="i2" />
+ </tuple>
+</marshal>""")
+ assert output == (1.0, 'abc', [])
+
+ output = loads("""<?xml version="1.0"?>
+<marshal:marshal xmlns:marshal="http://www.erp5.org/namespaces/marshaller">
+ <marshal:tuple>
+ <marshal:float> 1.0 </marshal:float>
+ <marshal:string>abc</marshal:string>
+ <marshal:list id="i2" />
+ </marshal:tuple>
+</marshal:marshal>""")
+ assert output == (1.0, 'abc', [])
+
+if __name__ == '__main__':
+ runtests()
+ runtests(namespace_uri='http://www.erp5.org/namespaces/marshaller')
More information about the Erp5-report
mailing list