Skip to content

Commit

Permalink
Merge pull request #28 from vodik/cleaner
Browse files Browse the repository at this point in the history
Add sippfmt utility
  • Loading branch information
vodik authored Aug 24, 2017
2 parents b989a31 + 830ffe0 commit c1345f3
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 1 deletion.
Empty file added pysipp/cli/__init__.py
Empty file.
107 changes: 107 additions & 0 deletions pysipp/cli/minidom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import functools
from xml.dom import minidom
from xml.dom.minidom import getDOMImplementation, parse, Node # noqa


@functools.total_ordering
class AttributeSorter(object):
"""Special attribute sorter.
Sort elements in alphabetical order, but let special important
attributes bubble to the front.
"""

__special__ = ('request', 'response')

def __init__(self, obj):
self.obj = obj

def __lt__(self, other):
if self.obj in self.__special__:
return True
elif other.obj in self.__special__:
return False
return self.obj < other.obj


class Newline(minidom.CharacterData):
"""Minidom node which represents a newline."""

__slots__ = ()

nodeType = Node.TEXT_NODE
nodeName = "#text"
attributes = None

def writexml(self, writer, indent="", addindent="", newl=""):
"""Emit a newline."""
writer.write(newl)


def createSeparator(self):
"""Create a document element which represents a empty line."""
c = Newline()
c.ownerDocument = self
return c


minidom.Document.createSeparator = createSeparator


def monkeypatch_scenario_xml(self, writer, indent="", addindent="", newl=""):
"""Ensure there's a newline before the scenario tag.
It needs to be there or SIPp otherwise seems to have trouble
parsing the document.
"""
writer.write('\n')
minidom.Element.writexml(self, writer, indent, addindent, newl)


def monkeypatch_element_xml(self, writer, indent="", addindent="", newl=""):
"""Format scenario step elements.
Ensures a stable and predictable order to the attributes of these
elements with the most important information always coming first,
then let all other elements follow in alphabetical order.
"""
writer.write("{}<{}".format(indent, self.tagName))

attrs = self._get_attributes()
a_names = sorted(attrs.keys(), key=AttributeSorter)

for a_name in a_names:
writer.write(" {}=\"".format(a_name))
minidom._write_data(writer, attrs[a_name].value)
writer.write("\"")
if self.childNodes:
writer.write(">")
if (len(self.childNodes) == 1 and
self.childNodes[0].nodeType == Node.TEXT_NODE):
self.childNodes[0].writexml(writer, '', '', '')
else:
writer.write(newl)
for node in self.childNodes:
node.writexml(writer, indent+addindent, addindent, newl)
writer.write(indent)
writer.write("</{}>{}".format(self.tagName, newl))
else:
writer.write("/>{}".format(newl))


def monkeypatch_sipp_cdata_xml(self, writer, indent="", addindent="", newl=""):
"""Format CDATA.
Ensure that CDATA blocks are indented as expected, for visual
clarity.
"""
if self.data.find("]]>") >= 0:
raise ValueError("']]>' not allowed in a CDATA section")

writer.write("{}<![CDATA[{}\n".format(indent, newl))

for line in self.data.splitlines():
x = "{}{}{}".format(indent, addindent, line.strip()).rstrip()
writer.write("{}\n".format(x))

writer.write("{}{}]]>{}".format(newl, indent, newl))
124 changes: 124 additions & 0 deletions pysipp/cli/sippfmt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
from __future__ import print_function

import argparse
import types

from . import minidom


def copy_tree(doc, node):
"""Duplicate a minidom element."""
new_node = doc.createElement(node.tagName)
for k, v in node.attributes.items():
new_node.setAttribute(k, v)

for child in node.childNodes:
if child.nodeType == minidom.Node.COMMENT_NODE:
new_node.appendChild(child)
elif child.nodeType == minidom.Node.ELEMENT_NODE:
new_node.appendChild(copy_tree(doc, child))

return new_node


def monkeypatch_element(node):
"""Alter the representation of scenario elements.
Monkey patch the `writexml` so that when pretty print these
elements, we always put the response/request attributes first, and
then all remaining attributes in alphabetical order.
"""
node.writexml = types.MethodType(minidom.monkeypatch_element_xml, node)


def monkeypatch_cdata(node):
"""Alter the representation of CDATA elements.
Monkey patch the `writexml` so that when pretty print these
elements, the appropriate amount of whitespace is embedded into
the CDATA block for visual consistency.
"""
node.writexml = types.MethodType(minidom.monkeypatch_sipp_cdata_xml, node)


def process_element(doc, elem):
"""Process individual sections of a sipp scenario.
Copy and format the various steps inside the sipp script, such as
the recv and send elements. Make sure that CDATA chunks are
preserved and well formatted.
"""
new_node = doc.createElement(elem.tagName)
monkeypatch_element(new_node)

# copy attributes
for k, v in elem.attributes.items():
new_node.setAttribute(k, v)

for child in elem.childNodes:
if child.nodeType == minidom.Node.CDATA_SECTION_NODE:
data = doc.createCDATASection(child.data.strip())
monkeypatch_cdata(data)
new_node.appendChild(data)
elif child.nodeType == minidom.Node.COMMENT_NODE:
new_node.appendChild(child)
elif child.nodeType == minidom.Node.ELEMENT_NODE:
new_node.appendChild(copy_tree(doc, child))

return new_node


def process_document(filepath):
"""Process an XML document.
Process the document with minidom, process it for consistency, and
emit a new document. Minidom is used since we need to preserve the
structure of the XML document rather than its content.
"""
dom = minidom.parse(filepath)
scenario = next(elem for elem in dom.childNodes
if getattr(elem, 'tagName', None) == 'scenario')

imp = minidom.getDOMImplementation('')
dt = imp.createDocumentType('scenario', None, 'sipp.dtd')
doc = imp.createDocument(None, 'scenario', dt)

new_scen = doc.childNodes[-1]
new_scen.writexml = types.MethodType(minidom.monkeypatch_scenario_xml,
new_scen)

for elem in scenario.childNodes:
if elem.nodeType == minidom.Node.TEXT_NODE:
continue
elif elem.nodeType == minidom.Node.CDATA_SECTION_NODE:
continue
elif elem.nodeType == minidom.Node.ELEMENT_NODE:
new_node = process_element(doc, elem)
if new_node:
new_scen.appendChild(new_node)
new_scen.appendChild(doc.createSeparator())
else:
new_scen.appendChild(elem)

# delete the last separator
if (new_scen.childNodes
and isinstance(new_scen.childNodes[-1], minidom.Newline)):
del new_scen.childNodes[-1]

doc.appendChild(new_scen)
return doc


def main():
"""Format sipp scripts."""
parser = argparse.ArgumentParser(description='Format sipp scripts')
parser.add_argument('filename')
args = parser.parse_args()

doc = process_document(args.filename)
xml = doc.toprettyxml(indent=' ', encoding='ISO-8859-1')
print(xml, end='')


if __name__ == '__main__':
main()
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,14 @@
author_email='[email protected]',
url='https://github.com/SIPp/pysipp',
platforms=['linux'],
packages=['pysipp'],
packages=['pysipp', 'pysipp.cli'],
install_requires=['pluggy==0.3.1'],
tests_require=['pytest'],
entry_points={
'console_scripts': [
'sippfmt=pysipp.cli.sippfmt:main'
],
},
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
Expand Down

0 comments on commit c1345f3

Please sign in to comment.