##// END OF EJS Templates
don't print info when message unpacking fails...
MinRK -
Show More
@@ -1,697 +1,698 b''
1 1 #!/usr/bin/env python
2 2 """Session object for building, serializing, sending, and receiving messages in
3 3 IPython. The Session object supports serialization, HMAC signatures, and
4 4 metadata on messages.
5 5
6 6 Also defined here are utilities for working with Sessions:
7 7 * A SessionFactory to be used as a base class for configurables that work with
8 8 Sessions.
9 9 * A Message object for convenience that allows attribute-access to the msg dict.
10 10
11 11 Authors:
12 12
13 13 * Min RK
14 14 * Brian Granger
15 15 * Fernando Perez
16 16 """
17 17 #-----------------------------------------------------------------------------
18 18 # Copyright (C) 2010-2011 The IPython Development Team
19 19 #
20 20 # Distributed under the terms of the BSD License. The full license is in
21 21 # the file COPYING, distributed as part of this software.
22 22 #-----------------------------------------------------------------------------
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Imports
26 26 #-----------------------------------------------------------------------------
27 27
28 28 import hmac
29 29 import logging
30 30 import os
31 31 import pprint
32 32 import uuid
33 33 from datetime import datetime
34 34
35 35 try:
36 36 import cPickle
37 37 pickle = cPickle
38 38 except:
39 39 cPickle = None
40 40 import pickle
41 41
42 42 import zmq
43 43 from zmq.utils import jsonapi
44 44 from zmq.eventloop.ioloop import IOLoop
45 45 from zmq.eventloop.zmqstream import ZMQStream
46 46
47 47 from IPython.config.configurable import Configurable, LoggingConfigurable
48 48 from IPython.utils.importstring import import_item
49 49 from IPython.utils.jsonutil import extract_dates, squash_dates, date_default
50 50 from IPython.utils.traitlets import (CBytes, Unicode, Bool, Any, Instance, Set,
51 51 DottedObjectName)
52 52
53 53 #-----------------------------------------------------------------------------
54 54 # utility functions
55 55 #-----------------------------------------------------------------------------
56 56
57 57 def squash_unicode(obj):
58 58 """coerce unicode back to bytestrings."""
59 59 if isinstance(obj,dict):
60 60 for key in obj.keys():
61 61 obj[key] = squash_unicode(obj[key])
62 62 if isinstance(key, unicode):
63 63 obj[squash_unicode(key)] = obj.pop(key)
64 64 elif isinstance(obj, list):
65 65 for i,v in enumerate(obj):
66 66 obj[i] = squash_unicode(v)
67 67 elif isinstance(obj, unicode):
68 68 obj = obj.encode('utf8')
69 69 return obj
70 70
71 71 #-----------------------------------------------------------------------------
72 72 # globals and defaults
73 73 #-----------------------------------------------------------------------------
74 74 key = 'on_unknown' if jsonapi.jsonmod.__name__ == 'jsonlib' else 'default'
75 75 json_packer = lambda obj: jsonapi.dumps(obj, **{key:date_default})
76 76 json_unpacker = lambda s: extract_dates(jsonapi.loads(s))
77 77
78 78 pickle_packer = lambda o: pickle.dumps(o,-1)
79 79 pickle_unpacker = pickle.loads
80 80
81 81 default_packer = json_packer
82 82 default_unpacker = json_unpacker
83 83
84 84
85 85 DELIM=b"<IDS|MSG>"
86 86
87 87 #-----------------------------------------------------------------------------
88 88 # Classes
89 89 #-----------------------------------------------------------------------------
90 90
91 91 class SessionFactory(LoggingConfigurable):
92 92 """The Base class for configurables that have a Session, Context, logger,
93 93 and IOLoop.
94 94 """
95 95
96 96 logname = Unicode('')
97 97 def _logname_changed(self, name, old, new):
98 98 self.log = logging.getLogger(new)
99 99
100 100 # not configurable:
101 101 context = Instance('zmq.Context')
102 102 def _context_default(self):
103 103 return zmq.Context.instance()
104 104
105 105 session = Instance('IPython.zmq.session.Session')
106 106
107 107 loop = Instance('zmq.eventloop.ioloop.IOLoop', allow_none=False)
108 108 def _loop_default(self):
109 109 return IOLoop.instance()
110 110
111 111 def __init__(self, **kwargs):
112 112 super(SessionFactory, self).__init__(**kwargs)
113 113
114 114 if self.session is None:
115 115 # construct the session
116 116 self.session = Session(**kwargs)
117 117
118 118
119 119 class Message(object):
120 120 """A simple message object that maps dict keys to attributes.
121 121
122 122 A Message can be created from a dict and a dict from a Message instance
123 123 simply by calling dict(msg_obj)."""
124 124
125 125 def __init__(self, msg_dict):
126 126 dct = self.__dict__
127 127 for k, v in dict(msg_dict).iteritems():
128 128 if isinstance(v, dict):
129 129 v = Message(v)
130 130 dct[k] = v
131 131
132 132 # Having this iterator lets dict(msg_obj) work out of the box.
133 133 def __iter__(self):
134 134 return iter(self.__dict__.iteritems())
135 135
136 136 def __repr__(self):
137 137 return repr(self.__dict__)
138 138
139 139 def __str__(self):
140 140 return pprint.pformat(self.__dict__)
141 141
142 142 def __contains__(self, k):
143 143 return k in self.__dict__
144 144
145 145 def __getitem__(self, k):
146 146 return self.__dict__[k]
147 147
148 148
149 149 def msg_header(msg_id, msg_type, username, session):
150 150 date = datetime.now()
151 151 return locals()
152 152
153 153 def extract_header(msg_or_header):
154 154 """Given a message or header, return the header."""
155 155 if not msg_or_header:
156 156 return {}
157 157 try:
158 158 # See if msg_or_header is the entire message.
159 159 h = msg_or_header['header']
160 160 except KeyError:
161 161 try:
162 162 # See if msg_or_header is just the header
163 163 h = msg_or_header['msg_id']
164 164 except KeyError:
165 165 raise
166 166 else:
167 167 h = msg_or_header
168 168 if not isinstance(h, dict):
169 169 h = dict(h)
170 170 return h
171 171
172 172 class Session(Configurable):
173 173 """Object for handling serialization and sending of messages.
174 174
175 175 The Session object handles building messages and sending them
176 176 with ZMQ sockets or ZMQStream objects. Objects can communicate with each
177 177 other over the network via Session objects, and only need to work with the
178 178 dict-based IPython message spec. The Session will handle
179 179 serialization/deserialization, security, and metadata.
180 180
181 181 Sessions support configurable serialiization via packer/unpacker traits,
182 182 and signing with HMAC digests via the key/keyfile traits.
183 183
184 184 Parameters
185 185 ----------
186 186
187 187 debug : bool
188 188 whether to trigger extra debugging statements
189 189 packer/unpacker : str : 'json', 'pickle' or import_string
190 190 importstrings for methods to serialize message parts. If just
191 191 'json' or 'pickle', predefined JSON and pickle packers will be used.
192 192 Otherwise, the entire importstring must be used.
193 193
194 194 The functions must accept at least valid JSON input, and output *bytes*.
195 195
196 196 For example, to use msgpack:
197 197 packer = 'msgpack.packb', unpacker='msgpack.unpackb'
198 198 pack/unpack : callables
199 199 You can also set the pack/unpack callables for serialization directly.
200 200 session : bytes
201 201 the ID of this Session object. The default is to generate a new UUID.
202 202 username : unicode
203 203 username added to message headers. The default is to ask the OS.
204 204 key : bytes
205 205 The key used to initialize an HMAC signature. If unset, messages
206 206 will not be signed or checked.
207 207 keyfile : filepath
208 208 The file containing a key. If this is set, `key` will be initialized
209 209 to the contents of the file.
210 210
211 211 """
212 212
213 213 debug=Bool(False, config=True, help="""Debug output in the Session""")
214 214
215 215 packer = DottedObjectName('json',config=True,
216 216 help="""The name of the packer for serializing messages.
217 217 Should be one of 'json', 'pickle', or an import name
218 218 for a custom callable serializer.""")
219 219 def _packer_changed(self, name, old, new):
220 220 if new.lower() == 'json':
221 221 self.pack = json_packer
222 222 self.unpack = json_unpacker
223 223 elif new.lower() == 'pickle':
224 224 self.pack = pickle_packer
225 225 self.unpack = pickle_unpacker
226 226 else:
227 227 self.pack = import_item(str(new))
228 228
229 229 unpacker = DottedObjectName('json', config=True,
230 230 help="""The name of the unpacker for unserializing messages.
231 231 Only used with custom functions for `packer`.""")
232 232 def _unpacker_changed(self, name, old, new):
233 233 if new.lower() == 'json':
234 234 self.pack = json_packer
235 235 self.unpack = json_unpacker
236 236 elif new.lower() == 'pickle':
237 237 self.pack = pickle_packer
238 238 self.unpack = pickle_unpacker
239 239 else:
240 240 self.unpack = import_item(str(new))
241 241
242 242 session = CBytes(b'', config=True,
243 243 help="""The UUID identifying this session.""")
244 244 def _session_default(self):
245 245 return bytes(uuid.uuid4())
246 246
247 247 username = Unicode(os.environ.get('USER',u'username'), config=True,
248 248 help="""Username for the Session. Default is your system username.""")
249 249
250 250 # message signature related traits:
251 251 key = CBytes(b'', config=True,
252 252 help="""execution key, for extra authentication.""")
253 253 def _key_changed(self, name, old, new):
254 254 if new:
255 255 self.auth = hmac.HMAC(new)
256 256 else:
257 257 self.auth = None
258 258 auth = Instance(hmac.HMAC)
259 259 digest_history = Set()
260 260
261 261 keyfile = Unicode('', config=True,
262 262 help="""path to file containing execution key.""")
263 263 def _keyfile_changed(self, name, old, new):
264 264 with open(new, 'rb') as f:
265 265 self.key = f.read().strip()
266 266
267 267 pack = Any(default_packer) # the actual packer function
268 268 def _pack_changed(self, name, old, new):
269 269 if not callable(new):
270 270 raise TypeError("packer must be callable, not %s"%type(new))
271 271
272 272 unpack = Any(default_unpacker) # the actual packer function
273 273 def _unpack_changed(self, name, old, new):
274 274 # unpacker is not checked - it is assumed to be
275 275 if not callable(new):
276 276 raise TypeError("unpacker must be callable, not %s"%type(new))
277 277
278 278 def __init__(self, **kwargs):
279 279 """create a Session object
280 280
281 281 Parameters
282 282 ----------
283 283
284 284 debug : bool
285 285 whether to trigger extra debugging statements
286 286 packer/unpacker : str : 'json', 'pickle' or import_string
287 287 importstrings for methods to serialize message parts. If just
288 288 'json' or 'pickle', predefined JSON and pickle packers will be used.
289 289 Otherwise, the entire importstring must be used.
290 290
291 291 The functions must accept at least valid JSON input, and output
292 292 *bytes*.
293 293
294 294 For example, to use msgpack:
295 295 packer = 'msgpack.packb', unpacker='msgpack.unpackb'
296 296 pack/unpack : callables
297 297 You can also set the pack/unpack callables for serialization
298 298 directly.
299 299 session : bytes
300 300 the ID of this Session object. The default is to generate a new
301 301 UUID.
302 302 username : unicode
303 303 username added to message headers. The default is to ask the OS.
304 304 key : bytes
305 305 The key used to initialize an HMAC signature. If unset, messages
306 306 will not be signed or checked.
307 307 keyfile : filepath
308 308 The file containing a key. If this is set, `key` will be
309 309 initialized to the contents of the file.
310 310 """
311 311 super(Session, self).__init__(**kwargs)
312 312 self._check_packers()
313 313 self.none = self.pack({})
314 314
315 315 @property
316 316 def msg_id(self):
317 317 """always return new uuid"""
318 318 return str(uuid.uuid4())
319 319
320 320 def _check_packers(self):
321 321 """check packers for binary data and datetime support."""
322 322 pack = self.pack
323 323 unpack = self.unpack
324 324
325 325 # check simple serialization
326 326 msg = dict(a=[1,'hi'])
327 327 try:
328 328 packed = pack(msg)
329 329 except Exception:
330 330 raise ValueError("packer could not serialize a simple message")
331 331
332 332 # ensure packed message is bytes
333 333 if not isinstance(packed, bytes):
334 334 raise ValueError("message packed to %r, but bytes are required"%type(packed))
335 335
336 336 # check that unpack is pack's inverse
337 337 try:
338 338 unpacked = unpack(packed)
339 339 except Exception:
340 340 raise ValueError("unpacker could not handle the packer's output")
341 341
342 342 # check datetime support
343 343 msg = dict(t=datetime.now())
344 344 try:
345 345 unpacked = unpack(pack(msg))
346 346 except Exception:
347 347 self.pack = lambda o: pack(squash_dates(o))
348 348 self.unpack = lambda s: extract_dates(unpack(s))
349 349
350 350 def msg_header(self, msg_type):
351 351 return msg_header(self.msg_id, msg_type, self.username, self.session)
352 352
353 353 def msg(self, msg_type, content=None, parent=None, subheader=None, header=None):
354 354 """Return the nested message dict.
355 355
356 356 This format is different from what is sent over the wire. The
357 357 serialize/unserialize methods converts this nested message dict to the wire
358 358 format, which is a list of message parts.
359 359 """
360 360 msg = {}
361 361 msg['header'] = self.msg_header(msg_type) if header is None else header
362 362 msg['parent_header'] = {} if parent is None else extract_header(parent)
363 363 msg['content'] = {} if content is None else content
364 364 sub = {} if subheader is None else subheader
365 365 msg['header'].update(sub)
366 366 return msg
367 367
368 368 def sign(self, msg_list):
369 369 """Sign a message with HMAC digest. If no auth, return b''.
370 370
371 371 Parameters
372 372 ----------
373 373 msg_list : list
374 374 The [p_header,p_parent,p_content] part of the message list.
375 375 """
376 376 if self.auth is None:
377 377 return b''
378 378 h = self.auth.copy()
379 379 for m in msg_list:
380 380 h.update(m)
381 381 return h.hexdigest()
382 382
383 383 def serialize(self, msg, ident=None):
384 384 """Serialize the message components to bytes.
385 385
386 386 This is roughly the inverse of unserialize. The serialize/unserialize
387 387 methods work with full message lists, whereas pack/unpack work with
388 388 the individual message parts in the message list.
389 389
390 390 Parameters
391 391 ----------
392 392 msg : dict or Message
393 393 The nexted message dict as returned by the self.msg method.
394 394
395 395 Returns
396 396 -------
397 397 msg_list : list
398 398 The list of bytes objects to be sent with the format:
399 399 [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_content,
400 400 buffer1,buffer2,...]. In this list, the p_* entities are
401 401 the packed or serialized versions, so if JSON is used, these
402 402 are uft8 encoded JSON strings.
403 403 """
404 404 content = msg.get('content', {})
405 405 if content is None:
406 406 content = self.none
407 407 elif isinstance(content, dict):
408 408 content = self.pack(content)
409 409 elif isinstance(content, bytes):
410 410 # content is already packed, as in a relayed message
411 411 pass
412 412 elif isinstance(content, unicode):
413 413 # should be bytes, but JSON often spits out unicode
414 414 content = content.encode('utf8')
415 415 else:
416 416 raise TypeError("Content incorrect type: %s"%type(content))
417 417
418 418 real_message = [self.pack(msg['header']),
419 419 self.pack(msg['parent_header']),
420 420 content
421 421 ]
422 422
423 423 to_send = []
424 424
425 425 if isinstance(ident, list):
426 426 # accept list of idents
427 427 to_send.extend(ident)
428 428 elif ident is not None:
429 429 to_send.append(ident)
430 430 to_send.append(DELIM)
431 431
432 432 signature = self.sign(real_message)
433 433 to_send.append(signature)
434 434
435 435 to_send.extend(real_message)
436 436
437 437 return to_send
438 438
439 439 def send(self, stream, msg_or_type, content=None, parent=None, ident=None,
440 440 buffers=None, subheader=None, track=False, header=None):
441 441 """Build and send a message via stream or socket.
442 442
443 443 The message format used by this function internally is as follows:
444 444
445 445 [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_content,
446 446 buffer1,buffer2,...]
447 447
448 448 The serialize/unserialize methods convert the nested message dict into this
449 449 format.
450 450
451 451 Parameters
452 452 ----------
453 453
454 454 stream : zmq.Socket or ZMQStream
455 455 The socket-like object used to send the data.
456 456 msg_or_type : str or Message/dict
457 457 Normally, msg_or_type will be a msg_type unless a message is being
458 458 sent more than once. If a header is supplied, this can be set to
459 459 None and the msg_type will be pulled from the header.
460 460
461 461 content : dict or None
462 462 The content of the message (ignored if msg_or_type is a message).
463 463 header : dict or None
464 464 The header dict for the message (ignores if msg_to_type is a message).
465 465 parent : Message or dict or None
466 466 The parent or parent header describing the parent of this message
467 467 (ignored if msg_or_type is a message).
468 468 ident : bytes or list of bytes
469 469 The zmq.IDENTITY routing path.
470 470 subheader : dict or None
471 471 Extra header keys for this message's header (ignored if msg_or_type
472 472 is a message).
473 473 buffers : list or None
474 474 The already-serialized buffers to be appended to the message.
475 475 track : bool
476 476 Whether to track. Only for use with Sockets, because ZMQStream
477 477 objects cannot track messages.
478 478
479 479 Returns
480 480 -------
481 481 msg : dict
482 482 The constructed message.
483 483 (msg,tracker) : (dict, MessageTracker)
484 484 if track=True, then a 2-tuple will be returned,
485 485 the first element being the constructed
486 486 message, and the second being the MessageTracker
487 487
488 488 """
489 489
490 490 if not isinstance(stream, (zmq.Socket, ZMQStream)):
491 491 raise TypeError("stream must be Socket or ZMQStream, not %r"%type(stream))
492 492 elif track and isinstance(stream, ZMQStream):
493 493 raise TypeError("ZMQStream cannot track messages")
494 494
495 495 if isinstance(msg_or_type, (Message, dict)):
496 496 # We got a Message or message dict, not a msg_type so don't
497 497 # build a new Message.
498 498 msg = msg_or_type
499 499 else:
500 500 msg = self.msg(msg_or_type, content=content, parent=parent,
501 501 subheader=subheader, header=header)
502 502
503 503 buffers = [] if buffers is None else buffers
504 504 to_send = self.serialize(msg, ident)
505 505 flag = 0
506 506 if buffers:
507 507 flag = zmq.SNDMORE
508 508 _track = False
509 509 else:
510 510 _track=track
511 511 if track:
512 512 tracker = stream.send_multipart(to_send, flag, copy=False, track=_track)
513 513 else:
514 514 tracker = stream.send_multipart(to_send, flag, copy=False)
515 515 for b in buffers[:-1]:
516 516 stream.send(b, flag, copy=False)
517 517 if buffers:
518 518 if track:
519 519 tracker = stream.send(buffers[-1], copy=False, track=track)
520 520 else:
521 521 tracker = stream.send(buffers[-1], copy=False)
522 522
523 523 # omsg = Message(msg)
524 524 if self.debug:
525 525 pprint.pprint(msg)
526 526 pprint.pprint(to_send)
527 527 pprint.pprint(buffers)
528 528
529 529 msg['tracker'] = tracker
530 530
531 531 return msg
532 532
533 533 def send_raw(self, stream, msg_list, flags=0, copy=True, ident=None):
534 534 """Send a raw message via ident path.
535 535
536 536 This method is used to send a already serialized message.
537 537
538 538 Parameters
539 539 ----------
540 540 stream : ZMQStream or Socket
541 541 The ZMQ stream or socket to use for sending the message.
542 542 msg_list : list
543 543 The serialized list of messages to send. This only includes the
544 544 [p_header,p_parent,p_content,buffer1,buffer2,...] portion of
545 545 the message.
546 546 ident : ident or list
547 547 A single ident or a list of idents to use in sending.
548 548 """
549 549 to_send = []
550 550 if isinstance(ident, bytes):
551 551 ident = [ident]
552 552 if ident is not None:
553 553 to_send.extend(ident)
554 554
555 555 to_send.append(DELIM)
556 556 to_send.append(self.sign(msg_list))
557 557 to_send.extend(msg_list)
558 558 stream.send_multipart(msg_list, flags, copy=copy)
559 559
560 560 def recv(self, socket, mode=zmq.NOBLOCK, content=True, copy=True):
561 561 """Receive and unpack a message.
562 562
563 563 Parameters
564 564 ----------
565 565 socket : ZMQStream or Socket
566 566 The socket or stream to use in receiving.
567 567
568 568 Returns
569 569 -------
570 570 [idents], msg
571 571 [idents] is a list of idents and msg is a nested message dict of
572 572 same format as self.msg returns.
573 573 """
574 574 if isinstance(socket, ZMQStream):
575 575 socket = socket.socket
576 576 try:
577 577 msg_list = socket.recv_multipart(mode)
578 578 except zmq.ZMQError as e:
579 579 if e.errno == zmq.EAGAIN:
580 580 # We can convert EAGAIN to None as we know in this case
581 581 # recv_multipart won't return None.
582 582 return None,None
583 583 else:
584 584 raise
585 585 # split multipart message into identity list and message dict
586 586 # invalid large messages can cause very expensive string comparisons
587 587 idents, msg_list = self.feed_identities(msg_list, copy)
588 588 try:
589 589 return idents, self.unserialize(msg_list, content=content, copy=copy)
590 590 except Exception as e:
591 print (idents, msg_list)
592 591 # TODO: handle it
593 592 raise e
594 593
595 594 def feed_identities(self, msg_list, copy=True):
596 595 """Split the identities from the rest of the message.
597 596
598 597 Feed until DELIM is reached, then return the prefix as idents and
599 598 remainder as msg_list. This is easily broken by setting an IDENT to DELIM,
600 599 but that would be silly.
601 600
602 601 Parameters
603 602 ----------
604 603 msg_list : a list of Message or bytes objects
605 604 The message to be split.
606 605 copy : bool
607 606 flag determining whether the arguments are bytes or Messages
608 607
609 608 Returns
610 609 -------
611 610 (idents, msg_list) : two lists
612 611 idents will always be a list of bytes, each of which is a ZMQ
613 612 identity. msg_list will be a list of bytes or zmq.Messages of the
614 613 form [HMAC,p_header,p_parent,p_content,buffer1,buffer2,...] and
615 614 should be unpackable/unserializable via self.unserialize at this
616 615 point.
617 616 """
618 617 if copy:
619 618 idx = msg_list.index(DELIM)
620 619 return msg_list[:idx], msg_list[idx+1:]
621 620 else:
622 621 failed = True
623 622 for idx,m in enumerate(msg_list):
624 623 if m.bytes == DELIM:
625 624 failed = False
626 625 break
627 626 if failed:
628 627 raise ValueError("DELIM not in msg_list")
629 628 idents, msg_list = msg_list[:idx], msg_list[idx+1:]
630 629 return [m.bytes for m in idents], msg_list
631 630
632 631 def unserialize(self, msg_list, content=True, copy=True):
633 632 """Unserialize a msg_list to a nested message dict.
634 633
635 634 This is roughly the inverse of serialize. The serialize/unserialize
636 635 methods work with full message lists, whereas pack/unpack work with
637 636 the individual message parts in the message list.
638 637
639 638 Parameters:
640 639 -----------
641 640 msg_list : list of bytes or Message objects
642 641 The list of message parts of the form [HMAC,p_header,p_parent,
643 642 p_content,buffer1,buffer2,...].
644 643 content : bool (True)
645 644 Whether to unpack the content dict (True), or leave it packed
646 645 (False).
647 646 copy : bool (True)
648 647 Whether to return the bytes (True), or the non-copying Message
649 648 object in each place (False).
650 649
651 650 Returns
652 651 -------
653 652 msg : dict
654 653 The nested message dict with top-level keys [header, parent_header,
655 654 content, buffers].
656 655 """
657 656 minlen = 4
658 657 message = {}
659 658 if not copy:
660 659 for i in range(minlen):
661 660 msg_list[i] = msg_list[i].bytes
662 661 if self.auth is not None:
663 662 signature = msg_list[0]
663 if not signature:
664 raise ValueError("Unsigned Message")
664 665 if signature in self.digest_history:
665 666 raise ValueError("Duplicate Signature: %r"%signature)
666 667 self.digest_history.add(signature)
667 668 check = self.sign(msg_list[1:4])
668 669 if not signature == check:
669 670 raise ValueError("Invalid Signature: %r"%signature)
670 671 if not len(msg_list) >= minlen:
671 672 raise TypeError("malformed message, must have at least %i elements"%minlen)
672 673 message['header'] = self.unpack(msg_list[1])
673 674 message['parent_header'] = self.unpack(msg_list[2])
674 675 if content:
675 676 message['content'] = self.unpack(msg_list[3])
676 677 else:
677 678 message['content'] = msg_list[3]
678 679
679 680 message['buffers'] = msg_list[4:]
680 681 return message
681 682
682 683 def test_msg2obj():
683 684 am = dict(x=1)
684 685 ao = Message(am)
685 686 assert ao.x == am['x']
686 687
687 688 am['y'] = dict(z=1)
688 689 ao = Message(am)
689 690 assert ao.y.z == am['y']['z']
690 691
691 692 k1, k2 = 'y', 'z'
692 693 assert ao[k1][k2] == am[k1][k2]
693 694
694 695 am2 = dict(ao)
695 696 assert am['x'] == am2['x']
696 697 assert am['y']['z'] == am2['y']['z']
697 698
General Comments 0
You need to be logged in to leave comments. Login now