changeset 29:2adc3480e4aa

Some cleanup and additional docstrings.
author cmlenz
date Sun, 19 Jun 2005 22:42:53 +0000
parents 1e562dd56ec0
children 75ad81953032
files bitten/master.py bitten/slave.py bitten/util/beep.py bitten/util/tests/beep.py bitten/util/xmlio.py
diffstat 5 files changed, 234 insertions(+), 179 deletions(-) [+]
line wrap: on
line diff
--- a/bitten/master.py
+++ b/bitten/master.py
@@ -22,8 +22,7 @@
 
 from trac.env import Environment
 from bitten import __version__ as VERSION
-from bitten.util import beep
-from bitten.util.xmlio import Element, parse_xml
+from bitten.util import beep, xmlio
 
 
 class Master(beep.Listener):
@@ -57,7 +56,7 @@
     """
     URI = 'http://bitten.cmlenz.net/beep/orchestration'
 
-    def handle_connect(self):
+    def handle_connect(self, init_elem=None):
         self.master = self.session.listener
         assert self.master
         self.slave_name = None
@@ -68,7 +67,7 @@
 
     def handle_msg(self, msgno, msg):
         assert msg.get_content_type() == beep.BEEP_XML
-        elem = parse_xml(msg.get_payload())
+        elem = xmlio.parse(msg.get_payload())
 
         if elem.tagname == 'register':
             platform, os, os_family, os_version = None, None, None, None
@@ -83,8 +82,8 @@
             self.slave_name = elem.name
             self.master.slaves[self.slave_name] = self
 
-            rpy = beep.MIMEMessage(Element('ok'), beep.BEEP_XML)
-            self.channel.send_rpy(msgno, rpy)
+            xml = xmlio.Element('ok')
+            self.channel.send_rpy(msgno, beep.MIMEMessage(xml))
             logging.info('Registered slave "%s" (%s running %s %s [%s])',
                          self.slave_name, platform, os, os_version, os_family)
 
--- a/bitten/slave.py
+++ b/bitten/slave.py
@@ -24,25 +24,16 @@
 import time
 
 from bitten import __version__ as VERSION
-from bitten.util import beep
-from bitten.util.xmlio import Element, parse_xml
+from bitten.util import beep, xmlio
 
 
 class Slave(beep.Initiator):
 
-    channelno = None # The channel number used by the bitten profile
-    terminated = False
-
-    def channel_started(self, channelno, profile_uri):
-        if profile_uri == OrchestrationProfileHandler.URI:
-            self.channelno = channelno
-
     def greeting_received(self, profiles):
         if OrchestrationProfileHandler.URI not in profiles:
             logging.error('Peer does not support Bitten profile')
             raise beep.TerminateSession, 'Peer does not support Bitten profile'
-        self.channels[0].profile.send_start([OrchestrationProfileHandler],
-                                            handle_ok=self.channel_started)
+        self.channels[0].profile.send_start([OrchestrationProfileHandler])
 
 
 class OrchestrationProfileHandler(beep.ProfileHandler):
@@ -51,25 +42,25 @@
     """
     URI = 'http://bitten.cmlenz.net/beep/orchestration'
 
-    def handle_connect(self):
+    def handle_connect(self, init_elem=None):
         """Register with the build master."""
-        sysname, nodename, release, version, machine = os.uname()
-        logging.info('Registering with build master as %s', nodename)
-        register = Element('register', name=nodename)[
-            Element('platform')[machine],
-            Element('os', family=os.name, version=release)[sysname]
-        ]
         def handle_reply(cmd, msgno, msg):
             if cmd == 'ERR':
                 if msg.get_content_type() == beep.BEEP_XML:
-                    elem = parse_xml(msg.get_payload())
+                    elem = xmlio.parse(msg.get_payload())
                     if elem.tagname == 'error':
                         raise beep.TerminateSession, \
-                              '%s (%s)' % (elem.gettext(), elem.code)
+                              '%s (%d)' % (elem.gettext(), int(elem.code))
                 raise beep.TerminateSession, 'Registration failed!'
             logging.info('Registration successful')
-        self.channel.send_msg(beep.MIMEMessage(register, beep.BEEP_XML),
-                              handle_reply)
+
+        sysname, nodename, release, version, machine = os.uname()
+        logging.info('Registering with build master as %s', nodename)
+        xml = xmlio.Element('register', name=nodename)[
+            xmlio.Element('platform')[machine],
+            xmlio.Element('os', family=os.name, version=release)[sysname]
+        ]
+        self.channel.send_msg(beep.MIMEMessage(xml), handle_reply)
 
     def handle_msg(self, msgno, msg):
         # TODO: Handle build initiation requests etc
--- a/bitten/util/beep.py
+++ b/bitten/util/beep.py
@@ -29,14 +29,14 @@
 import asynchat
 import asyncore
 import bisect
+import email
 from email.Message import Message
-from email.Parser import Parser
 import logging
 import socket
 import sys
 import time
 
-from bitten.util.xmlio import Element, parse_xml
+from bitten.util import xmlio
 
 __all__ = ['Listener', 'Initiator', 'Profile']
 
@@ -69,24 +69,25 @@
         self.listen(5)
 
     def writable(self):
