changeset 243:e75816cb2f45

* Add an <x:transform/> task for applying XSLT transformations. Can use either libxslt or MSXML if available. Closes #35. * Fix some of the unit test failures on windows.
author cmlenz
date Mon, 03 Oct 2005 16:03:37 +0000
parents 372d1de2e3ec
children 1aa624af9ebb
files bitten/build/tests/__init__.py bitten/build/tests/config.py bitten/build/tests/xmltools.py bitten/build/xmltools.py bitten/util/tests/archive.py setup.py
diffstat 6 files changed, 240 insertions(+), 21 deletions(-) [+]
line wrap: on
line diff
--- 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__':
--- 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'])
new file mode 100644
--- /dev/null
+++ b/bitten/build/tests/xmltools.py
@@ -0,0 +1,125 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# 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("""<doc>
+<title>Document Title</title>
+<section>
+<title>Section Title</title>
+<para>This is a test.</para>
+<note>This is a note.</note>
+</section>
+</doc>
+""")
+        finally:
+            src_file.close()
+
+        style_file = file(self.ctxt.resolve('style.xsl'), 'w')
+        try:
+            style_file.write("""<xsl:stylesheet version="1.0"
+    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+    xmlns="http://www.w3.org/TR/xhtml1/strict">
+ <xsl:template match="doc">
+  <html>
+   <head>
+    <title><xsl:value-of select="title"/></title>
+   </head>
+   <body>
+    <xsl:apply-templates/>
+   </body>
+  </html>
+ </xsl:template>
+ <xsl:template match="doc/title">
+  <h1><xsl:apply-templates/></h1>
+ </xsl:template>
+ <xsl:template match="section/title">
+  <h2><xsl:apply-templates/></h2>
+ </xsl:template>
+ <xsl:template match="para">
+  <p><xsl:apply-templates/></p>
+ </xsl:template>
+ <xsl:template match="note">
+  <p class="note"><b>NOTE: </b><xsl:apply-templates/></p>
+ </xsl:template>
+</xsl:stylesheet>
+""")
+        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')
new file mode 100644
--- /dev/null
+++ b/bitten/build/xmltools.py
@@ -0,0 +1,86 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# 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?
--- 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')
--- 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
Copyright (C) 2012-2017 Edgewall Software