# HG changeset patch # User cmlenz # Date 1128355417 0 # Node ID e75816cb2f452f850f5583ddcee5001e6eebee23 # Parent 372d1de2e3ecdcea6c9cdaf379577259544be978 * Add an task for applying XSLT transformations. Can use either libxslt or MSXML if available. Closes #35. * Fix some of the unit test failures on windows. diff --git a/bitten/build/tests/__init__.py b/bitten/build/tests/__init__.py --- a/bitten/build/tests/__init__.py +++ b/bitten/build/tests/__init__.py @@ -9,13 +9,14 @@ import unittest -from bitten.build.tests import api, config, pythontools +from bitten.build.tests import api, config, pythontools, xmltools def suite(): suite = unittest.TestSuite() suite.addTest(api.suite()) suite.addTest(config.suite()) suite.addTest(pythontools.suite()) + suite.addTest(xmltools.suite()) return suite if __name__ == '__main__': diff --git a/bitten/build/tests/config.py b/bitten/build/tests/config.py --- a/bitten/build/tests/config.py +++ b/bitten/build/tests/config.py @@ -45,8 +45,8 @@ self.assertEqual('VERSION', config['version']) def test_sysinfo_configfile_override(self): - inifile = tempfile.NamedTemporaryFile(prefix='bitten_test') - inifile.write(""" + inifd, ininame = tempfile.mkstemp(prefix='bitten_test') + os.write(inifd, """ [machine] name = MACHINE processor = PROCESSOR @@ -56,8 +56,8 @@ family = FAMILY version = VERSION """) - inifile.seek(0) - config = Configuration(inifile.name) + os.close(inifd) + config = Configuration(ininame) self.assertEqual('MACHINE', config['machine']) self.assertEqual('PROCESSOR', config['processor']) @@ -75,14 +75,14 @@ self.assertEqual('2.3.5', config['python.version']) def test_package_configfile(self): - inifile = tempfile.NamedTemporaryFile(prefix='bitten_test') - inifile.write(""" + inifd, ininame = tempfile.mkstemp(prefix='bitten_test') + os.write(inifd, """ [python] path = /usr/local/bin/python2.3 version = 2.3.5 """) - inifile.seek(0) - config = Configuration(inifile.name) + os.close(inifd) + config = Configuration(ininame) self.assertEqual(True, 'python' in config.packages) self.assertEqual('/usr/local/bin/python2.3', config['python.path']) diff --git a/bitten/build/tests/xmltools.py b/bitten/build/tests/xmltools.py new file mode 100644 --- /dev/null +++ b/bitten/build/tests/xmltools.py @@ -0,0 +1,125 @@ +# -*- coding: iso8859-1 -*- +# +# Copyright (C) 2005 Christopher Lenz +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.cmlenz.net/wiki/License. + +import os +import shutil +import tempfile +import unittest + +from bitten.build import xmltools +from bitten.recipe import Context +from bitten.util import xmlio + + +class TransformTestCase(unittest.TestCase): + + def setUp(self): + self.basedir = os.path.realpath(tempfile.mkdtemp()) + self.ctxt = Context(self.basedir) + + def tearDown(self): + shutil.rmtree(self.basedir) + + def test_transform_no_src(self): + self.assertRaises(AssertionError, xmltools.transform, self.ctxt) + + def test_transform_no_dest(self): + self.assertRaises(AssertionError, xmltools.transform, self.ctxt, + src='src.xml') + + def test_transform_no_stylesheet(self): + self.assertRaises(AssertionError, xmltools.transform, self.ctxt, + src='src.xml', dest='dest.xml') + + def test_transform(self): + src_file = file(self.ctxt.resolve('src.xml'), 'w') + try: + src_file.write(""" +Document Title +
+Section Title +This is a test. +This is a note. +
+
+""") + finally: + src_file.close() + + style_file = file(self.ctxt.resolve('style.xsl'), 'w') + try: + style_file.write(""" + + + + <xsl:value-of select="title"/> + + + + + + + +

