changeset 4:196009657e5e

Simplify the recipe commands interface: * The implementation of a command is now located using a pseudo-protocol for namespace URIs. * Commands are simply module-level functions instead of components. * Remove dependency of the recipe/slave code on the Trac component architecture.
author cmlenz
date Mon, 06 Jun 2005 15:54:29 +0000
parents 9ac0ee86ec7c
children 738a0ae251f6
files Makefile bitten/general/__init__.py bitten/general/cmd_make.py bitten/python/__init__.py bitten/python/cmd_distutils.py bitten/python/rep_pylint.py bitten/python/rep_trace.py bitten/python/rep_unittest.py bitten/recipe.py bitten/recipe/__init__.py bitten/recipe/api.py bitten/recipe/ctools.py bitten/recipe/pythontools.py bitten/recipe/tests/__init__.py bitten/recipe/tests/api.py bitten/tests/__init__.py bitten/tests/recipe.py recipe.xml scripts/build.py setup.py
diffstat 19 files changed, 187 insertions(+), 208 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,7 @@
 PYLINT_MSGS = C0101,E0201,E0213,W0103,W0704,R0921,R0923
+PYTHONPATH = .
 
 all: pylint
 
 pylint:
-	PYTHONPATH=.:../Trac/trunk pylint --parseable=y --disable-msg=$(PYLINT_MSGS) --ignore=tests bitten > build/pylint-results.txt
+	PYTHONPATH=$(PYTHONPATH) pylint --parseable=y --disable-msg=$(PYLINT_MSGS) --ignore=tests bitten > build/pylint-results.txt
deleted file mode 100644
deleted file mode 100644
--- a/bitten/general/cmd_make.py
+++ /dev/null
@@ -1,18 +0,0 @@
-import os
-
-from trac.core import *
-from trac.util import NaivePopen
-from bitten import BuildError
-from bitten.recipe import ICommandExecutor
-
-
-class MakeExecutor(Component):
-    implements(ICommandExecutor)
-
-    def get_name(self):
-        return 'make'
-
-    def execute(self, basedir, target='all'):
-        cmd = NaivePopen('make %s' % target, capturestderr=True)
-        for line in cmd.out.splitlines():
-            print '[make] %s' % line
deleted file mode 100644
deleted file mode 100644
--- a/bitten/python/cmd_distutils.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import os
-
-from trac.core import *
-from trac.util import NaivePopen
-from bitten import BuildError
-from bitten.recipe import ICommandExecutor
-
-
-class DistutilsExecutor(Component):
-    implements(ICommandExecutor)
-
-    def get_name(self):
-        return 'distutils'
-
-    def execute(self, basedir, command='build'):
-        try:
-            cmd = NaivePopen('python setup.py %s' % command)
-            for line in cmd.out.splitlines():
-                print '[distutils] %s' % line
-        except OSError, e:
-            raise BuildError, 'Executing distutils failed: %s' % e
deleted file mode 100644
--- a/bitten/python/rep_pylint.py
+++ /dev/null
@@ -1,29 +0,0 @@
-import os
-import re
-
-from elementtree import ElementTree
-from trac.core import *
-from bitten import BuildError
-from bitten.recipe import IReportProcessor
-
-
-_msg_re = re.compile(r'^(?P<file>.+):(?P<line>\d+): '
-                     r'\[(?P<type>[A-Z])(?:, (?P<tag>[\w\.]+))?\] '
-                     r'(?P<msg>.*)$')
-
-class PylintReportProcessor(Component):
-    implements(IReportProcessor)
-
-    def get_name(self):
-        return 'pylint'
-
-    def process(self, basedir, file=None):
-        assert file, 'Missing required attribute "file"'
-
-        for line in open(file, 'r'):
-            match = _msg_re.search(line)
-            if match:
-                filename = match.group('file')
-                if filename.startswith(basedir):
-                    filename = filename[len(basedir) + 1:]
-                lineno = int(match.group('line'))
deleted file mode 100644
--- a/bitten/python/rep_trace.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import os
-import re
-
-from elementtree import ElementTree
-from trac.core import *
-from bitten import BuildError
-from bitten.recipe import IReportProcessor
-
-
-class TraceReportProcessor(Component):
-    implements(IReportProcessor)
-
-    def get_name(self):
-        return 'trace'
-
-    def process(self, basedir, summary=None, coverdir=None, include=None,
-                exclude=None):
-        assert summary, 'Missing required attribute "summary"'
-        assert coverdir, 'Missing required attribute "coverdir"'
deleted file mode 100644
--- a/bitten/python/rep_unittest.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import os
-import re
-
-from elementtree import ElementTree
-from trac.core import *
-from bitten import BuildError
-from bitten.recipe import IReportProcessor
-
-
-_test_re = re.compile(r'^(?P<testname>\w+) \((?P<testcase>\d+): '
-                      r'\[(?P<type>[A-Z])(?:, (?P<tag>[\w\.]+))?\] '
-                      r'(?P<msg>.*)$')
-
-class UnittestReportProcessor(Component):
-    implements(IReportProcessor)
-
-    def get_name(self):
-        return 'unittest'
-
-    def process(self, basedir, file=None):
-        assert file, 'Missing required attribute "file"'
new file mode 100644
--- /dev/null
+++ b/bitten/recipe/__init__.py
@@ -0,0 +1,1 @@
+from bitten.recipe.api import *
\ No newline at end of file
rename from bitten/recipe.py
rename to bitten/recipe/api.py
--- a/bitten/recipe.py
+++ b/bitten/recipe/api.py
@@ -1,71 +1,63 @@
-import os
 import os.path