+        """Called by asyncore to determine whether the channel is writable."""
         return False
 
     def handle_read(self):
+        """Called by asyncore to signal data available for reading."""
         pass
 
     def readable(self):
+        """Called by asyncore to determine whether the channel is readable."""
         return True
 
-    def handle_connect(self):
-        pass
-
     def handle_accept(self):
-        """Start a new BEEP session."""
+        """Start a new BEEP session initiated by a peer."""
         conn, (ip, port) = self.accept()
         logging.debug('Connected to %s:%d', ip, port)
         Session(self, conn, (ip, port), self.profiles, first_channelno=2)
 
     def run(self, timeout=15.0, granularity=5):
+        """Start listening to incoming connections."""
         socket_map = asyncore.socket_map
         last_event_check = 0
         while socket_map:
@@ -94,25 +95,29 @@
             if (now - last_event_check) >= granularity:
                 last_event_check = now
                 fired = []
-                # yuck. i want my lisp.
                 i = j = 0
                 while i < len(self.eventqueue):
-                    when, what = self.eventqueue[i]
+                    when, callback = self.eventqueue[i]
                     if now >= when:
-                        fired.append(what)
+                        fired.append(callback)
                         j = i + 1
                     else:
                         break
                     i = i + 1
                 if fired:
                     self.eventqueue = self.eventqueue[j:]
-                    for what in fired:
-                        what (self, now)
+                    for callback in fired:
+                        callback(self, now)
             asyncore.poll(timeout)
 
-    def schedule (self, delta, callback):
-        now = int(time.time())
-        bisect.insort(self.eventqueue, (now + delta, callback))
+    def schedule(self, delta, callback):
+        """Schedule a function to be called.
+        
+        @param delta: The number of seconds after which the callback should be
+                      invoked
+        @param callback: The function to call
+        """
+        bisect.insort(self.eventqueue, (int(time.time()) + delta, callback))
 
 
 class Session(asynchat.async_chat):
@@ -145,14 +150,14 @@
         self.channels = {0: Channel(self, 0, ManagementProfileHandler)}
 
     def handle_connect(self):
-        pass
+        """Called by asyncore when the connection is established."""
 
     def handle_error(self):
         """Called by asyncore when an exception is raised."""
-        t, v = sys.exc_info()[:2]
-        if t is TerminateSession:
-            raise t, v
-        logging.exception(v)
+        cls, value = sys.exc_info()[:2]
+        if cls is TerminateSession:
+            raise cls, value
+        logging.exception(value)
         self.close()
 
     def collect_incoming_data(self, data):
@@ -225,7 +230,6 @@
                 msgno = int(header[2])
                 more = header[3] == '*'
                 seqno = int(header[4])
-                size = int(header[5])
                 ansno = None
                 if cmd == 'ANS':
                     ansno = int(header[6])
@@ -238,19 +242,34 @@
 
     def send_data_frame(self, cmd, channel, msgno, more, seqno, ansno=None,
                   payload=''):
-        """Send the specified data frame to the peer."""
+        """Send the specified data frame to the peer.
+        
+        @param cmd: The frame keyword (one of MSG, RPY, ERR, ANS, or NUL)
+        @param channel: The number of the sending channel
+        @param msgno: The message number
+        @param more: Whether this is the last frame belonging to the message
+        @param seqno: The frame-specific sequence number
+        @param ansno: For ANS frames, the answer number, otherwise `None`
+        @param payload: The payload of the frame
+        """
         headerbits = [cmd, channel, msgno, more and '*' or '.', seqno,
                       len(payload)]
         if cmd == 'ANS':
             assert ansno is not None
             headerbits.append(ansno)
-        header = ' '.join([str(hb) for hb in headerbits])
+        header = ' '.join([str(bit) for bit in headerbits])
         logging.debug('Sending frame [%s]', header)
         self.push('\r\n'.join((header, payload, 'END', '')))
 
     def send_seq_frame(self, channel, ackno, window):
+        """Send a SEQ frame to the peer.
+        
+        @param channel: The number of the sending channel
+        @param ackno: The acknowledgement number.
+        @param window: The requested window size
+        """
         headerbits = ['SEQ', channel, ackno, window]
-        header = ' '.join([str(hb) for hb in headerbits])
+        header = ' '.join([str(bit) for bit in headerbits])
         logging.debug('Sending frame [%s]', header)
         self.push('\r\n'.join((header, 'END', '')))
 
@@ -278,6 +297,7 @@
             raise TerminateSession, 'Connection to %s:%d failed' % ip, port
 
     def handle_close(self):
+        """Called by asyncore when the socket has been closed."""
         self.terminated = True
 
     def greeting_received(self, profiles):
@@ -286,6 +306,7 @@
         @param profiles: A list of URIs of the profiles the peer claims to
                          support.
         """
+        pass
 
     def run(self):
         """Start this peer, which will try to connect to the server and send a
@@ -329,19 +350,20 @@
         self.channelno = channelno
         self.windowsize = 4096
         self.inqueue = {}
-        self.outqueue = []
         self.reply_handlers = {}
 
         self.msgno = cycle_through(0, 2147483647)
         self.msgnos = {} # message numbers currently in use
         self.ansnos = {} # answer numbers keyed by msgno, each 0-2147483647
