├── README.rst ├── completer.py ├── frontend.py ├── kernel.py ├── message_spec.rst └── session.py /README.rst: -------------------------------------------------------------------------------- 1 | ======================================================= 2 | Simple interactive Python kernel/frontend with ZeroMQ 3 | ======================================================= 4 | 5 | This is the code that served as the original prototype for today's IPython 6 | client/server model. It is kept here purely as a reference to illustrate how 7 | to implement similar ideas for interactive Python interpreters on top of 8 | zeromq. This example used to be included with pyzmq but for some reason was 9 | removed, so it's available here in standalone form. It should be useful to 10 | anyone wishing to either implement a similar system or understand IPython's 11 | basic architecture without all of the details. 12 | 13 | The message spec included here was the original, minimal spec we used for this 14 | implementation, today's IPython messaging is based on these ideas but has 15 | evolved substantially. 16 | 17 | 18 | Usage 19 | ===== 20 | 21 | Run in one terminal:: 22 | 23 | ./kernel.py 24 | 25 | and in another:: 26 | 27 | ./frontend.py 28 | 29 | In the latter, you can type python code, tab-complete, etc. The kernel 30 | terminal prints all messages for debugging. Exit the frontend with Ctrl-D, and 31 | the kernel with Ctrl-\ (note that Ctrl-C will *not* stop the kernel). 32 | 33 | 34 | License 35 | ======= 36 | 37 | This code is released under the terms of the BSD license, same as IPython 38 | itself. It was originally authored by Brian Granger and Fernando Perez, but no 39 | further development is planned, as all the ideas illustrated here are now 40 | implemented in IPython and developed there as production code. 41 | -------------------------------------------------------------------------------- /completer.py: -------------------------------------------------------------------------------- 1 | """Tab-completion over zmq""" 2 | 3 | # Trying to get print statements to work during completion, not very 4 | # successfully... 5 | from __future__ import print_function 6 | 7 | import itertools 8 | import readline 9 | import rlcompleter 10 | import time 11 | 12 | import session 13 | 14 | class KernelCompleter(object): 15 | """Kernel-side completion machinery.""" 16 | def __init__(self, namespace): 17 | self.namespace = namespace 18 | self.completer = rlcompleter.Completer(namespace) 19 | 20 | def complete(self, line, text): 21 | # We'll likely use linel later even if now it's not used for anything 22 | matches = [] 23 | complete = self.completer.complete 24 | for state in itertools.count(): 25 | comp = complete(text, state) 26 | if comp is None: 27 | break 28 | matches.append(comp) 29 | return matches 30 | 31 | 32 | class ClientCompleter(object): 33 | """Client-side completion machinery. 34 | 35 | How it works: self.complete will be called multiple times, with 36 | state=0,1,2,... When state=0 it should compute ALL the completion matches, 37 | and then return them for each value of state.""" 38 | 39 | def __init__(self, client, session, socket): 40 | # ugly, but we get called asynchronously and need access to some 41 | # client state, like backgrounded code 42 | self.client = client 43 | self.session = session 44 | self.socket = socket 45 | self.matches = [] 46 | 47 | def request_completion(self, text): 48 | # Get full line to give to the kernel in case it wants more info. 49 | line = readline.get_line_buffer() 50 | # send completion request to kernel 51 | msg = self.session.send(self.socket, 52 | 'complete_request', 53 | dict(text=text, line=line)) 54 | 55 | # Give the kernel up to 0.5s to respond 56 | for i in range(5): 57 | rep = self.session.recv(self.socket) 58 | if rep is not None and rep.msg_type == 'complete_reply': 59 | matches = rep.content.matches 60 | break 61 | time.sleep(0.1) 62 | else: 63 | # timeout 64 | print ('TIMEOUT') # Can't see this message... 65 | matches = None 66 | return matches 67 | 68 | def complete(self, text, state): 69 | 70 | if self.client.backgrounded > 0: 71 | print("\n[Not completing, background tasks active]") 72 | print(readline.get_line_buffer(), end='') 73 | return None 74 | 75 | if state==0: 76 | matches = self.request_completion(text) 77 | if matches is None: 78 | self.matches = [] 79 | print('WARNING: Kernel timeout on tab completion.') 80 | else: 81 | self.matches = matches 82 | 83 | try: 84 | return self.matches[state] 85 | except IndexError: 86 | return None 87 | -------------------------------------------------------------------------------- /frontend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A simple interactive frontend that talks to a kernel over 0MQ. 3 | """ 4 | 5 | #----------------------------------------------------------------------------- 6 | # Imports 7 | #----------------------------------------------------------------------------- 8 | # stdlib 9 | import cPickle as pickle 10 | import code 11 | import readline 12 | import sys 13 | import time 14 | import uuid 15 | 16 | # our own 17 | import zmq 18 | import session 19 | import completer 20 | 21 | #----------------------------------------------------------------------------- 22 | # Classes and functions 23 | #----------------------------------------------------------------------------- 24 | 25 | class Console(code.InteractiveConsole): 26 | 27 | def __init__(self, locals=None, filename="", 28 | session = session, 29 | request_socket=None, 30 | sub_socket=None): 31 | code.InteractiveConsole.__init__(self, locals, filename) 32 | self.session = session 33 | self.request_socket = request_socket 34 | self.sub_socket = sub_socket 35 | self.backgrounded = 0 36 | self.messages = {} 37 | 38 | # Set tab completion 39 | self.completer = completer.ClientCompleter(self, session, request_socket) 40 | readline.parse_and_bind('tab: complete') 41 | readline.parse_and_bind('set show-all-if-ambiguous on') 42 | readline.set_completer(self.completer.complete) 43 | 44 | # Set system prompts 45 | sys.ps1 = 'Py>>> ' 46 | sys.ps2 = ' ... ' 47 | sys.ps3 = 'Out : ' 48 | # Build dict of handlers for message types 49 | self.handlers = {} 50 | for msg_type in ['pyin', 'pyout', 'pyerr', 'stream']: 51 | self.handlers[msg_type] = getattr(self, 'handle_%s' % msg_type) 52 | 53 | def handle_pyin(self, omsg): 54 | if omsg.parent_header.session == self.session.session: 55 | return 56 | c = omsg.content.code.rstrip() 57 | if c: 58 | print '[IN from %s]' % omsg.parent_header.username 59 | print c 60 | 61 | def handle_pyout(self, omsg): 62 | #print omsg # dbg 63 | if omsg.parent_header.session == self.session.session: 64 | print "%s%s" % (sys.ps3, omsg.content.data) 65 | else: 66 | print '[Out from %s]' % omsg.parent_header.username 67 | print omsg.content.data 68 | 69 | def print_pyerr(self, err): 70 | print >> sys.stderr, err.etype,':', err.evalue 71 | print >> sys.stderr, ''.join(err.traceback) 72 | 73 | def handle_pyerr(self, omsg): 74 | if omsg.parent_header.session == self.session.session: 75 | return 76 | print >> sys.stderr, '[ERR from %s]' % omsg.parent_header.username 77 | self.print_pyerr(omsg.content) 78 | 79 | def handle_stream(self, omsg): 80 | if omsg.content.name == 'stdout': 81 | outstream = sys.stdout 82 | else: 83 | outstream = sys.stderr 84 | print >> outstream, '*ERR*', 85 | print >> outstream, omsg.content.data, 86 | 87 | def handle_output(self, omsg): 88 | handler = self.handlers.get(omsg.msg_type, None) 89 | if handler is not None: 90 | handler(omsg) 91 | 92 | def recv_output(self): 93 | while True: 94 | omsg = self.session.recv(self.sub_socket) 95 | if omsg is None: 96 | break 97 | self.handle_output(omsg) 98 | 99 | def handle_reply(self, rep): 100 | # Handle any side effects on output channels 101 | self.recv_output() 102 | # Now, dispatch on the possible reply types we must handle 103 | if rep is None: 104 | return 105 | if rep.content.status == 'error': 106 | self.print_pyerr(rep.content) 107 | elif rep.content.status == 'aborted': 108 | print >> sys.stderr, "ERROR: ABORTED" 109 | ab = self.messages[rep.parent_header.msg_id].content 110 | if 'code' in ab: 111 | print >> sys.stderr, ab.code 112 | else: 113 | print >> sys.stderr, ab 114 | 115 | def recv_reply(self): 116 | rep = self.session.recv(self.request_socket) 117 | self.handle_reply(rep) 118 | return rep 119 | 120 | def runcode(self, code): 121 | # We can't pickle code objects, so fetch the actual source 122 | src = '\n'.join(self.buffer) 123 | 124 | # for non-background inputs, if we do have previoiusly backgrounded 125 | # jobs, check to see if they've produced results 126 | if not src.endswith(';'): 127 | while self.backgrounded > 0: 128 | #print 'checking background' 129 | rep = self.recv_reply() 130 | if rep: 131 | self.backgrounded -= 1 132 | time.sleep(0.05) 133 | 134 | # Send code execution message to kernel 135 | omsg = self.session.send(self.request_socket, 136 | 'execute_request', dict(code=src)) 137 | self.messages[omsg.header.msg_id] = omsg 138 | 139 | # Fake asynchronicity by letting the user put ';' at the end of the line 140 | if src.endswith(';'): 141 | self.backgrounded += 1 142 | return 143 | 144 | # For foreground jobs, wait for reply 145 | while True: 146 | rep = self.recv_reply() 147 | if rep is not None: 148 | break 149 | self.recv_output() 150 | time.sleep(0.05) 151 | else: 152 | # We exited without hearing back from the kernel! 153 | print >> sys.stderr, 'ERROR!!! kernel never got back to us!!!' 154 | 155 | 156 | class InteractiveClient(object): 157 | def __init__(self, session, request_socket, sub_socket): 158 | self.session = session 159 | self.request_socket = request_socket 160 | self.sub_socket = sub_socket 161 | self.console = Console(None, '', 162 | session, request_socket, sub_socket) 163 | 164 | def interact(self): 165 | self.console.interact() 166 | 167 | 168 | def main(): 169 | # Defaults 170 | #ip = '192.168.2.109' 171 | ip = '127.0.0.1' 172 | #ip = '99.146.222.252' 173 | port_base = 5555 174 | connection = ('tcp://%s' % ip) + ':%i' 175 | req_conn = connection % port_base 176 | sub_conn = connection % (port_base+1) 177 | 178 | # Create initial sockets 179 | c = zmq.Context() 180 | request_socket = c.socket(zmq.DEALER) 181 | request_socket.connect(req_conn) 182 | 183 | sub_socket = c.socket(zmq.SUB) 184 | sub_socket.connect(sub_conn) 185 | sub_socket.setsockopt(zmq.SUBSCRIBE, '') 186 | 187 | # Make session and user-facing client 188 | sess = session.Session() 189 | client = InteractiveClient(sess, request_socket, sub_socket) 190 | client.interact() 191 | 192 | 193 | if __name__ == '__main__': 194 | main() 195 | -------------------------------------------------------------------------------- /kernel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """A simple interactive kernel that talks to a frontend over 0MQ. 3 | 4 | Things to do: 5 | 6 | * Finish implementing `raw_input`. 7 | * Implement `set_parent` logic. Right before doing exec, the Kernel should 8 | call set_parent on all the PUB objects with the message about to be executed. 9 | * Implement random port and security key logic. 10 | * Implement control messages. 11 | * Implement event loop and poll version. 12 | """ 13 | 14 | import __builtin__ 15 | import sys 16 | import time 17 | import traceback 18 | 19 | from code import CommandCompiler 20 | 21 | import zmq 22 | 23 | from session import Session, Message, extract_header 24 | from completer import KernelCompleter 25 | 26 | class OutStream(object): 27 | """A file like object that publishes the stream to a 0MQ PUB socket.""" 28 | 29 | def __init__(self, session, pub_socket, name, max_buffer=200): 30 | self.session = session 31 | self.pub_socket = pub_socket 32 | self.name = name 33 | self._buffer = [] 34 | self._buffer_len = 0 35 | self.max_buffer = max_buffer 36 | self.parent_header = {} 37 | 38 | def set_parent(self, parent): 39 | self.parent_header = extract_header(parent) 40 | 41 | def close(self): 42 | self.pub_socket = None 43 | 44 | def flush(self): 45 | if self.pub_socket is None: 46 | raise ValueError(u'I/O operation on closed file') 47 | else: 48 | if self._buffer: 49 | data = ''.join(self._buffer) 50 | content = {u'name':self.name, u'data':data} 51 | msg = self.session.msg(u'stream', content=content, 52 | parent=self.parent_header) 53 | print>>sys.__stdout__, Message(msg) 54 | self.pub_socket.send_json(msg) 55 | self._buffer_len = 0 56 | self._buffer = [] 57 | 58 | def isattr(self): 59 | return False 60 | 61 | def next(self): 62 | raise IOError('Read not supported on a write only stream.') 63 | 64 | def read(self, size=None): 65 | raise IOError('Read not supported on a write only stream.') 66 | 67 | readline=read 68 | 69 | def write(self, s): 70 | if self.pub_socket is None: 71 | raise ValueError('I/O operation on closed file') 72 | else: 73 | self._buffer.append(s) 74 | self._buffer_len += len(s) 75 | self._maybe_send() 76 | 77 | def _maybe_send(self): 78 | if '\n' in self._buffer[-1]: 79 | self.flush() 80 | if self._buffer_len > self.max_buffer: 81 | self.flush() 82 | 83 | def writelines(self, sequence): 84 | if self.pub_socket is None: 85 | raise ValueError('I/O operation on closed file') 86 | else: 87 | for s in sequence: 88 | self.write(s) 89 | 90 | 91 | class DisplayHook(object): 92 | 93 | def __init__(self, session, pub_socket): 94 | self.session = session 95 | self.pub_socket = pub_socket 96 | self.parent_header = {} 97 | 98 | def __call__(self, obj): 99 | if obj is None: 100 | return 101 | 102 | __builtin__._ = obj 103 | msg = self.session.msg(u'pyout', {u'data':repr(obj)}, 104 | parent=self.parent_header) 105 | self.pub_socket.send_json(msg) 106 | 107 | def set_parent(self, parent): 108 | self.parent_header = extract_header(parent) 109 | 110 | 111 | class RawInput(object): 112 | 113 | def __init__(self, session, socket): 114 | self.session = session 115 | self.socket = socket 116 | 117 | def __call__(self, prompt=None): 118 | msg = self.session.msg(u'raw_input') 119 | self.socket.send_json(msg) 120 | while True: 121 | try: 122 | reply = self.socket.recv_json(zmq.NOBLOCK) 123 | except zmq.ZMQError, e: 124 | if e.errno == zmq.EAGAIN: 125 | pass 126 | else: 127 | raise 128 | else: 129 | break 130 | return reply[u'content'][u'data'] 131 | 132 | 133 | class Kernel(object): 134 | 135 | def __init__(self, session, reply_socket, pub_socket): 136 | self.session = session 137 | self.reply_socket = reply_socket 138 | self.pub_socket = pub_socket 139 | self.user_ns = {} 140 | self.history = [] 141 | self.compiler = CommandCompiler() 142 | self.completer = KernelCompleter(self.user_ns) 143 | 144 | # Build dict of handlers for message types 145 | self.handlers = {} 146 | for msg_type in ['execute_request', 'complete_request']: 147 | self.handlers[msg_type] = getattr(self, msg_type) 148 | 149 | def abort_queue(self): 150 | while True: 151 | try: 152 | ident = self.reply_socket.recv(zmq.NOBLOCK) 153 | except zmq.ZMQError, e: 154 | if e.errno == zmq.EAGAIN: 155 | break 156 | else: 157 | assert self.reply_socket.rcvmore(), "Unexpected missing message part." 158 | msg = self.reply_socket.recv_json() 159 | print>>sys.__stdout__, "Aborting:" 160 | print>>sys.__stdout__, Message(msg) 161 | msg_type = msg['msg_type'] 162 | reply_type = msg_type.split('_')[0] + '_reply' 163 | reply_msg = self.session.msg(reply_type, {'status' : 'aborted'}, msg) 164 | print>>sys.__stdout__, Message(reply_msg) 165 | self.reply_socket.send(ident,zmq.SNDMORE) 166 | self.reply_socket.send_json(reply_msg) 167 | # We need to wait a bit for requests to come in. This can probably 168 | # be set shorter for true asynchronous clients. 169 | time.sleep(0.1) 170 | 171 | def execute_request(self, ident, parent): 172 | try: 173 | code = parent[u'content'][u'code'] 174 | except: 175 | print>>sys.__stderr__, "Got bad msg: " 176 | print>>sys.__stderr__, Message(parent) 177 | return 178 | pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent) 179 | self.pub_socket.send_json(pyin_msg) 180 | try: 181 | comp_code = self.compiler(code, '') 182 | sys.displayhook.set_parent(parent) 183 | exec comp_code in self.user_ns, self.user_ns 184 | except: 185 | result = u'error' 186 | etype, evalue, tb = sys.exc_info() 187 | tb = traceback.format_exception(etype, evalue, tb) 188 | exc_content = { 189 | u'status' : u'error', 190 | u'traceback' : tb, 191 | u'etype' : unicode(etype), 192 | u'evalue' : unicode(evalue) 193 | } 194 | exc_msg = self.session.msg(u'pyerr', exc_content, parent) 195 | self.pub_socket.send_json(exc_msg) 196 | reply_content = exc_content 197 | else: 198 | reply_content = {'status' : 'ok'} 199 | reply_msg = self.session.msg(u'execute_reply', reply_content, parent) 200 | print>>sys.__stdout__, Message(reply_msg) 201 | self.reply_socket.send(ident, zmq.SNDMORE) 202 | self.reply_socket.send_json(reply_msg) 203 | if reply_msg['content']['status'] == u'error': 204 | self.abort_queue() 205 | 206 | def complete_request(self, ident, parent): 207 | matches = {'matches' : self.complete(parent), 208 | 'status' : 'ok'} 209 | completion_msg = self.session.send(self.reply_socket, 'complete_reply', 210 | matches, parent, ident) 211 | print >> sys.__stdout__, completion_msg 212 | 213 | def complete(self, msg): 214 | return self.completer.complete(msg.content.line, msg.content.text) 215 | 216 | def start(self): 217 | while True: 218 | ident = self.reply_socket.recv() 219 | assert self.reply_socket.rcvmore, "Unexpected missing message part." 220 | msg = self.reply_socket.recv_json() 221 | omsg = Message(msg) 222 | print>>sys.__stdout__, omsg 223 | handler = self.handlers.get(omsg.msg_type, None) 224 | if handler is None: 225 | print >> sys.__stderr__, "UNKNOWN MESSAGE TYPE:", omsg 226 | else: 227 | handler(ident, omsg) 228 | 229 | 230 | def main(): 231 | c = zmq.Context() 232 | 233 | ip = '127.0.0.1' 234 | port_base = 5555 235 | connection = ('tcp://%s' % ip) + ':%i' 236 | rep_conn = connection % port_base 237 | pub_conn = connection % (port_base+1) 238 | 239 | print >>sys.__stdout__, "Starting the kernel..." 240 | print >>sys.__stdout__, "On:",rep_conn, pub_conn 241 | 242 | session = Session(username=u'kernel') 243 | 244 | reply_socket = c.socket(zmq.ROUTER) 245 | reply_socket.bind(rep_conn) 246 | 247 | pub_socket = c.socket(zmq.PUB) 248 | pub_socket.bind(pub_conn) 249 | 250 | stdout = OutStream(session, pub_socket, u'stdout') 251 | stderr = OutStream(session, pub_socket, u'stderr') 252 | sys.stdout = stdout 253 | sys.stderr = stderr 254 | 255 | display_hook = DisplayHook(session, pub_socket) 256 | sys.displayhook = display_hook 257 | 258 | kernel = Kernel(session, reply_socket, pub_socket) 259 | 260 | # For debugging convenience, put sleep and a string in the namespace, so we 261 | # have them every time we start. 262 | kernel.user_ns['sleep'] = time.sleep 263 | kernel.user_ns['s'] = 'Test string' 264 | 265 | print >>sys.__stdout__, "Use Ctrl-\\ (NOT Ctrl-C!) to terminate." 266 | kernel.start() 267 | 268 | 269 | if __name__ == '__main__': 270 | main() 271 | -------------------------------------------------------------------------------- /message_spec.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Message Specification 3 | ===================== 4 | 5 | Note: not all of these have yet been fully fleshed out, but the key ones are, 6 | see kernel and frontend files for actual implementation details. 7 | 8 | General Message Format 9 | ===================== 10 | 11 | General message format:: 12 | 13 | { 14 | header : { 'msg_id' : 10, # start with 0 15 | 'username' : 'name', 16 | 'session' : uuid 17 | }, 18 | parent_header : dict, 19 | msg_type : 'string_message_type', 20 | content : blackbox_dict , # Must be a dict 21 | } 22 | 23 | Side effect: (PUB/SUB) 24 | ====================== 25 | 26 | # msg_type = 'stream' 27 | content = { 28 | name : 'stdout', 29 | data : 'blob', 30 | } 31 | 32 | # msg_type = 'pyin' 33 | content = { 34 | code = 'x=1', 35 | } 36 | 37 | # msg_type = 'pyout' 38 | content = { 39 | data = 'repr(obj)', 40 | prompt_number = 10 41 | } 42 | 43 | # msg_type = 'pyerr' 44 | content = { 45 | traceback : 'full traceback', 46 | exc_type : 'TypeError', 47 | exc_value : 'msg' 48 | } 49 | 50 | # msg_type = 'file' 51 | content = { 52 | path = 'cool.jpg', 53 | data : 'blob' 54 | } 55 | 56 | Request/Reply 57 | ============= 58 | 59 | Execute 60 | ------- 61 | 62 | Request: 63 | 64 | # msg_type = 'execute_request' 65 | content = { 66 | code : 'a = 10', 67 | } 68 | 69 | Reply: 70 | 71 | # msg_type = 'execute_reply' 72 | content = { 73 | 'status' : 'ok' OR 'error' OR 'abort' 74 | # data depends on status value 75 | } 76 | 77 | Complete 78 | -------- 79 | 80 | # msg_type = 'complete_request' 81 | content = { 82 | text : 'a.f', # complete on this 83 | line : 'print a.f' # full line 84 | } 85 | 86 | # msg_type = 'complete_reply' 87 | content = { 88 | matches : ['a.foo', 'a.bar'] 89 | } 90 | 91 | Control 92 | ------- 93 | 94 | # msg_type = 'heartbeat' 95 | content = { 96 | 97 | } 98 | -------------------------------------------------------------------------------- /session.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | import pprint 4 | 5 | import zmq 6 | 7 | class Message(object): 8 | """A simple message object that maps dict keys to attributes. 9 | 10 | A Message can be created from a dict and a dict from a Message instance 11 | simply by calling dict(msg_obj).""" 12 | 13 | def __init__(self, msg_dict): 14 | dct = self.__dict__ 15 | for k, v in msg_dict.iteritems(): 16 | if isinstance(v, dict): 17 | v = Message(v) 18 | dct[k] = v 19 | 20 | # Having this iterator lets dict(msg_obj) work out of the box. 21 | def __iter__(self): 22 | return iter(self.__dict__.iteritems()) 23 | 24 | def __repr__(self): 25 | return repr(self.__dict__) 26 | 27 | def __str__(self): 28 | return pprint.pformat(self.__dict__) 29 | 30 | def __contains__(self, k): 31 | return k in self.__dict__ 32 | 33 | def __getitem__(self, k): 34 | return self.__dict__[k] 35 | 36 | 37 | def msg_header(msg_id, username, session): 38 | return { 39 | 'msg_id' : msg_id, 40 | 'username' : username, 41 | 'session' : session 42 | } 43 | 44 | 45 | def extract_header(msg_or_header): 46 | """Given a message or header, return the header.""" 47 | if not msg_or_header: 48 | return {} 49 | try: 50 | # See if msg_or_header is the entire message. 51 | h = msg_or_header['header'] 52 | except KeyError: 53 | try: 54 | # See if msg_or_header is just the header 55 | h = msg_or_header['msg_id'] 56 | except KeyError: 57 | raise 58 | else: 59 | h = msg_or_header 60 | if not isinstance(h, dict): 61 | h = dict(h) 62 | return h 63 | 64 | 65 | class Session(object): 66 | 67 | def __init__(self, username=os.environ.get('USER','username')): 68 | self.username = username 69 | self.session = str(uuid.uuid4()) 70 | self.msg_id = 0 71 | 72 | def msg_header(self): 73 | h = msg_header(self.msg_id, self.username, self.session) 74 | self.msg_id += 1 75 | return h 76 | 77 | def msg(self, msg_type, content=None, parent=None): 78 | msg = {} 79 | msg['header'] = self.msg_header() 80 | msg['parent_header'] = {} if parent is None else extract_header(parent) 81 | msg['msg_type'] = msg_type 82 | msg['content'] = {} if content is None else content 83 | return msg 84 | 85 | def send(self, socket, msg_type, content=None, parent=None, ident=None): 86 | msg = self.msg(msg_type, content, parent) 87 | if ident is not None: 88 | socket.send(ident, zmq.SNDMORE) 89 | socket.send_json(msg) 90 | omsg = Message(msg) 91 | return omsg 92 | 93 | def recv(self, socket, mode=zmq.NOBLOCK): 94 | try: 95 | msg = socket.recv_json(mode) 96 | except zmq.ZMQError, e: 97 | if e.errno == zmq.EAGAIN: 98 | # We can convert EAGAIN to None as we know in this case 99 | # recv_json won't return None. 100 | return None 101 | else: 102 | raise 103 | return Message(msg) 104 | 105 | def test_msg2obj(): 106 | am = dict(x=1) 107 | ao = Message(am) 108 | assert ao.x == am['x'] 109 | 110 | am['y'] = dict(z=1) 111 | ao = Message(am) 112 | assert ao.y.z == am['y']['z'] 113 | 114 | k1, k2 = 'y', 'z' 115 | assert ao[k1][k2] == am[k1][k2] 116 | 117 | am2 = dict(ao) 118 | assert am['x'] == am2['x'] 119 | assert am['y']['z'] == am2['y']['z'] 120 | --------------------------------------------------------------------------------