-from elementtree import ElementTree
+from xml.dom import minidom
 
-from trac.core import *
+from bitten import BuildError
+
+__all__ = ['Recipe']
 
 
-class ICommandExecutor(Interface):
-
-    def get_name():
-        """
-        Return the name of the command as used in the XML file.
-        """
-
-    def execute(basedir, **attrs):
-        """
-        """
+class Step(object):
+    """Represents a single step of a build recipe.
 
-
-class IReportProcessor(Interface):
+    Iterate over an object of this class to get the commands to execute, and
+    their keyword arguments.
+    """
 
-    def get_name():
-        """
-        Return the name of the command as used in the XML file.
-        """
+    def __init__(self, node):
+        self._node = node
+        self.id = node.getAttribute('id')
+        self.description = node.getAttribute('description')
 
-    def process(basedir, **attrs):
-        """
-        """
+    def __iter__(self):
+        for child in [c for c in self._node.childNodes if c.nodeType == 1]:
+            if child.namespaceURI:
+                # Commands
+                yield self._translate(child)
+            elif child.tagName == 'reports':
+                # Reports
+                for child in [c for c in child.childNodes if c.nodeType == 1]:
+                    yield self._translate(child)
+            else:
+                raise BuildError, "Unknown element <%s>" % child.tagName
+
+    def _translate(self, node):
+        if not node.namespaceURI.startswith('bitten:'):
+            # Ignore elements in a foreign namespace
+            return None
+
+        module = __import__(node.namespaceURI[7:], globals(), locals(),
+                            node.localName)
+        func = getattr(module, node.localName)
+        attrs = {}
+        for name, value in node.attributes.items():
+            attrs[name.encode()] = value.encode()
+        return func, attrs
 
 
 class Recipe(object):
+    """Represents a build recipe.
+    
+    Iterate over this object to get the individual build steps in the order they
+    have been defined in the recipe file."""
 
     def __init__(self, filename='recipe.xml', basedir=os.getcwd()):
         self.filename = filename
         self.basedir = basedir
         self.path = os.path.join(basedir, filename)
-        self.tree = ElementTree.parse(self.path).getroot()
-
-    description = property(fget=lambda self: self.tree.attrib['description'])
-
-
-class RecipeExecutor(Component):
-
-    command_executors = ExtensionPoint(ICommandExecutor)
-    report_processors = ExtensionPoint(IReportProcessor)
+        self.root = minidom.parse(self.path).documentElement
+        self.description = self.root.getAttribute('description')
 
