# HG changeset patch # User cmlenz # Date 1119220973 0 # Node ID 2adc3480e4aad383c9d2eaf86d59d3da05db8650 # Parent 1e562dd56ec06a2b02eaaa160bb0cd171404b9cb Some cleanup and additional docstrings. diff --git a/bitten/master.py b/bitten/master.py --- 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) diff --git a/bitten/slave.py b/bitten/slave.py --- 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 diff --git a/bitten/util/beep.py b/bitten/util/beep.py --- 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): diff --git a/bitten/util/tests/beep.py b/bitten/util/tests/beep.py --- 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 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 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') diff --git a/bitten/util/xmlio.py b/bitten/util/xmlio.py --- a/bitten/util/xmlio.py +++ b/bitten/util/xmlio.py @@ -67,6 +67,14 @@ >>> print Element('foo')['Hello ', Element('b')['world']] Hello world + + 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')[''] + ]]> """ __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('') + else: + out.write(self._escape_text(child)) out.write('') 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)