##// END OF EJS Templates
split serialize step of Session.send into separate method...
MinRK -
Show More
@@ -1,410 +1,416 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """edited session.py to work with streams, and move msg_type to the header
2 """edited session.py to work with streams, and move msg_type to the header
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (C) 2010-2011 The IPython Development Team
5 # Copyright (C) 2010-2011 The IPython Development Team
6 #
6 #
7 # Distributed under the terms of the BSD License. The full license is in
7 # Distributed under the terms of the BSD License. The full license is in
8 # the file COPYING, distributed as part of this software.
8 # the file COPYING, distributed as part of this software.
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11
11
12 import os
12 import os
13 import pprint
13 import pprint
14 import uuid
14 import uuid
15 from datetime import datetime
15 from datetime import datetime
16
16
17 try:
17 try:
18 import cPickle
18 import cPickle
19 pickle = cPickle
19 pickle = cPickle
20 except:
20 except:
21 cPickle = None
21 cPickle = None
22 import pickle
22 import pickle
23
23
24 import zmq
24 import zmq
25 from zmq.utils import jsonapi
25 from zmq.utils import jsonapi
26 from zmq.eventloop.zmqstream import ZMQStream
26 from zmq.eventloop.zmqstream import ZMQStream
27
27
28 from .util import ISO8601
28 from .util import ISO8601
29
29
30 def squash_unicode(obj):
30 def squash_unicode(obj):
31 """coerce unicode back to bytestrings."""
31 """coerce unicode back to bytestrings."""
32 if isinstance(obj,dict):
32 if isinstance(obj,dict):
33 for key in obj.keys():
33 for key in obj.keys():
34 obj[key] = squash_unicode(obj[key])
34 obj[key] = squash_unicode(obj[key])
35 if isinstance(key, unicode):
35 if isinstance(key, unicode):
36 obj[squash_unicode(key)] = obj.pop(key)
36 obj[squash_unicode(key)] = obj.pop(key)
37 elif isinstance(obj, list):
37 elif isinstance(obj, list):
38 for i,v in enumerate(obj):
38 for i,v in enumerate(obj):
39 obj[i] = squash_unicode(v)
39 obj[i] = squash_unicode(v)
40 elif isinstance(obj, unicode):
40 elif isinstance(obj, unicode):
41 obj = obj.encode('utf8')
41 obj = obj.encode('utf8')
42 return obj
42 return obj
43
43
44 def _date_default(obj):
44 def _date_default(obj):
45 if isinstance(obj, datetime):
45 if isinstance(obj, datetime):
46 return obj.strftime(ISO8601)
46 return obj.strftime(ISO8601)
47 else:
47 else:
48 raise TypeError("%r is not JSON serializable"%obj)
48 raise TypeError("%r is not JSON serializable"%obj)
49
49
50 _default_key = 'on_unknown' if jsonapi.jsonmod.__name__ == 'jsonlib' else 'default'
50 _default_key = 'on_unknown' if jsonapi.jsonmod.__name__ == 'jsonlib' else 'default'
51 json_packer = lambda obj: jsonapi.dumps(obj, **{_default_key:_date_default})
51 json_packer = lambda obj: jsonapi.dumps(obj, **{_default_key:_date_default})
52 json_unpacker = lambda s: squash_unicode(jsonapi.loads(s))
52 json_unpacker = lambda s: squash_unicode(jsonapi.loads(s))
53
53
54 pickle_packer = lambda o: pickle.dumps(o,-1)
54 pickle_packer = lambda o: pickle.dumps(o,-1)
55 pickle_unpacker = pickle.loads
55 pickle_unpacker = pickle.loads
56
56
57 default_packer = json_packer
57 default_packer = json_packer
58 default_unpacker = json_unpacker
58 default_unpacker = json_unpacker
59
59
60
60
61 DELIM="<IDS|MSG>"
61 DELIM="<IDS|MSG>"
62
62
63 class Message(object):
63 class Message(object):
64 """A simple message object that maps dict keys to attributes.
64 """A simple message object that maps dict keys to attributes.
65
65
66 A Message can be created from a dict and a dict from a Message instance
66 A Message can be created from a dict and a dict from a Message instance
67 simply by calling dict(msg_obj)."""
67 simply by calling dict(msg_obj)."""
68
68
69 def __init__(self, msg_dict):
69 def __init__(self, msg_dict):
70 dct = self.__dict__
70 dct = self.__dict__
71 for k, v in dict(msg_dict).iteritems():
71 for k, v in dict(msg_dict).iteritems():
72 if isinstance(v, dict):
72 if isinstance(v, dict):
73 v = Message(v)
73 v = Message(v)
74 dct[k] = v
74 dct[k] = v
75
75
76 # Having this iterator lets dict(msg_obj) work out of the box.
76 # Having this iterator lets dict(msg_obj) work out of the box.
77 def __iter__(self):
77 def __iter__(self):
78 return iter(self.__dict__.iteritems())
78 return iter(self.__dict__.iteritems())
79
79
80 def __repr__(self):
80 def __repr__(self):
81 return repr(self.__dict__)
81 return repr(self.__dict__)
82
82
83 def __str__(self):
83 def __str__(self):
84 return pprint.pformat(self.__dict__)
84 return pprint.pformat(self.__dict__)
85
85
86 def __contains__(self, k):
86 def __contains__(self, k):
87 return k in self.__dict__
87 return k in self.__dict__
88
88
89 def __getitem__(self, k):
89 def __getitem__(self, k):
90 return self.__dict__[k]
90 return self.__dict__[k]
91
91
92
92
93 def msg_header(msg_id, msg_type, username, session):
93 def msg_header(msg_id, msg_type, username, session):
94 date=datetime.now().strftime(ISO8601)
94 date=datetime.now().strftime(ISO8601)
95 return locals()
95 return locals()
96
96
97 def extract_header(msg_or_header):
97 def extract_header(msg_or_header):
98 """Given a message or header, return the header."""
98 """Given a message or header, return the header."""
99 if not msg_or_header:
99 if not msg_or_header:
100 return {}
100 return {}
101 try:
101 try:
102 # See if msg_or_header is the entire message.
102 # See if msg_or_header is the entire message.
103 h = msg_or_header['header']
103 h = msg_or_header['header']
104 except KeyError:
104 except KeyError:
105 try:
105 try:
106 # See if msg_or_header is just the header
106 # See if msg_or_header is just the header
107 h = msg_or_header['msg_id']
107 h = msg_or_header['msg_id']
108 except KeyError:
108 except KeyError:
109 raise
109 raise
110 else:
110 else:
111 h = msg_or_header
111 h = msg_or_header
112 if not isinstance(h, dict):
112 if not isinstance(h, dict):
113 h = dict(h)
113 h = dict(h)
114 return h
114 return h
115
115
116 class StreamSession(object):
116 class StreamSession(object):
117 """tweaked version of IPython.zmq.session.Session, for development in Parallel"""
117 """tweaked version of IPython.zmq.session.Session, for development in Parallel"""
118 debug=False
118 debug=False
119 key=None
119 key=None
120
120
121 def __init__(self, username=None, session=None, packer=None, unpacker=None, key=None, keyfile=None):
121 def __init__(self, username=None, session=None, packer=None, unpacker=None, key=None, keyfile=None):
122 if username is None:
122 if username is None:
123 username = os.environ.get('USER','username')
123 username = os.environ.get('USER','username')
124 self.username = username
124 self.username = username
125 if session is None:
125 if session is None:
126 self.session = str(uuid.uuid4())
126 self.session = str(uuid.uuid4())
127 else:
127 else:
128 self.session = session
128 self.session = session
129 self.msg_id = str(uuid.uuid4())
129 self.msg_id = str(uuid.uuid4())
130 if packer is None:
130 if packer is None:
131 self.pack = default_packer
131 self.pack = default_packer
132 else:
132 else:
133 if not callable(packer):
133 if not callable(packer):
134 raise TypeError("packer must be callable, not %s"%type(packer))
134 raise TypeError("packer must be callable, not %s"%type(packer))
135 self.pack = packer
135 self.pack = packer
136
136
137 if unpacker is None:
137 if unpacker is None:
138 self.unpack = default_unpacker
138 self.unpack = default_unpacker
139 else:
139 else:
140 if not callable(unpacker):
140 if not callable(unpacker):
141 raise TypeError("unpacker must be callable, not %s"%type(unpacker))
141 raise TypeError("unpacker must be callable, not %s"%type(unpacker))
142 self.unpack = unpacker
142 self.unpack = unpacker
143
143
144 if key is not None and keyfile is not None:
144 if key is not None and keyfile is not None:
145 raise TypeError("Must specify key OR keyfile, not both")
145 raise TypeError("Must specify key OR keyfile, not both")
146 if keyfile is not None:
146 if keyfile is not None:
147 with open(keyfile) as f:
147 with open(keyfile) as f:
148 self.key = f.read().strip()
148 self.key = f.read().strip()
149 else:
149 else:
150 self.key = key
150 self.key = key
151 if isinstance(self.key, unicode):
151 if isinstance(self.key, unicode):
152 self.key = self.key.encode('utf8')
152 self.key = self.key.encode('utf8')
153 # print key, keyfile, self.key
153 # print key, keyfile, self.key
154 self.none = self.pack({})
154 self.none = self.pack({})
155
155
156 def msg_header(self, msg_type):
156 def msg_header(self, msg_type):
157 h = msg_header(self.msg_id, msg_type, self.username, self.session)
157 h = msg_header(self.msg_id, msg_type, self.username, self.session)
158 self.msg_id = str(uuid.uuid4())
158 self.msg_id = str(uuid.uuid4())
159 return h
159 return h
160
160
161 def msg(self, msg_type, content=None, parent=None, subheader=None):
161 def msg(self, msg_type, content=None, parent=None, subheader=None):
162 msg = {}
162 msg = {}
163 msg['header'] = self.msg_header(msg_type)
163 msg['header'] = self.msg_header(msg_type)
164 msg['msg_id'] = msg['header']['msg_id']
164 msg['msg_id'] = msg['header']['msg_id']
165 msg['parent_header'] = {} if parent is None else extract_header(parent)
165 msg['parent_header'] = {} if parent is None else extract_header(parent)
166 msg['msg_type'] = msg_type
166 msg['msg_type'] = msg_type
167 msg['content'] = {} if content is None else content
167 msg['content'] = {} if content is None else content
168 sub = {} if subheader is None else subheader
168 sub = {} if subheader is None else subheader
169 msg['header'].update(sub)
169 msg['header'].update(sub)
170 return msg
170 return msg
171
171
172 def check_key(self, msg_or_header):
172 def check_key(self, msg_or_header):
173 """Check that a message's header has the right key"""
173 """Check that a message's header has the right key"""
174 if self.key is None:
174 if self.key is None:
175 return True
175 return True
176 header = extract_header(msg_or_header)
176 header = extract_header(msg_or_header)
177 return header.get('key', None) == self.key
177 return header.get('key', None) == self.key
178
178
179
180 def serialize(self, msg, ident=None):
181 content = msg.get('content', {})
182 if content is None:
183 content = self.none
184 elif isinstance(content, dict):
185 content = self.pack(content)
186 elif isinstance(content, bytes):
187 # content is already packed, as in a relayed message
188 pass
189 else:
190 raise TypeError("Content incorrect type: %s"%type(content))
191
192 to_send = []
193
194 if isinstance(ident, list):
195 # accept list of idents
196 to_send.extend(ident)
197 elif ident is not None:
198 to_send.append(ident)
199 to_send.append(DELIM)
200 if self.key is not None:
201 to_send.append(self.key)
202 to_send.append(self.pack(msg['header']))
203 to_send.append(self.pack(msg['parent_header']))
204 to_send.append(content)
205
206 return to_send
179
207
180 def send(self, stream, msg_or_type, content=None, buffers=None, parent=None, subheader=None, ident=None, track=False):
208 def send(self, stream, msg_or_type, content=None, buffers=None, parent=None, subheader=None, ident=None, track=False):
181 """Build and send a message via stream or socket.
209 """Build and send a message via stream or socket.
182
210
183 Parameters
211 Parameters
184 ----------
212 ----------
185
213
186 stream : zmq.Socket or ZMQStream
214 stream : zmq.Socket or ZMQStream
187 the socket-like object used to send the data
215 the socket-like object used to send the data
188 msg_or_type : str or Message/dict
216 msg_or_type : str or Message/dict
189 Normally, msg_or_type will be a msg_type unless a message is being sent more
217 Normally, msg_or_type will be a msg_type unless a message is being sent more
190 than once.
218 than once.
191
219
192 content : dict or None
220 content : dict or None
193 the content of the message (ignored if msg_or_type is a message)
221 the content of the message (ignored if msg_or_type is a message)
194 buffers : list or None
222 buffers : list or None
195 the already-serialized buffers to be appended to the message
223 the already-serialized buffers to be appended to the message
196 parent : Message or dict or None
224 parent : Message or dict or None
197 the parent or parent header describing the parent of this message
225 the parent or parent header describing the parent of this message
198 subheader : dict or None
226 subheader : dict or None
199 extra header keys for this message's header
227 extra header keys for this message's header
200 ident : bytes or list of bytes
228 ident : bytes or list of bytes
201 the zmq.IDENTITY routing path
229 the zmq.IDENTITY routing path
202 track : bool
230 track : bool
203 whether to track. Only for use with Sockets, because ZMQStream objects cannot track messages.
231 whether to track. Only for use with Sockets, because ZMQStream objects cannot track messages.
204
232
205 Returns
233 Returns
206 -------
234 -------
207 msg : message dict
235 msg : message dict
208 the constructed message
236 the constructed message
209 (msg,tracker) : (message dict, MessageTracker)
237 (msg,tracker) : (message dict, MessageTracker)
210 if track=True, then a 2-tuple will be returned, the first element being the constructed
238 if track=True, then a 2-tuple will be returned, the first element being the constructed
211 message, and the second being the MessageTracker
239 message, and the second being the MessageTracker
212
240
213 """
241 """
214
242
215 if not isinstance(stream, (zmq.Socket, ZMQStream)):
243 if not isinstance(stream, (zmq.Socket, ZMQStream)):
216 raise TypeError("stream must be Socket or ZMQStream, not %r"%type(stream))
244 raise TypeError("stream must be Socket or ZMQStream, not %r"%type(stream))
217 elif track and isinstance(stream, ZMQStream):
245 elif track and isinstance(stream, ZMQStream):
218 raise TypeError("ZMQStream cannot track messages")
246 raise TypeError("ZMQStream cannot track messages")
219
247
220 if isinstance(msg_or_type, (Message, dict)):
248 if isinstance(msg_or_type, (Message, dict)):
221 # we got a Message, not a msg_type
249 # we got a Message, not a msg_type
222 # don't build a new Message
250 # don't build a new Message
223 msg = msg_or_type
251 msg = msg_or_type
224 content = msg['content']
225 else:
252 else:
226 msg = self.msg(msg_or_type, content, parent, subheader)
253 msg = self.msg(msg_or_type, content, parent, subheader)
227
254
228 buffers = [] if buffers is None else buffers
255 buffers = [] if buffers is None else buffers
229 to_send = []
256 to_send = self.serialize(msg, ident)
230 if isinstance(ident, list):
231 # accept list of idents
232 to_send.extend(ident)
233 elif ident is not None:
234 to_send.append(ident)
235 to_send.append(DELIM)
236 if self.key is not None:
237 to_send.append(self.key)
238 to_send.append(self.pack(msg['header']))
239 to_send.append(self.pack(msg['parent_header']))
240
241 if content is None:
242 content = self.none
243 elif isinstance(content, dict):
244 content = self.pack(content)
245 elif isinstance(content, bytes):
246 # content is already packed, as in a relayed message
247 pass
248 else:
249 raise TypeError("Content incorrect type: %s"%type(content))
250 to_send.append(content)
251 flag = 0
257 flag = 0
252 if buffers:
258 if buffers:
253 flag = zmq.SNDMORE
259 flag = zmq.SNDMORE
254 _track = False
260 _track = False
255 else:
261 else:
256 _track=track
262 _track=track
257 if track:
263 if track:
258 tracker = stream.send_multipart(to_send, flag, copy=False, track=_track)
264 tracker = stream.send_multipart(to_send, flag, copy=False, track=_track)
259 else:
265 else:
260 tracker = stream.send_multipart(to_send, flag, copy=False)
266 tracker = stream.send_multipart(to_send, flag, copy=False)
261 for b in buffers[:-1]:
267 for b in buffers[:-1]:
262 stream.send(b, flag, copy=False)
268 stream.send(b, flag, copy=False)
263 if buffers:
269 if buffers:
264 if track:
270 if track:
265 tracker = stream.send(buffers[-1], copy=False, track=track)
271 tracker = stream.send(buffers[-1], copy=False, track=track)
266 else:
272 else:
267 tracker = stream.send(buffers[-1], copy=False)
273 tracker = stream.send(buffers[-1], copy=False)
268
274
269 # omsg = Message(msg)
275 # omsg = Message(msg)
270 if self.debug:
276 if self.debug:
271 pprint.pprint(msg)
277 pprint.pprint(msg)
272 pprint.pprint(to_send)
278 pprint.pprint(to_send)
273 pprint.pprint(buffers)
279 pprint.pprint(buffers)
274
280
275 msg['tracker'] = tracker
281 msg['tracker'] = tracker
276
282
277 return msg
283 return msg
278
284
279 def send_raw(self, stream, msg, flags=0, copy=True, ident=None):
285 def send_raw(self, stream, msg, flags=0, copy=True, ident=None):
280 """Send a raw message via ident path.
286 """Send a raw message via ident path.
281
287
282 Parameters
288 Parameters
283 ----------
289 ----------
284 msg : list of sendable buffers"""
290 msg : list of sendable buffers"""
285 to_send = []
291 to_send = []
286 if isinstance(ident, bytes):
292 if isinstance(ident, bytes):
287 ident = [ident]
293 ident = [ident]
288 if ident is not None:
294 if ident is not None:
289 to_send.extend(ident)
295 to_send.extend(ident)
290 to_send.append(DELIM)
296 to_send.append(DELIM)
291 if self.key is not None:
297 if self.key is not None:
292 to_send.append(self.key)
298 to_send.append(self.key)
293 to_send.extend(msg)
299 to_send.extend(msg)
294 stream.send_multipart(msg, flags, copy=copy)
300 stream.send_multipart(msg, flags, copy=copy)
295
301
296 def recv(self, socket, mode=zmq.NOBLOCK, content=True, copy=True):
302 def recv(self, socket, mode=zmq.NOBLOCK, content=True, copy=True):
297 """receives and unpacks a message
303 """receives and unpacks a message
298 returns [idents], msg"""
304 returns [idents], msg"""
299 if isinstance(socket, ZMQStream):
305 if isinstance(socket, ZMQStream):
300 socket = socket.socket
306 socket = socket.socket
301 try:
307 try:
302 msg = socket.recv_multipart(mode)
308 msg = socket.recv_multipart(mode)
303 except zmq.ZMQError as e:
309 except zmq.ZMQError as e:
304 if e.errno == zmq.EAGAIN:
310 if e.errno == zmq.EAGAIN:
305 # We can convert EAGAIN to None as we know in this case
311 # We can convert EAGAIN to None as we know in this case
306 # recv_multipart won't return None.
312 # recv_multipart won't return None.
307 return None
313 return None
308 else:
314 else:
309 raise
315 raise
310 # return an actual Message object
316 # return an actual Message object
311 # determine the number of idents by trying to unpack them.
317 # determine the number of idents by trying to unpack them.
312 # this is terrible:
318 # this is terrible:
313 idents, msg = self.feed_identities(msg, copy)
319 idents, msg = self.feed_identities(msg, copy)
314 try:
320 try:
315 return idents, self.unpack_message(msg, content=content, copy=copy)
321 return idents, self.unpack_message(msg, content=content, copy=copy)
316 except Exception as e:
322 except Exception as e:
317 print (idents, msg)
323 print (idents, msg)
318 # TODO: handle it
324 # TODO: handle it
319 raise e
325 raise e
320
326
321 def feed_identities(self, msg, copy=True):
327 def feed_identities(self, msg, copy=True):
322 """feed until DELIM is reached, then return the prefix as idents and remainder as
328 """feed until DELIM is reached, then return the prefix as idents and remainder as
323 msg. This is easily broken by setting an IDENT to DELIM, but that would be silly.
329 msg. This is easily broken by setting an IDENT to DELIM, but that would be silly.
324
330
325 Parameters
331 Parameters
326 ----------
332 ----------
327 msg : a list of Message or bytes objects
333 msg : a list of Message or bytes objects
328 the message to be split
334 the message to be split
329 copy : bool
335 copy : bool
330 flag determining whether the arguments are bytes or Messages
336 flag determining whether the arguments are bytes or Messages
331
337
332 Returns
338 Returns
333 -------
339 -------
334 (idents,msg) : two lists
340 (idents,msg) : two lists
335 idents will always be a list of bytes - the indentity prefix
341 idents will always be a list of bytes - the indentity prefix
336 msg will be a list of bytes or Messages, unchanged from input
342 msg will be a list of bytes or Messages, unchanged from input
337 msg should be unpackable via self.unpack_message at this point.
343 msg should be unpackable via self.unpack_message at this point.
338 """
344 """
339 ikey = int(self.key is not None)
345 ikey = int(self.key is not None)
340 minlen = 3 + ikey
346 minlen = 3 + ikey
341 msg = list(msg)
347 msg = list(msg)
342 idents = []
348 idents = []
343 while len(msg) > minlen:
349 while len(msg) > minlen:
344 if copy:
350 if copy:
345 s = msg[0]
351 s = msg[0]
346 else:
352 else:
347 s = msg[0].bytes
353 s = msg[0].bytes
348 if s == DELIM:
354 if s == DELIM:
349 msg.pop(0)
355 msg.pop(0)
350 break
356 break
351 else:
357 else:
352 idents.append(s)
358 idents.append(s)
353 msg.pop(0)
359 msg.pop(0)
354
360
355 return idents, msg
361 return idents, msg
356
362
357 def unpack_message(self, msg, content=True, copy=True):
363 def unpack_message(self, msg, content=True, copy=True):
358 """Return a message object from the format
364 """Return a message object from the format
359 sent by self.send.
365 sent by self.send.
360
366
361 Parameters:
367 Parameters:
362 -----------
368 -----------
363
369
364 content : bool (True)
370 content : bool (True)
365 whether to unpack the content dict (True),
371 whether to unpack the content dict (True),
366 or leave it serialized (False)
372 or leave it serialized (False)
367
373
368 copy : bool (True)
374 copy : bool (True)
369 whether to return the bytes (True),
375 whether to return the bytes (True),
370 or the non-copying Message object in each place (False)
376 or the non-copying Message object in each place (False)
371
377
372 """
378 """
373 ikey = int(self.key is not None)
379 ikey = int(self.key is not None)
374 minlen = 3 + ikey
380 minlen = 3 + ikey
375 message = {}
381 message = {}
376 if not copy:
382 if not copy:
377 for i in range(minlen):
383 for i in range(minlen):
378 msg[i] = msg[i].bytes
384 msg[i] = msg[i].bytes
379 if ikey:
385 if ikey:
380 if not self.key == msg[0]:
386 if not self.key == msg[0]:
381 raise KeyError("Invalid Session Key: %s"%msg[0])
387 raise KeyError("Invalid Session Key: %s"%msg[0])
382 if not len(msg) >= minlen:
388 if not len(msg) >= minlen:
383 raise TypeError("malformed message, must have at least %i elements"%minlen)
389 raise TypeError("malformed message, must have at least %i elements"%minlen)
384 message['header'] = self.unpack(msg[ikey+0])
390 message['header'] = self.unpack(msg[ikey+0])
385 message['msg_type'] = message['header']['msg_type']
391 message['msg_type'] = message['header']['msg_type']
386 message['parent_header'] = self.unpack(msg[ikey+1])
392 message['parent_header'] = self.unpack(msg[ikey+1])
387 if content:
393 if content:
388 message['content'] = self.unpack(msg[ikey+2])
394 message['content'] = self.unpack(msg[ikey+2])
389 else:
395 else:
390 message['content'] = msg[ikey+2]
396 message['content'] = msg[ikey+2]
391
397
392 message['buffers'] = msg[ikey+3:]# [ m.buffer for m in msg[3:] ]
398 message['buffers'] = msg[ikey+3:]# [ m.buffer for m in msg[3:] ]
393 return message
399 return message
394
400
395
401
396 def test_msg2obj():
402 def test_msg2obj():
397 am = dict(x=1)
403 am = dict(x=1)
398 ao = Message(am)
404 ao = Message(am)
399 assert ao.x == am['x']
405 assert ao.x == am['x']
400
406
401 am['y'] = dict(z=1)
407 am['y'] = dict(z=1)
402 ao = Message(am)
408 ao = Message(am)
403 assert ao.y.z == am['y']['z']
409 assert ao.y.z == am['y']['z']
404
410
405 k1, k2 = 'y', 'z'
411 k1, k2 = 'y', 'z'
406 assert ao[k1][k2] == am[k1][k2]
412 assert ao[k1][k2] == am[k1][k2]
407
413
408 am2 = dict(ao)
414 am2 = dict(ao)
409 assert am['x'] == am2['x']
415 assert am['x'] == am2['x']
410 assert am['y']['z'] == am2['y']['z']
416 assert am['y']['z'] == am2['y']['z']
General Comments 0
You need to be logged in to leave comments. Login now