-    def execute(self, recipe):
-        for step in recipe.tree:
-            print '---> %s' % step.attrib['title']
-            for element in step:
-                if element.tag == 'reports':
-                    for report in element:
-                        reporter = self._get_report_processor(report.tag)
-                        reporter.process(recipe.basedir, **report.attrib)
-                else:
-                    cmd = self._get_command_executor(element.tag)
-                    cmd.execute(recipe.basedir, **element.attrib)
-            print
-
-    def _get_command_executor(self, name):
-        for command_executor in self.command_executors:
-            if command_executor.get_name() == name:
-                return command_executor
-        raise Exception, "Unknown command <%s>" % name
-
-    def _get_report_processor(self, name):
-        for report_processor in self.report_processors:
-            if report_processor.get_name() == name:
-                return report_processor
-        raise Exception, "Unknown report type <%s>" % name
+    def __iter__(self):
+        """Provide an iterator over the individual steps of the recipe."""
+        for child in self.root.getElementsByTagName('step'):
+            yield Step(child)
new file mode 100644
--- /dev/null
+++ b/bitten/recipe/ctools.py
@@ -0,0 +1,18 @@
+from popen2 import Popen3
+
+def make(basedir, target='all'):
+    """Execute a Makefile target."""
+    cmdline = 'make %s' % target
+    pipe = Popen3(cmdline, capturestderr=True) # FIXME: Windows compatibility
+    while True:
+        retval = pipe.poll()
+        if retval != -1:
+            break
+        line = pipe.fromchild.readline()
+        if line:
+            print '[make] %s' % line.rstrip()
+        line = pipe.childerr.readline()
+        if line:
+            print '[make] %s' % line.rstrip()
+    if retval != 0:
+        raise BuildError, "Executing distutils failed (%s)" % retval
new file mode 100644
--- /dev/null
+++ b/bitten/recipe/pythontools.py
@@ -0,0 +1,44 @@
+import re
+from popen2 import Popen3
+
+from bitten import BuildError
+
+def distutils(basedir, command='build'):
+    """Execute a `distutils` command."""
+    cmdline = 'python setup.py %s' % command
+    pipe = Popen3(cmdline, capturestderr=True) # FIXME: Windows compatibility
+    while True:
+        retval = pipe.poll()
+        if retval != -1:
+            break
+        line = pipe.fromchild.readline()
+        if line:
+            print '[distutils] %s' % line.rstrip()
+        line = pipe.childerr.readline()
+        if line:
+            print '[distutils] %s' % line.rstrip()
+    if retval != 0:
+        raise BuildError, "Executing distutils failed (%s)" % retval
+
+def pylint(basedir, file=None):
+    """Extract data from a `pylint` run written to a file."""
+    assert file, 'Missing required attribute "file"'
+    _msg_re = re.compile(r'^(?P<file>.+):(?P<line>\d+): '
+                         r'\[(?P<type>[A-Z])(?:, (?P<tag>[\w\.]+))?\] '
+                         r'(?P<msg>.*)$')
+    for line in open(file, 'r'):
+        match = _msg_re.search(line)
+        if match:
+            filename = match.group('file')
+            if filename.startswith(basedir):
+                filename = filename[len(basedir) + 1:]
+            lineno = int(match.group('line'))
+
+def trace(basedir, summary=None, coverdir=None, include=None, exclude=None):
+    """Extract data from a `trac.py` run."""
+    assert summary, 'Missing required attribute "summary"'
+    assert coverdir, 'Missing required attribute "coverdir"'
+
+def unittest(basedir, file=None):
+    """Extract data from a unittest results file in XML format."""
+    assert file, 'Missing required attribute "file"'
new file mode 100644
--- /dev/null
+++ b/bitten/recipe/tests/__init__.py
@@ -0,0 +1,8 @@
+import unittest
+
+from bitten.recipe.tests import api
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(api.suite())
+    return suite
new file mode 100644
--- /dev/null
+++ b/bitten/recipe/tests/api.py
@@ -0,0 +1,27 @@
+import os
+import os.path
+import tempfile
+import unittest
+
+from bitten.recipe import Recipe
+
+
+class RecipeTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.temp_dir = tempfile.gettempdir()
+        self.recipe_xml = open(os.path.join(self.temp_dir, 'recipe.xml'), 'w')
+
+    def tearDown(self):
+        os.unlink(os.path.join(self.temp_dir, 'recipe.xml'))
+
+    def testDescription(self):
+        self.recipe_xml.write('<?xml version="1.0"?>'
+                              '<recipe description="test">'
+                              '</recipe>')
+        self.recipe_xml.close()
+        recipe = Recipe(basedir=self.temp_dir)
+        self.assertEqual('test', recipe.description)
+
+def suite():
+    return unittest.makeSuite(RecipeTestCase, 'test')
--- a/bitten/tests/__init__.py
+++ b/bitten/tests/__init__.py
@@ -1,6 +1,6 @@
 import unittest
 