+
+ +

+
+ +

+
+ +

NOTE:

+
+
+""") + finally: + style_file.close() + + xmltools.transform(self.ctxt, src='src.xml', dest='dest.xml', + stylesheet='style.xsl') + + dest_file = file(self.ctxt.resolve('dest.xml')) + try: + dest = xmlio.parse(dest_file) + finally: + dest_file.close() + + self.assertEqual('html', dest.name) + self.assertEqual('http://www.w3.org/TR/xhtml1/strict', dest.namespace) + children = list(dest.children()) + self.assertEqual(2, len(children)) + self.assertEqual('head', children[0].name) + head_children = list(children[0].children()) + self.assertEqual(1, len(head_children)) + self.assertEqual('title', head_children[0].name) + self.assertEqual('Document Title', head_children[0].gettext()) + self.assertEqual('body', children[1].name) + body_children = list(children[1].children()) + self.assertEqual(4, len(body_children)) + self.assertEqual('h1', body_children[0].name) + self.assertEqual('Document Title', body_children[0].gettext()) + self.assertEqual('h2', body_children[1].name) + self.assertEqual('Section Title', body_children[1].gettext()) + self.assertEqual('p', body_children[2].name) + self.assertEqual('This is a test.', body_children[2].gettext()) + self.assertEqual('p', body_children[3].name) + self.assertEqual('note', body_children[3].attr['class']) + self.assertEqual('This is a note.', body_children[3].gettext()) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TransformTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/bitten/build/xmltools.py b/bitten/build/xmltools.py new file mode 100644 --- /dev/null +++ b/bitten/build/xmltools.py @@ -0,0 +1,86 @@ +# -*- coding: iso8859-1 -*- +# +# Copyright (C) 2005 Christopher Lenz +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://bitten.cmlenz.net/wiki/License. + +import logging +import os + +from bitten.build import CommandLine +from bitten.util import xmlio + +try: + import libxml2 + import libxslt + have_libxslt = True +except ImportError: + have_libxslt = False + +if not have_libxslt and os.name == 'nt': + try: + import win32com.client + have_msxml = True + except ImportError: + have_msxml = False +else: + have_msxml = False + +log = logging.getLogger('bitten.build.xmltools') + +def transform(ctxt, src=None, dest=None, stylesheet=None): + """Apply an XSLT stylesheet to a source XML document.""" + assert src, 'Missing required attribute "src"' + assert dest, 'Missing required attribute "dest"' + assert stylesheet, 'Missing required attribute "stylesheet"' + + if have_libxslt: + log.debug('Using libxslt for XSLT transformation') + srcdoc, styledoc, result = None, None, None + try: + srcdoc = libxml2.parseFile(ctxt.resolve(src)) + styledoc = libxslt.parseStylesheetFile(ctxt.resolve(stylesheet)) + result = styledoc.applyStylesheet(srcdoc, None) + styledoc.saveResultToFilename(ctxt.resolve(dest), result, 0) + finally: + if styledoc: + styledoc.freeStylesheet() + if srcdoc: + srcdoc.freeDoc() + if result: + result.freeDoc() + + elif have_msxml: + log.debug('Using MSXML for XSLT transformation') + srcdoc = win32com.client.Dispatch('MSXML2.DOMDocument.3.0') + if not srcdoc.load(ctxt.resolve(src)): + err = styledoc.parseError + ctxt.error('Failed to parse XML source %s: %s', src, err.reason) + return + styledoc = win32com.client.Dispatch('MSXML2.DOMDocument.3.0') + if not styledoc.load(ctxt.resolve(stylesheet)): + err = styledoc.parseError + ctxt.error('Failed to parse XSLT stylesheet %s: %s', stylesheet, + err.reason) + return + result = srcdoc.transformNode(styledoc) + + # MSXML seems to always write produce the resulting XML document using + # UTF-16 encoding, regardless of the encoding specified in the + # stylesheet. For better interoperability, recode to UTF-8 here. + result = result.encode('utf-8').replace(' encoding="UTF-16"?>', '?>') + + dest_file = file(ctxt.resolve(dest), 'w') + try: + dest_file.write(result) + finally: + dest_file.close() + + else: + ctxt.error('No usable XSLT implementation found') + + # TODO: as a last resort, try to invoke 'xsltproc' to do the + # transformation? diff --git a/bitten/util/tests/archive.py b/bitten/util/tests/archive.py --- a/bitten/util/tests/archive.py +++ b/bitten/util/tests/archive.py @@ -43,9 +43,12 @@ return filename def test_index_formats(self): - targz_path = self._create_file('snapshots/foo_r123.tar.gz') - tarbz2_path = self._create_file('snapshots/foo_r123.tar.bz2') - zip_path = self._create_file('snapshots/foo_r123.zip') + targz_path = self._create_file(os.path.join('snapshots', + 'foo_r123.tar.gz')) + tarbz2_path = self._create_file(os.path.join('snapshots', + 'foo_r123.tar.bz2')) + zip_path = self._create_file(os.path.join('snapshots', + 'foo_r123.zip')) index = list(archive.index(self.env, 'foo')) self.assertEqual(3, len(index)) assert ('123', 'gzip', targz_path) in index @@ -53,8 +56,10 @@ assert ('123', 'zip', zip_path) in index def test_index_revs(self): - rev123_path = self._create_file('snapshots/foo_r123.tar.gz') - rev124_path = self._create_file('snapshots/foo_r124.tar.gz') + rev123_path = self._create_file(os.path.join('snapshots', + 'foo_r123.tar.gz')) + rev124_path = self._create_file(os.path.join('snapshots', + 'foo_r124.tar.gz')) index = list(archive.index(self.env, 'foo')) self.assertEqual(2, len(index)) assert ('123', 'gzip', rev123_path) in index @@ -65,26 +70,27 @@ self.assertEqual(0, len(index)) def test_index_prefix(self): - path = self._create_file('snapshots/foo_r123.tar.gz') - self._create_file('snapshots/bar_r123.tar.gz') + path = self._create_file(os.path.join('snapshots', 'foo_r123.tar.gz')) + self._create_file(os.path.join('snapshots', 'bar_r123.tar.gz')) index = list(archive.index(self.env, 'foo')) self.assertEqual(1, len(index)) assert ('123', 'gzip', path) in index def test_index_no_rev(self): - path = self._create_file('snapshots/foo_r123.tar.gz') - self._create_file('snapshots/foo_map.tar.gz') + path = self._create_file(os.path.join('snapshots', 'foo_r123.tar.gz')) + self._create_file(os.path.join('snapshots', 'foo_map.tar.gz')) index = list(archive.index(self.env, 'foo')) self.assertEqual(1, len(index)) assert ('123', 'gzip', path) in index def test_index_missing_md5sum(self): - self._create_file('snapshots/foo_r123.tar.gz', create_md5sum=False) + self._create_file(os.path.join('snapshots', 'foo_r123.tar.gz'), + create_md5sum=False) index = list(archive.index(self.env, 'foo')) self.assertEqual(0, len(index)) def test_index_nonmatching_md5sum(self): - path = self._create_file('snapshots/foo_r123.tar.gz', + path = self._create_file(os.path.join('snapshots', 'foo_r123.tar.gz'), create_md5sum=False) md5sum = md5.new('Foo bar') md5sum_file = file(path + '.md5', 'w') diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -51,7 +51,8 @@ NS + 'python#exec = bitten.build.pythontools:exec_', NS + 'python#pylint = bitten.build.pythontools:pylint', NS + 'python#trace = bitten.build.pythontools:trace', - NS + 'python#unittest = bitten.build.pythontools:unittest' + NS + 'python#unittest = bitten.build.pythontools:unittest', + NS + 'x#transform = bitten.build.xmltools:transform' ] }, test_suite='bitten.tests.suite', zip_safe=True