-        self.seqno = [serial(), serial()] # incoming, outgoing sequence numbers
-        self.mime_parser = Parser()
+
+        # incoming, outgoing sequence numbers
+        self.seqno = [SerialNumber(), SerialNumber()]
 
         self.profile = profile_cls(self)
         self.profile.handle_connect()
 
     def close(self):
+        """Close the channel."""
         self.profile.handle_disconnect()
         del self.session.channels[self.channelno]
 
@@ -385,14 +407,16 @@
             del self.msgnos[msgno]
         message = None
         if payload:
-            message = self.mime_parser.parsestr(payload)
+            message = email.message_from_string(payload)
 
         if cmd == 'MSG':
             self.profile.handle_msg(msgno, message)
         else:
             if msgno in self.reply_handlers:
-                self.reply_handlers[msgno](cmd, msgno, message)
-                del self.reply_handlers[msgno]
+                try:
+                    self.reply_handlers[msgno](cmd, msgno, message)
+                finally:
+                    del self.reply_handlers[msgno]
             elif cmd == 'RPY':
                 self.profile.handle_rpy(msgno, message)
             elif cmd == 'ERR':
@@ -403,6 +427,7 @@
                 self.profile.handle_nul(msgno)
 
     def _send(self, cmd, msgno, ansno=None, message=None):
+        """Send a frame to the peer."""
         payload = ''
         if message is not None:
             payload = message.as_string()
@@ -414,7 +439,7 @@
             self.session.send_data_frame(cmd, self.channelno, msgno, True,
                                          self.seqno[1].value, payload=window,
                                          ansno=ansno)
-            self.seqno[1] += len(window)
+            self.seqno[1] += self.windowsize
             payload = payload[self.windowsize:]
 
         # Send the final frame
@@ -424,6 +449,7 @@
         self.seqno[1] += len(payload)
 
     def send_msg(self, message, handle_reply=None):
+        """Send a MSG frame to the peer."""
         while True: # Find a unique message number
             msgno = self.msgno.next()
             if msgno not in self.msgnos:
@@ -435,22 +461,37 @@
         return msgno
 
     def send_rpy(self, msgno, message):
+        """Send a RPY frame to the peer.
+        
+        @param msgno: The number of the message this reply is in reference to
+        @param message: The message payload (a `MIMEMessage` instance)
+        """
         self._send('RPY', msgno, None, message)
 
     def send_err(self, msgno, message):
+        """Send an ERR frame to the peer.
+        
+        @param msgno: The number of the message this reply is in reference to
+        @param message: The message payload (a `MIMEMessage` instance)
+        """
         self._send('ERR', msgno, None, message)
 
     def send_ans(self, msgno, message):
-        if not msgno in self.ansnos:
-            ansno = cycle_through(0, 2147483647)
-            self.ansnos[msgno] = ansno
-        else:
-            ansno = self.ansnos[msgno]
-        next_ansno = ansno.next()
+        """Send an ANS frame to the peer.
+        
+        @param msgno: The number of the message this reply is in reference to
+        @param message: The message payload (a `MIMEMessage` instance)
+        """
+        ansnos = self.ansnos.setdefault(msgno, cycle_through(0, 2147483647))
+        next_ansno = ansnos.next()
         self._send('ANS', msgno, next_ansno, message)
         return next_ansno
 
     def send_nul(self, msgno):
+        """Send a NUL frame to the peer.
+        
+        @param msgno: The number of the message this reply is in reference to
+        """
         self._send('NUL', msgno)
         del self.ansnos[msgno] # dealloc answer numbers for the message
 
@@ -475,18 +516,23 @@
         """Called when the channel this profile is associated with is closed."""
 
     def handle_msg(self, msgno, message):
+        """Handle a MSG frame."""
         raise NotImplementedError
 
     def handle_rpy(self, msgno, message):
+        """Handle a RPY frame."""
         pass
 
     def handle_err(self, msgno, message):
+        """Handle an ERR frame."""
         pass
 
     def handle_ans(self, msgno, ansno, message):
+        """Handle an ANS frame."""
         pass
 
     def handle_nul(self, msgno):
+        """Handle a NUL frame."""
         pass
 
 
@@ -496,15 +542,16 @@
     def handle_connect(self):
         """Send a greeting reply directly after connecting to the peer."""
         profile_uris = self.session.profiles.keys()