-from bitten.tests import recipe
+from bitten.recipe import tests as recipe
 
 def suite():
     suite = unittest.TestSuite()
deleted file mode 100644
--- a/bitten/tests/recipe.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import os
-import os.path
-import tempfile
-import unittest
-
-from bitten.recipe import Recipe
-
-
-class RecipeTestCase(unittest.TestCase):
-
-    def setUp(self):
-        self.temp_dir = tempfile.gettempdir()
-        self.recipe_xml = open(os.path.join(self.temp_dir, 'recipe.xml'), 'w')
-
-    def tearDown(self):
-        os.unlink(os.path.join(self.temp_dir, 'recipe.xml'))
-
-    def testDescription(self):
-        self.recipe_xml.write('<?xml version="1.0"?>'
-                              '<recipe description="test">'
-                              '</recipe>')
-        self.recipe_xml.close()
-        recipe = Recipe(basedir=self.temp_dir)
-        self.assertEqual('test', recipe.description)
-
-def suite():
-    return unittest.makeSuite(RecipeTestCase, 'test')
--- a/recipe.xml
+++ b/recipe.xml
@@ -1,30 +1,32 @@
 <?xml version="1.0"?>
-<recipe description="My project">
+<recipe description="My project"
+    xmlns:c="bitten:bitten.recipe.ctools"
+    xmlns:python="bitten:bitten.recipe.pythontools">
 
     <step id="build" title="Let Distutils build the python code">
-        <distutils command="build"/>
+        <python:distutils command="build"/>
     </step>
 
     <step id="test" title="Unit tests"
           description="Run unit tests and trace code coverage">
-        <distutils command="unittest"/>
+        <python:distutils command="unittest"/>
         <reports>
-            <unittest file="build/test-results.xml"/>
-            <trace summary="build/test-coverage.txt" coverdir="build/coverage"
-                   include="trac*" exclude="*.tests.*"/>
+            <python:unittest file="build/test-results.xml"/>
+            <python:trace summary="build/test-coverage.txt" 
+                coverdir="build/coverage" include="trac*" exclude="*.tests.*"/>
         </reports>
     </step>
 
     <step id="lint" title="Run Pylint"
           description="Run Pylint to check for bad style and potential errors">
-        <make target="pylint"/>
+        <c:make target="pylint"/>
         <reports>
-            <pylint file="build/pylint-results.txt"/>
+            <python:pylint file="build/pylint-results.txt"/>
         </reports>
     </step>
 
     <step id="dist" title="Package up distributions">
-        <distutils command="sdist"/>
+        <python:distutils command="sdist"/>
     </step>
 
 </recipe>
--- a/scripts/build.py
+++ b/scripts/build.py
@@ -1,12 +1,31 @@
 #!/usr/bin/env python
 
-from trac.core import ComponentManager
+import sys
 
-from bitten.recipe import Recipe, RecipeExecutor
-from bitten.python import cmd_distutils, rep_pylint, rep_unittest, rep_trace
-from bitten.general import cmd_make
+from bitten import BuildError
+from bitten.recipe import Recipe
+
+def build():
+    step_id = None
+    if len(sys.argv) > 1:
+        step_id = sys.argv[1]
+
+    recipe = Recipe()
+    steps_run = []
+    for step in recipe:
+        if not step_id or step.id == step_id:
+            print '-->', step.description or step.id
+            for function, kw in step:
+                function(recipe.basedir, **kw)
+            print
+            steps_run.append(step.id)
+
+    if step_id and not step_id in steps_run:
+        raise BuildError, "Recipe has no step named '%s'" % step_id
 
 if __name__ == '__main__':
-    mgr = ComponentManager()
-    recipe = Recipe()
-    RecipeExecutor(mgr).execute(recipe)
+    try:
+        build()
+    except BuildError, e:
+        print>>sys.stderr, "FAILED: %s" % e
+        sys.exit(-1)
old mode 100644
new mode 100755
--- a/setup.py
+++ b/setup.py
@@ -1,3 +1,5 @@
+#!/usr/bin/env python
+
 from distutils.core import setup, Command
 from bitten.distutils.testrunner import unittest
 
Copyright (C) 2012-2017 Edgewall Software