-        logging.debug('Send greeting with profiles %s', profile_uris)
-        greeting = Element('greeting')[
-            [Element('profile', uri=k) for k in profile_uris]
+        logging.debug('Send greeting with profiles: %s', profile_uris)
+        xml = xmlio.Element('greeting')[
+            [xmlio.Element('profile', uri=uri) for uri in profile_uris]
         ]
-        self.channel.send_rpy(0, MIMEMessage(greeting, BEEP_XML))
+        self.channel.send_rpy(0, MIMEMessage(xml))
 
     def handle_msg(self, msgno, message):
+        """Handle an incoming message."""
         assert message.get_content_type() == BEEP_XML
-        elem = parse_xml(message.get_payload())
+        elem = xmlio.parse(message.get_payload())
 
         if elem.tagname == 'start':
             channelno = int(elem.number)
@@ -518,9 +565,8 @@
                     channel = Channel(self.session, channelno,
                                       self.session.profiles[profile.uri])
                     self.session.channels[channelno] = channel
-                    message = MIMEMessage(Element('profile', uri=profile.uri),
-                                          BEEP_XML)
-                    self.channel.send_rpy(msgno, message)
+                    xml = xmlio.Element('profile', uri=profile.uri)
+                    self.channel.send_rpy(msgno, MIMEMessage(xml))
                     return
             self.send_error(msgno, 550,
                             'None of the requested profiles is supported')
@@ -538,14 +584,14 @@
                 self.send_error(msgno, 550, 'Channel waiting for replies')
                 return
             self.session.channels[channelno].close()
-            message = MIMEMessage(Element('ok'), BEEP_XML)
-            self.channel.send_rpy(msgno, message)
+            self.channel.send_rpy(msgno, MIMEMessage(xmlio.Element('ok')))
             if not self.session.channels:
                 self.session.close()
 
     def handle_rpy(self, msgno, message):
+        """Handle a positive reply."""
         assert message.get_content_type() == BEEP_XML
-        elem = parse_xml(message.get_payload())
+        elem = xmlio.parse(message.get_payload())
 
         if elem.tagname == 'greeting':
             if isinstance(self.session, Initiator):
@@ -556,17 +602,19 @@
             self.send_error(msgno, 501, 'What are you replying to, son?')
 
     def handle_err(self, msgno, message):
+        """Handle a negative reply."""
         # Probably an error on connect, because other errors should get handled
         # by the corresponding callbacks
         # TODO: Terminate the session, I guess
         assert message.get_content_type() == BEEP_XML
-        elem = parse_xml(message.get_payload())
+        elem = xmlio.parse(message.get_payload())
         assert elem.tagname == 'error'
-        logging.warning('Received error in response to message #%d: %s (%s)',
-                        msgno, elem.gettext(), elem.code)
+        logging.warning('Received error in response to message #%d: %s (%d)',
+                        msgno, elem.gettext(), int(elem.code))
 
     def send_close(self, channelno=0, code=200, handle_ok=None,
                    handle_error=None):
+        """Send a request to close a channel to the peer."""
         def handle_reply(cmd, msgno, message):
             if cmd == 'RPY':
                 logging.debug('Channel %d closed', channelno)
@@ -577,7 +625,7 @@
                     logging.debug('Session terminated')
                     self.session.close()
             elif cmd == 'ERR':
-                elem = parse_xml(message.get_payload())
+                elem = xmlio.parse(message.get_payload())
                 text = elem.gettext()
                 code = int(elem.code)
                 logging.debug('Peer refused to start channel %d: %s (%d)',
@@ -586,19 +634,21 @@
                     handle_error(code, text)
 
         logging.debug('Requesting closure of channel %d', channelno)
-        xml = Element('close', number=channelno, code=code)
-        return self.channel.send_msg(MIMEMessage(xml, BEEP_XML), handle_reply)
+        xml = xmlio.Element('close', number=channelno, code=code)
+        return self.channel.send_msg(MIMEMessage(xml), handle_reply)
 
     def send_error(self, msgno, code, message=''):
+        """Send an error reply to the peer."""
         logging.warning('%s (%d)', message, code)
-        xml = Element('error', code=code)[message]
-        self.channel.send_err(msgno, MIMEMessage(xml, BEEP_XML))
+        xml = xmlio.Element('error', code=code)[message]
+        self.channel.send_err(msgno, MIMEMessage(xml))
 
     def send_start(self, profiles, handle_ok=None, handle_error=None):
+        """Send a request to start a new channel to the peer."""
         channelno = self.session.channelno.next()
         def handle_reply(cmd, msgno, message):
             if cmd == 'RPY':
-                elem = parse_xml(message.get_payload())
+                elem = xmlio.parse(message.get_payload())
                 for cls in [cls for cls in profiles if cls.URI == elem.uri]:
                     logging.debug('Channel %d started with profile %s',
                                   channelno, elem.uri)
@@ -608,7 +658,7 @@
                 if handle_ok is not None:
                     handle_ok(channelno, elem.uri)
             elif cmd == 'ERR':
-                elem = parse_xml(message.get_payload())
+                elem = xmlio.parse(message.get_payload())
                 text = elem.gettext()
                 code = int(elem.code)
                 logging.debug('Peer refused to start channel %d: %s (%d)',
@@ -618,22 +668,23 @@
 
         logging.debug('Requesting start of channel %d with profiles %s',
                       channelno, [profile.URI for profile in profiles])
-        xml = Element('start', number=channelno)[
-            [Element('profile', uri=profile.URI) for profile in profiles]
+        xml = xmlio.Element('start', number=channelno)[
+            [xmlio.Element('profile', uri=profile.URI) for profile in profiles]
         ]
-        return self.channel.send_msg(MIMEMessage(xml, BEEP_XML), handle_reply)
+        return self.channel.send_msg(MIMEMessage(xml), handle_reply)
 
 
 class MIMEMessage(Message):
     """Simplified construction of generic MIME messages for transmission as
     payload with BEEP."""
 
-    def __init__(self, payload, content_type=None):
+    def __init__(self, payload, content_type=BEEP_XML):
+        """Create the MIME message."""
         Message.__init__(self)
         if content_type:
             self.set_type(content_type)
+            del self['MIME-Version']
         self.set_payload(str(payload))
-        del self['MIME-Version']
 
 
 def cycle_through(start, stop=None, step=1):
@@ -649,7 +700,7 @@
             cur = start
 
 
-class serial(object):
+class SerialNumber(object):
     """Serial number (RFC 1982)."""
 
     def __init__(self, limit=4294967295L):
--- a/bitten/util/tests/beep.py
+++ b/bitten/util/tests/beep.py
@@ -1,8 +1,7 @@
 import logging
 import unittest
 
-from bitten.util import beep
-from bitten.util.xmlio import Element
+from bitten.util import beep, xmlio
 
 
 class MockSession(beep.Initiator):
@@ -12,9 +11,10 @@
         self.profiles = {}
         self.sent_messages = []
         self.channelno = beep.cycle_through(1, 2147483647, step=2)
-        self.channels = {0: beep.Channel(self, 0, beep.ManagementProfileHandler)}
+        self.channels = {0: beep.Channel(self, 0,
+                                         beep.ManagementProfileHandler)}
         del self.sent_messages[0] # Clear out the management greeting
-        self.channels[0].seqno = [beep.serial(), beep.serial()]
+        self.channels[0].seqno = [beep.SerialNumber(), beep.SerialNumber()]
 
     def close(self):
         self.closed = True
@@ -31,9 +31,10 @@
 
     def __init__(self, channel):
         self.handled_messages = []
+        self.init_elem = None
 
-    def handle_connect(self):
-        pass
+    def handle_connect(self, init_elem=None):
+        self.init_elem = init_elem
 
     def handle_disconnect(self):
         pass
@@ -90,7 +91,7 @@
         corresponding message number (0) is reserved.
         """
         channel = beep.Channel(self.session, 0, MockProfileHandler)
-        msgno = channel.send_msg(beep.MIMEMessage('foo bar'))
+        msgno = channel.send_msg(beep.MIMEMessage('foo bar', None))
         self.assertEqual(('MSG', 0, msgno, False, 0L, None, 'foo bar'),
                          self.session.sent_messages[0])
         assert msgno in channel.msgnos
@@ -101,8 +102,8 @@
         expected.
         """
         channel = beep.Channel(self.session, 0, MockProfileHandler)
-        channel.send_msg(beep.MIMEMessage('foo bar'))
-        channel.send_rpy(0, beep.MIMEMessage('nil'))
+        channel.send_msg(beep.MIMEMessage('foo bar', None))
+        channel.send_rpy(0, beep.MIMEMessage('nil', None))
         self.assertEqual(('MSG', 0, 0, False, 0L, None, 'foo bar'),
                          self.session.sent_messages[0])
         self.assertEqual(('RPY', 0, 0, False, 8L, None, 'nil'),
@@ -114,12 +115,12 @@
         messages.
         """
         channel = beep.Channel(self.session, 0, MockProfileHandler)
-        msgno = channel.send_msg(beep.MIMEMessage('foo bar'))
+        msgno = channel.send_msg(beep.MIMEMessage('foo bar', None))
         assert msgno == 0
         self.assertEqual(('MSG', 0, msgno, False, 0L, None, 'foo bar'),
                          self.session.sent_messages[0])
         assert msgno in channel.msgnos
-        msgno = channel.send_msg(beep.MIMEMessage('foo baz'))
+        msgno = channel.send_msg(beep.MIMEMessage('foo baz', None))
         assert msgno == 1
         self.assertEqual(('MSG', 0, msgno, False, 8L, None, 'foo baz'),
                          self.session.sent_messages[1])
@@ -130,7 +131,7 @@
         Verify that sending an ANS message is processed correctly.
         """
         channel = beep.Channel(self.session, 0, MockProfileHandler)
-        channel.send_rpy(0, beep.MIMEMessage('foo bar'))
+        channel.send_rpy(0, beep.MIMEMessage('foo bar', None))
         self.assertEqual(('RPY', 0, 0, False, 0L, None, 'foo bar'),
                          self.session.sent_messages[0])
 
@@ -140,7 +141,7 @@
         received.
         """
         channel = beep.Channel(self.session, 0, MockProfileHandler)
-        msgno = channel.send_msg(beep.MIMEMessage('foo bar'))
+        msgno = channel.send_msg(beep.MIMEMessage('foo bar', None))
         self.assertEqual(('MSG', 0, msgno, False, 0L, None, 'foo bar'),
                          self.session.sent_messages[0])
         assert msgno in channel.msgnos
@@ -154,7 +155,7 @@
         Verify that sending an ERR message is processed correctly.
         """
         channel = beep.Channel(self.session, 0, MockProfileHandler)
-        channel.send_err(0, beep.MIMEMessage('oops'))
+        channel.send_err(0, beep.MIMEMessage('oops', None))
         self.assertEqual(('ERR', 0, 0, False, 0L, None, 'oops'),
                          self.session.sent_messages[0])
 
@@ -163,12 +164,12 @@
         Verify that sending an ANS message is processed correctly.
         """
         channel = beep.Channel(self.session, 0, MockProfileHandler)
-        ansno = channel.send_ans(0, beep.MIMEMessage('foo bar'))
+        ansno = channel.send_ans(0, beep.MIMEMessage('foo bar', None))
         assert ansno == 0
         self.assertEqual(('ANS', 0, 0, False, 0L, ansno, 'foo bar'),
                          self.session.sent_messages[0])
         assert 0 in channel.ansnos
-        ansno = channel.send_ans(0, beep.MIMEMessage('foo baz'))
+        ansno = channel.send_ans(0, beep.MIMEMessage('foo baz', None))
         assert ansno == 1
         self.assertEqual(('ANS', 0, 0, False, 8L, ansno, 'foo baz'),
                          self.session.sent_messages[1])
@@ -193,8 +194,8 @@
         """
         self.profile.handle_connect()
         self.assertEqual(1, len(self.session.sent_messages))
-        xml = Element('greeting')
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('greeting')
+        message = beep.MIMEMessage(xml).as_string()
         self.assertEqual(('RPY', 0, 0, False, 0, None, message),
                          self.session.sent_messages[0])
 
@@ -206,10 +207,10 @@
         self.session.profiles[MockProfileHandler.URI] = MockProfileHandler
         self.profile.handle_connect()
         self.assertEqual(1, len(self.session.sent_messages))
-        xml = Element('greeting')[
-            Element('profile', uri=MockProfileHandler.URI)
+        xml = xmlio.Element('greeting')[
+            xmlio.Element('profile', uri=MockProfileHandler.URI)
         ]
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        message = beep.MIMEMessage(xml).as_string()
         self.assertEqual(('RPY', 0, 0, False, 0, None, message),
                          self.session.sent_messages[0])
 
@@ -223,38 +224,38 @@
             self.assertEqual(['test'], profiles)
         greeting_received.called = False
         self.session.greeting_received = greeting_received
-        xml = Element('greeting')[Element('profile', uri='test')]
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('greeting')[xmlio.Element('profile', uri='test')]
+        message = beep.MIMEMessage(xml).as_string()
         self.channel.handle_data_frame('RPY', 0, False, 0L, None, message)
         assert greeting_received.called
 
     def test_handle_start(self):
         self.session.profiles[MockProfileHandler.URI] = MockProfileHandler
-        xml = Element('start', number=2)[
-            Element('profile', uri=MockProfileHandler.URI),
-            Element('profile', uri='http://example.com/bogus')
+        xml = xmlio.Element('start', number=2)[
+            xmlio.Element('profile', uri=MockProfileHandler.URI),
+            xmlio.Element('profile', uri='http://example.com/bogus')
         ]
-        self.profile.handle_msg(0, beep.MIMEMessage(xml, beep.BEEP_XML))
+        self.profile.handle_msg(0, beep.MIMEMessage(xml))
 
         assert 2 in self.session.channels
-        xml = Element('profile', uri=MockProfileHandler.URI)
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('profile', uri=MockProfileHandler.URI)
+        message = beep.MIMEMessage(xml).as_string()
         self.assertEqual(('RPY', 0, 0, False, 0, None, message),
                          self.session.sent_messages[0])
 
     def test_handle_start_unsupported_profile(self):
         self.session.profiles[MockProfileHandler.URI] = MockProfileHandler
-        xml = Element('start', number=2)[
-            Element('profile', uri='http://example.com/foo'),
-            Element('profile', uri='http://example.com/bar')
+        xml = xmlio.Element('start', number=2)[
+            xmlio.Element('profile', uri='http://example.com/foo'),
+            xmlio.Element('profile', uri='http://example.com/bar')
         ]
-        self.profile.handle_msg(0, beep.MIMEMessage(xml, beep.BEEP_XML))
+        self.profile.handle_msg(0, beep.MIMEMessage(xml))
 
         assert 2 not in self.session.channels
-        xml = Element('error', code=550)[
+        xml = xmlio.Element('error', code=550)[
             'None of the requested profiles is supported'
         ]
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        message = beep.MIMEMessage(xml).as_string()
         self.assertEqual(('ERR', 0, 0, False, 0, None, message),
                          self.session.sent_messages[0])
 
@@ -263,46 +264,46 @@
                                                 MockProfileHandler)
         orig_profile = self.session.channels[2].profile
         self.session.profiles[MockProfileHandler.URI] = MockProfileHandler
-        xml = Element('start', number=2)[
-            Element('profile', uri=MockProfileHandler.URI)
+        xml = xmlio.Element('start', number=2)[
+            xmlio.Element('profile', uri=MockProfileHandler.URI)
         ]
-        self.profile.handle_msg(0, beep.MIMEMessage(xml, beep.BEEP_XML))
+        self.profile.handle_msg(0, beep.MIMEMessage(xml))
 
         assert self.session.channels[2].profile is orig_profile
-        xml = Element('error', code=550)['Channel already in use']
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('error', code=550)['Channel already in use']
+        message = beep.MIMEMessage(xml).as_string()
         self.assertEqual(('ERR', 0, 0, False, 0, None, message),
                          self.session.sent_messages[0])
 
     def test_handle_close(self):
         self.session.channels[1] = beep.Channel(self.session, 1,
                                                 MockProfileHandler)
-        xml = Element('close', number=1, code=200)
-        self.profile.handle_msg(0, beep.MIMEMessage(xml, beep.BEEP_XML))
+        xml = xmlio.Element('close', number=1, code=200)
+        self.profile.handle_msg(0, beep.MIMEMessage(xml))
 
         assert 1 not in self.session.channels
-        xml = Element('ok')
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('ok')
+        message = beep.MIMEMessage(xml).as_string()
         self.assertEqual(('RPY', 0, 0, False, 0, None, message),
                          self.session.sent_messages[0])
 
     def test_handle_close_session(self):
-        xml = Element('close', number=0, code=200)
-        self.profile.handle_msg(0, beep.MIMEMessage(xml, beep.BEEP_XML))
+        xml = xmlio.Element('close', number=0, code=200)
+        self.profile.handle_msg(0, beep.MIMEMessage(xml))
 
         assert 1 not in self.session.channels
-        xml = Element('ok')
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('ok')
+        message = beep.MIMEMessage(xml).as_string()
         self.assertEqual(('RPY', 0, 0, False, 0, None, message),
                          self.session.sent_messages[0])
         assert self.session.closed
 
     def test_handle_close_channel_not_open(self):
-        xml = Element('close', number=1, code=200)
-        self.profile.handle_msg(0, beep.MIMEMessage(xml, beep.BEEP_XML))
+        xml = xmlio.Element('close', number=1, code=200)
+        self.profile.handle_msg(0, beep.MIMEMessage(xml))
 
-        xml = Element('error', code=550)['Channel not open']
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('error', code=550)['Channel not open']
+        message = beep.MIMEMessage(xml).as_string()
         self.assertEqual(('ERR', 0, 0, False, 0, None, message),
                          self.session.sent_messages[0])
 
@@ -312,11 +313,11 @@
         self.session.channels[1].send_msg(beep.MIMEMessage('test'))
         assert self.session.channels[1].msgnos
 
-        xml = Element('close', number=1, code=200)
-        self.profile.handle_msg(0, beep.MIMEMessage(xml, beep.BEEP_XML))
+        xml = xmlio.Element('close', number=1, code=200)
+        self.profile.handle_msg(0, beep.MIMEMessage(xml))
 
-        xml = Element('error', code=550)['Channel waiting for replies']
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('error', code=550)['Channel waiting for replies']
+        message = beep.MIMEMessage(xml).as_string()
         self.assertEqual(('ERR', 0, 0, False, 0, None, message),
                          self.session.sent_messages[1])
 
@@ -324,11 +325,11 @@
         self.session.channels[1] = beep.Channel(self.session, 1,
                                                 MockProfileHandler)
 
-        xml = Element('close', number=0, code=200)
-        self.profile.handle_msg(0, beep.MIMEMessage(xml, beep.BEEP_XML))
+        xml = xmlio.Element('close', number=0, code=200)
+        self.profile.handle_msg(0, beep.MIMEMessage(xml))
 
-        xml = Element('error', code=550)['Other channels still open']
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('error', code=550)['Other channels still open']
+        message = beep.MIMEMessage(xml).as_string()
         self.assertEqual(('ERR', 0, 0, False, 0, None, message),
                          self.session.sent_messages[0])
 
@@ -337,8 +338,8 @@
         Verify that a negative reply is sent as expected.
         """
         self.profile.send_error(0, 521, 'ouch')
-        xml = Element('error', code=521)['ouch']
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('error', code=521)['ouch']
+        message = beep.MIMEMessage(xml).as_string()
         self.assertEqual(('ERR', 0, 0, False, 0, None, message),
                          self.session.sent_messages[0])
 
@@ -347,10 +348,10 @@
         Verify that a <start> request is sent correctly.
         """
         self.profile.send_start([MockProfileHandler])
-        xml = Element('start', number="1")[
-            Element('profile', uri=MockProfileHandler.URI)
+        xml = xmlio.Element('start', number="1")[
+            xmlio.Element('profile', uri=MockProfileHandler.URI)
         ]
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        message = beep.MIMEMessage(xml).as_string()
         self.assertEqual(('MSG', 0, 0, False, 0, None, message),
                          self.session.sent_messages[0])
 
@@ -360,8 +361,8 @@
         and the channel is created.
         """
         self.profile.send_start([MockProfileHandler])
-        xml = Element('profile', uri=MockProfileHandler.URI)
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('profile', uri=MockProfileHandler.URI)
+        message = beep.MIMEMessage(xml).as_string()
         self.channel.handle_data_frame('RPY', 0, False, 0L, None, message)
         assert isinstance(self.session.channels[1].profile, MockProfileHandler)
 
@@ -371,8 +372,8 @@
         and no channel gets created.
         """
         self.profile.send_start([MockProfileHandler])
-        xml = Element('error', code=500)['ouch']
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('error', code=500)['ouch']
+        message = beep.MIMEMessage(xml).as_string()
         self.channel.handle_data_frame('ERR', 0, False, 0L, None, message)
         assert 1 not in self.session.channels
 
@@ -392,8 +393,8 @@
         self.profile.send_start([MockProfileHandler], handle_ok=handle_ok,
                                 handle_error=handle_error)
 
-        xml = Element('profile', uri=MockProfileHandler.URI)
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('profile', uri=MockProfileHandler.URI)
+        message = beep.MIMEMessage(xml).as_string()
         self.channel.handle_data_frame('RPY', 0, False, 0L, None, message)
         assert isinstance(self.session.channels[1].profile, MockProfileHandler)
         assert handle_ok.called
@@ -415,8 +416,8 @@
         self.profile.send_start([MockProfileHandler], handle_ok=handle_ok,
                                 handle_error=handle_error)
 
-        xml = Element('error', code=500)['ouch']
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('error', code=500)['ouch']
+        message = beep.MIMEMessage(xml).as_string()
         self.channel.handle_data_frame('ERR', 0, False, 0L, None, message)
         assert 1 not in self.session.channels
         assert not handle_ok.called
@@ -427,8 +428,8 @@
         Verify that a <close> request is sent correctly.
         """
         self.profile.send_close(1, code=200)
-        xml = Element('close', number=1, code=200)
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('close', number=1, code=200)
+        message = beep.MIMEMessage(xml).as_string()
         self.assertEqual(('MSG', 0, 0, False, 0, None, message),
                          self.session.sent_messages[0])
 
@@ -441,8 +442,8 @@
                                                 MockProfileHandler)
         self.profile.send_close(1, code=200)
 
-        xml = Element('ok')
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('ok')
+        message = beep.MIMEMessage(xml).as_string()
         self.channel.handle_data_frame('RPY', 0, False, 0L, None, message)
         assert 1 not in self.session.channels
 
@@ -453,8 +454,8 @@
         """
         self.profile.send_close(0, code=200)
 
-        xml = Element('ok')
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('ok')
+        message = beep.MIMEMessage(xml).as_string()
         self.channel.handle_data_frame('RPY', 0, False, 0L, None, message)
         assert 0 not in self.session.channels
         assert self.session.closed
@@ -468,8 +469,8 @@
                                                 MockProfileHandler)
         self.profile.send_close(1, code=200)
 
-        xml = Element('error', code=500)['ouch']
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('error', code=500)['ouch']
+        message = beep.MIMEMessage(xml).as_string()
         self.channel.handle_data_frame('ERR', 0, False, 0L, None, message)
         assert 1 in self.session.channels
 
@@ -489,8 +490,8 @@
         self.profile.send_close(1, code=200, handle_ok=handle_ok,
                                 handle_error=handle_error)
 
-        xml = Element('profile', uri=MockProfileHandler.URI)
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('profile', uri=MockProfileHandler.URI)
+        message = beep.MIMEMessage(xml).as_string()
         self.channel.handle_data_frame('RPY', 0, False, 0L, None, message)
         assert 1 not in self.session.channels
         assert handle_ok.called
@@ -514,8 +515,8 @@
         self.profile.send_close(1, code=200, handle_ok=handle_ok,
                                 handle_error=handle_error)
 
-        xml = Element('error', code=500)['ouch']
-        message = beep.MIMEMessage(xml, beep.BEEP_XML).as_string()
+        xml = xmlio.Element('error', code=500)['ouch']
+        message = beep.MIMEMessage(xml).as_string()
         self.channel.handle_data_frame('ERR', 0, False, 0L, None, message)
         assert 1 in self.session.channels
         assert not handle_ok.called
@@ -529,4 +530,5 @@
     return suite
 
 if __name__ == '__main__':
+    logging.getLogger().setLevel(logging.CRITICAL)
     unittest.main(defaultTest='suite')
--- a/bitten/util/xmlio.py
+++ b/bitten/util/xmlio.py
@@ -67,6 +67,14 @@
 
     >>> print Element('foo')['Hello ', Element('b')['world']]
     <foo>Hello <b>world</b></foo>
+
+    Finally, text starting with an opening angle bracket is treated specially:
+    under the assumption that the text actually contains XML itself, the whole
+    thing is wrapped in a CDATA block instead of escaping all special characters
+    individually:
+
+    >>> print Element('foo')['<bar a="3" b="4"><baz/></bar>']
+    <foo><![CDATA[<bar a="3" b="4"><baz/></bar>]]></foo>
     """
     __slots__ = ['tagname', 'attrs', 'children']
 
@@ -83,7 +91,8 @@
         """Add child nodes to an element."""
         if not isinstance(children, (list, tuple)):
             children = [children]
-        self.children = children
+        self.children = [child for child in children
+                         if child is not None and child != '']
         return self
 
     def __str__(self):
@@ -106,7 +115,10 @@
                 if isinstance(child, Element):
                     child.write(out, newlines=newlines)
                 else:
-                    out.write(self._escape_text(child))
+                    if child[0] == '<':
+                        out.write('<![CDATA[' + child + ']]>')
+                    else:
+                        out.write(self._escape_text(child))
             out.write('</' + self.tagname + '>')
         else:
             out.write('/>')
@@ -134,7 +146,7 @@
         parent.children.append(self)
 
 
-def parse_xml(text):
+def parse(text):
     from xml.dom import minidom
     if isinstance(text, (str, unicode)):
         dom = minidom.parseString(text)
Copyright (C) 2012-2017 Edgewall Software