##// END OF EJS Templates
* Adding object_info_request support to prototype kernel....
epatters -
Show More
@@ -1,330 +1,316 b''
1 1 # Standard library imports
2 2 from codeop import CommandCompiler
3 3 from threading import Thread
4 4 import time
5 5 import types
6 6
7 7 # System library imports
8 8 from pygments.lexers import PythonLexer
9 9 from PyQt4 import QtCore, QtGui
10 10 import zmq
11 11
12 12 # Local imports
13 13 from call_tip_widget import CallTipWidget
14 14 from completion_lexer import CompletionLexer
15 15 from console_widget import HistoryConsoleWidget
16 16 from pygments_highlighter import PygmentsHighlighter
17 17
18 18
19 19 class FrontendHighlighter(PygmentsHighlighter):
20 20 """ A Python PygmentsHighlighter that can be turned on and off and which
21 21 knows about continuation prompts.
22 22 """
23 23
24 24 def __init__(self, frontend):
25 25 PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer())
26 26 self._current_offset = 0
27 27 self._frontend = frontend
28 28 self.highlighting_on = False
29 29
30 30 def highlightBlock(self, qstring):
31 31 """ Highlight a block of text. Reimplemented to highlight selectively.
32 32 """
33 33 if self.highlighting_on:
34 34 for prompt in (self._frontend._prompt,
35 35 self._frontend.continuation_prompt):
36 36 if qstring.startsWith(prompt):
37 37 qstring.remove(0, len(prompt))
38 38 self._current_offset = len(prompt)
39 39 break
40 40 PygmentsHighlighter.highlightBlock(self, qstring)
41 41
42 42 def setFormat(self, start, count, format):
43 43 """ Reimplemented to avoid highlighting continuation prompts.
44 44 """
45 45 start += self._current_offset
46 46 PygmentsHighlighter.setFormat(self, start, count, format)
47 47
48 48
49 49 class FrontendWidget(HistoryConsoleWidget):
50 50 """ A Qt frontend for an IPython kernel.
51 51 """
52 52
53 53 # Emitted when an 'execute_reply' is received from the kernel.
54 54 executed = QtCore.pyqtSignal(object)
55 55
56 56 #---------------------------------------------------------------------------
57 57 # 'QWidget' interface
58 58 #---------------------------------------------------------------------------
59 59
60 60 def __init__(self, kernel_manager, parent=None):
61 61 super(FrontendWidget, self).__init__(parent)
62 62
63 63 self._call_tip_widget = CallTipWidget(self)
64 64 self._compile = CommandCompiler()
65 65 self._completion_lexer = CompletionLexer(PythonLexer())
66 66 self._highlighter = FrontendHighlighter(self)
67 67 self._kernel_manager = None
68 68
69 self.document().contentsChange.connect(self._document_contents_change)
70
71 69 self.continuation_prompt = '... '
72 70 self.kernel_manager = kernel_manager
73 71
72 self.document().contentsChange.connect(self._document_contents_change)
73
74 74 def focusOutEvent(self, event):
75 75 """ Reimplemented to hide calltips.
76 76 """
77 77 self._call_tip_widget.hide()
78 78 return super(FrontendWidget, self).focusOutEvent(event)
79 79
80 80 def keyPressEvent(self, event):
81 81 """ Reimplemented to hide calltips.
82 82 """
83 83 if event.key() == QtCore.Qt.Key_Escape:
84 84 self._call_tip_widget.hide()
85 85 return super(FrontendWidget, self).keyPressEvent(event)
86 86
87 87 #---------------------------------------------------------------------------
88 88 # 'ConsoleWidget' abstract interface
89 89 #---------------------------------------------------------------------------
90 90
91 91 def _execute(self, interactive):
92 92 """ Called to execute the input buffer. When triggered by an the enter
93 93 key press, 'interactive' is True; otherwise, it is False. Returns
94 94 whether the input buffer was completely processed and a new prompt
95 95 created.
96 96 """
97 97 return self.execute_source(self.input_buffer, interactive=interactive)
98 98
99 99 def _prompt_started_hook(self):
100 100 """ Called immediately after a new prompt is displayed.
101 101 """
102 102 self._highlighter.highlighting_on = True
103 103
104 104 def _prompt_finished_hook(self):
105 105 """ Called immediately after a prompt is finished, i.e. when some input
106 106 will be processed and a new prompt displayed.
107 107 """
108 108 self._highlighter.highlighting_on = False
109 109
110 110 def _tab_pressed(self):
111 111 """ Called when the tab key is pressed. Returns whether to continue
112 112 processing the event.
113 113 """
114 114 self._keep_cursor_in_buffer()
115 115 cursor = self.textCursor()
116 116 if not self._complete():
117 117 cursor.insertText(' ')
118 118 return False
119 119
120 120 #---------------------------------------------------------------------------
121 121 # 'FrontendWidget' interface
122 122 #---------------------------------------------------------------------------
123 123
124 124 def execute_source(self, source, hidden=False, interactive=False):
125 125 """ Execute a string containing Python code. If 'hidden', no output is
126 126 shown. Returns whether the source executed (i.e., returns True only
127 127 if no more input is necessary).
128 128 """
129 129 try:
130 130 code = self._compile(source, symbol='single')
131 131 except (OverflowError, SyntaxError, ValueError):
132 132 # Just let IPython deal with the syntax error.
133 133 code = Exception
134 134
135 135 # Only execute interactive multiline input if it ends with a blank line
136 136 lines = source.splitlines()
137 137 if interactive and len(lines) > 1 and lines[-1].strip() != '':
138 138 code = None
139 139
140 140 executed = code is not None
141 141 if executed:
142 142 self.kernel_manager.xreq_channel.execute(source)
143 143 else:
144 144 space = 0
145 145 for char in lines[-1]:
146 146 if char == '\t':
147 147 space += 4
148 148 elif char == ' ':
149 149 space += 1
150 150 else:
151 151 break
152 152 if source.endswith(':') or source.endswith(':\n'):
153 153 space += 4
154 154 self._show_continuation_prompt()
155 155 self.appendPlainText(' ' * space)
156 156
157 157 return executed
158 158
159 159 def execute_file(self, path, hidden=False):
160 160 """ Attempts to execute file with 'path'. If 'hidden', no output is
161 161 shown.
162 162 """
163 163 self.execute_source('run %s' % path, hidden=hidden)
164 164
165 165 def _get_kernel_manager(self):
166 166 """ Returns the current kernel manager.
167 167 """
168 168 return self._kernel_manager
169 169
170 170 def _set_kernel_manager(self, kernel_manager):
171 171 """ Sets a new kernel manager, configuring its channels as necessary.
172 172 """
173 173 # Disconnect the old kernel manager.
174 174 if self._kernel_manager is not None:
175 175 sub = self._kernel_manager.sub_channel
176 176 xreq = self._kernel_manager.xreq_channel
177 177 sub.message_received.disconnect(self._handle_sub)
178 178 xreq.execute_reply.disconnect(self._handle_execute_reply)
179 179 xreq.complete_reply.disconnect(self._handle_complete_reply)
180 180 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
181 181
182 182 # Connect the new kernel manager.
183 183 self._kernel_manager = kernel_manager
184 184 sub = kernel_manager.sub_channel
185 185 xreq = kernel_manager.xreq_channel
186 186 sub.message_received.connect(self._handle_sub)
187 187 xreq.execute_reply.connect(self._handle_execute_reply)
188 #xreq.complete_reply.connect(self._handle_complete_reply)
189 #xreq.object_info_repy.connect(self._handle_object_info_reply)
188 xreq.complete_reply.connect(self._handle_complete_reply)
189 xreq.object_info_reply.connect(self._handle_object_info_reply)
190 190
191 191 self._show_prompt('>>> ')
192 192
193 193 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
194 194
195 195 #---------------------------------------------------------------------------
196 196 # 'FrontendWidget' protected interface
197 197 #---------------------------------------------------------------------------
198 198
199 199 def _call_tip(self):
200 200 """ Shows a call tip, if appropriate, at the current cursor location.
201 201 """
202 202 # Decide if it makes sense to show a call tip
203 203 cursor = self.textCursor()
204 204 cursor.movePosition(QtGui.QTextCursor.Left)
205 205 document = self.document()
206 206 if document.characterAt(cursor.position()).toAscii() != '(':
207 207 return False
208 208 context = self._get_context(cursor)
209 209 if not context:
210 210 return False
211 211
212 212 # Send the metadata request to the kernel
213 text = '.'.join(context)
214 msg = self.session.send(self.request_socket, 'metadata_request',
215 dict(context=text))
216
217 # Give the kernel some time to respond
218 rep = self._recv_reply_now('metadata_reply')
219 doc = rep.content.docstring if rep else ''
220
221 # Show the call tip
222 if doc:
223 self._call_tip_widget.show_tip(doc)
213 name = '.'.join(context)
214 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
215 self._calltip_pos = self.textCursor().position()
224 216 return True
225 217
226 218 def _complete(self):
227 219 """ Performs completion at the current cursor location.
228 220 """
229 221 # Decide if it makes sense to do completion
230 222 context = self._get_context()
231 223 if not context:
232 224 return False
233 225
234 226 # Send the completion request to the kernel
235 227 text = '.'.join(context)
236 line = self.input_buffer_cursor_line
237 msg = self.session.send(self.request_socket, 'complete_request',
238 dict(text=text, line=line))
239
240 # Give the kernel some time to respond
241 rep = self._recv_reply_now('complete_reply')
242 matches = rep.content.matches if rep else []
243
244 # Show the completion at the correct location
245 cursor = self.textCursor()
246 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
247 self._complete_with_items(cursor, matches)
228 self._complete_id = self.kernel_manager.xreq_channel.complete(
229 text, self.input_buffer_cursor_line, self.input_buffer)
230 self._complete_pos = self.textCursor().position()
248 231 return True
249 232
250 233 def _get_context(self, cursor=None):
251 234 """ Gets the context at the current cursor location.
252 235 """
253 236 if cursor is None:
254 237 cursor = self.textCursor()
255 238 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
256 239 QtGui.QTextCursor.KeepAnchor)
257 240 text = unicode(cursor.selectedText())
258 241 return self._completion_lexer.get_context(text)
259 242
260 243 #------ Signal handlers ----------------------------------------------------
261 244
262 245 def _document_contents_change(self, position, removed, added):
263 246 """ Called whenever the document's content changes. Display a calltip
264 247 if appropriate.
265 248 """
266 249 # Calculate where the cursor should be *after* the change:
267 250 position += added
268 251
269 252 document = self.document()
270 253 if position == self.textCursor().position():
271 254 self._call_tip()
272 255
273 256 def _handle_sub(self, omsg):
274 257 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
275 258 if handler is not None:
276 259 handler(omsg)
277 260
278 261 def _handle_pyout(self, omsg):
279 262 session = omsg['parent_header']['session']
280 263 if session == self.kernel_manager.session.session:
281 264 self.appendPlainText(omsg['content']['data'] + '\n')
282 265
283 266 def _handle_stream(self, omsg):
284 267 self.appendPlainText(omsg['content']['data'])
285 268
286 269 def _handle_execute_reply(self, rep):
287 270 content = rep['content']
288 271 status = content['status']
289 272 if status == 'error':
290 273 self.appendPlainText(content['traceback'][-1])
291 274 elif status == 'aborted':
292 275 text = "ERROR: ABORTED\n"
293 276 self.appendPlainText(text)
294 277 self._show_prompt('>>> ')
295 278 self.executed.emit(rep)
296 279
297 #------ Communication methods ----------------------------------------------
298
299 def _recv_reply(self):
300 return self.session.recv(self.request_socket)
280 def _handle_complete_reply(self, rep):
281 cursor = self.textCursor()
282 if rep['parent_header']['msg_id'] == self._complete_id and \
283 cursor.position() == self._complete_pos:
284 text = '.'.join(self._get_context())
285 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
286 self._complete_with_items(cursor, rep['content']['matches'])
301 287
302 def _recv_reply_now(self, msg_type):
303 for i in xrange(5):
304 rep = self._recv_reply()
305 if rep is not None and rep.msg_type == msg_type:
306 return rep
307 time.sleep(0.1)
308 return None
288 def _handle_object_info_reply(self, rep):
289 cursor = self.textCursor()
290 if rep['parent_header']['msg_id'] == self._calltip_id and \
291 cursor.position() == self._calltip_pos:
292 doc = rep['content']['docstring']
293 if doc:
294 self._call_tip_widget.show_tip(doc)
309 295
310 296
311 297 if __name__ == '__main__':
312 298 import sys
313 299 from IPython.frontend.qt.kernelmanager import QtKernelManager
314 300
315 301 # Create KernelManager
316 302 xreq_addr = ('127.0.0.1', 5575)
317 303 sub_addr = ('127.0.0.1', 5576)
318 304 rep_addr = ('127.0.0.1', 5577)
319 305 kernel_manager = QtKernelManager(xreq_addr, sub_addr, rep_addr)
320 306 kernel_manager.sub_channel.start()
321 307 kernel_manager.xreq_channel.start()
322 308
323 309 # Launch application
324 310 app = QtGui.QApplication(sys.argv)
325 311 widget = FrontendWidget(kernel_manager)
326 312 widget.setWindowTitle('Python')
327 313 widget.resize(640, 480)
328 314 widget.show()
329 315 sys.exit(app.exec_())
330 316
@@ -1,272 +1,311 b''
1 1 #!/usr/bin/env python
2 2 """A simple interactive kernel that talks to a frontend over 0MQ.
3 3
4 4 Things to do:
5 5
6 6 * Finish implementing `raw_input`.
7 7 * Implement `set_parent` logic. Right before doing exec, the Kernel should
8 8 call set_parent on all the PUB objects with the message about to be executed.
9 9 * Implement random port and security key logic.
10 10 * Implement control messages.
11 11 * Implement event loop and poll version.
12 12 """
13 13
14 14 import __builtin__
15 15 import sys
16 16 import time
17 17 import traceback
18 18
19 19 from code import CommandCompiler
20 20
21 21 import zmq
22 22
23 23 from session import Session, Message, extract_header
24 24 from completer import KernelCompleter
25 25
26 26 class OutStream(object):
27 27 """A file like object that publishes the stream to a 0MQ PUB socket."""
28 28
29 29 def __init__(self, session, pub_socket, name, max_buffer=200):
30 30 self.session = session
31 31 self.pub_socket = pub_socket
32 32 self.name = name
33 33 self._buffer = []
34 34 self._buffer_len = 0
35 35 self.max_buffer = max_buffer
36 36 self.parent_header = {}
37 37
38 38 def set_parent(self, parent):
39 39 self.parent_header = extract_header(parent)
40 40
41 41 def close(self):
42 42 self.pub_socket = None
43 43
44 44 def flush(self):
45 45 if self.pub_socket is None:
46 46 raise ValueError(u'I/O operation on closed file')
47 47 else:
48 48 if self._buffer:
49 49 data = ''.join(self._buffer)
50 50 content = {u'name':self.name, u'data':data}
51 51 msg = self.session.msg(u'stream', content=content,
52 52 parent=self.parent_header)
53 53 print>>sys.__stdout__, Message(msg)
54 54 self.pub_socket.send_json(msg)
55 55 self._buffer_len = 0
56 56 self._buffer = []
57 57
58 58 def isattr(self):
59 59 return False
60 60
61 61 def next(self):
62 62 raise IOError('Read not supported on a write only stream.')
63 63
64 64 def read(self, size=None):
65 65 raise IOError('Read not supported on a write only stream.')
66 66
67 67 readline=read
68 68
69 69 def write(self, s):
70 70 if self.pub_socket is None:
71 71 raise ValueError('I/O operation on closed file')
72 72 else:
73 73 self._buffer.append(s)
74 74 self._buffer_len += len(s)
75 75 self._maybe_send()
76 76
77 77 def _maybe_send(self):
78 78 if '\n' in self._buffer[-1]:
79 79 self.flush()
80 80 if self._buffer_len > self.max_buffer:
81 81 self.flush()
82 82
83 83 def writelines(self, sequence):
84 84 if self.pub_socket is None:
85 85 raise ValueError('I/O operation on closed file')
86 86 else:
87 87 for s in sequence:
88 88 self.write(s)
89 89
90 90
91 91 class DisplayHook(object):
92 92
93 93 def __init__(self, session, pub_socket):
94 94 self.session = session
95 95 self.pub_socket = pub_socket
96 96 self.parent_header = {}
97 97
98 98 def __call__(self, obj):
99 99 if obj is None:
100 100 return
101 101
102 102 __builtin__._ = obj
103 103 msg = self.session.msg(u'pyout', {u'data':repr(obj)},
104 104 parent=self.parent_header)
105 105 self.pub_socket.send_json(msg)
106 106
107 107 def set_parent(self, parent):
108 108 self.parent_header = extract_header(parent)
109 109
110 110
111 111 class RawInput(object):
112 112
113 113 def __init__(self, session, socket):
114 114 self.session = session
115 115 self.socket = socket
116 116
117 117 def __call__(self, prompt=None):
118 118 msg = self.session.msg(u'raw_input')
119 119 self.socket.send_json(msg)
120 120 while True:
121 121 try:
122 122 reply = self.socket.recv_json(zmq.NOBLOCK)
123 123 except zmq.ZMQError, e:
124 124 if e.errno == zmq.EAGAIN:
125 125 pass
126 126 else:
127 127 raise
128 128 else:
129 129 break
130 130 return reply[u'content'][u'data']
131 131
132 132
133 133 class Kernel(object):
134 134
135 135 def __init__(self, session, reply_socket, pub_socket):
136 136 self.session = session
137 137 self.reply_socket = reply_socket
138 138 self.pub_socket = pub_socket
139 139 self.user_ns = {}
140 140 self.history = []
141 141 self.compiler = CommandCompiler()
142 142 self.completer = KernelCompleter(self.user_ns)
143 143
144 144 # Build dict of handlers for message types
145 msg_types = [ 'execute_request', 'complete_request',
146 'object_info_request' ]
145 147 self.handlers = {}
146 for msg_type in ['execute_request', 'complete_request']:
148 for msg_type in msg_types:
147 149 self.handlers[msg_type] = getattr(self, msg_type)
148 150
149 151 def abort_queue(self):
150 152 while True:
151 153 try:
152 154 ident = self.reply_socket.recv(zmq.NOBLOCK)
153 155 except zmq.ZMQError, e:
154 156 if e.errno == zmq.EAGAIN:
155 157 break
156 158 else:
157 159 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
158 160 msg = self.reply_socket.recv_json()
159 161 print>>sys.__stdout__, "Aborting:"
160 162 print>>sys.__stdout__, Message(msg)
161 163 msg_type = msg['msg_type']
162 164 reply_type = msg_type.split('_')[0] + '_reply'
163 165 reply_msg = self.session.msg(reply_type, {'status' : 'aborted'}, msg)
164 166 print>>sys.__stdout__, Message(reply_msg)
165 167 self.reply_socket.send(ident,zmq.SNDMORE)
166 168 self.reply_socket.send_json(reply_msg)
167 169 # We need to wait a bit for requests to come in. This can probably
168 170 # be set shorter for true asynchronous clients.
169 171 time.sleep(0.1)
170 172
171 173 def execute_request(self, ident, parent):
172 174 try:
173 175 code = parent[u'content'][u'code']
174 176 except:
175 177 print>>sys.__stderr__, "Got bad msg: "
176 178 print>>sys.__stderr__, Message(parent)
177 179 return
178 180 pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
179 181 self.pub_socket.send_json(pyin_msg)
180 182 try:
181 183 comp_code = self.compiler(code, '<zmq-kernel>')
182 184 sys.displayhook.set_parent(parent)
183 185 exec comp_code in self.user_ns, self.user_ns
184 186 except:
185 187 result = u'error'
186 188 etype, evalue, tb = sys.exc_info()
187 189 tb = traceback.format_exception(etype, evalue, tb)
188 190 exc_content = {
189 191 u'status' : u'error',
190 192 u'traceback' : tb,
191 193 u'etype' : unicode(etype),
192 194 u'evalue' : unicode(evalue)
193 195 }
194 196 exc_msg = self.session.msg(u'pyerr', exc_content, parent)
195 197 self.pub_socket.send_json(exc_msg)
196 198 reply_content = exc_content
197 199 else:
198 200 reply_content = {'status' : 'ok'}
199 201 reply_msg = self.session.msg(u'execute_reply', reply_content, parent)
200 202 print>>sys.__stdout__, Message(reply_msg)
201 203 self.reply_socket.send(ident, zmq.SNDMORE)
202 204 self.reply_socket.send_json(reply_msg)
203 205 if reply_msg['content']['status'] == u'error':
204 206 self.abort_queue()
205 207
206 208 def complete_request(self, ident, parent):
207 209 matches = {'matches' : self.complete(parent),
208 210 'status' : 'ok'}
209 211 completion_msg = self.session.send(self.reply_socket, 'complete_reply',
210 212 matches, parent, ident)
211 213 print >> sys.__stdout__, completion_msg
212 214
213 215 def complete(self, msg):
214 216 return self.completer.complete(msg.content.line, msg.content.text)
215 217
218 def object_info_request(self, ident, parent):
219 context = parent['content']['oname'].split('.')
220 object_info = self.object_info(context)
221 msg = self.session.send(self.reply_socket, 'object_info_reply',
222 object_info, parent, ident)
223 print >> sys.__stdout__, msg
224
225 def object_info(self, context):
226 symbol, leftover = self.symbol_from_context(context)
227 if symbol is not None and not leftover:
228 doc = getattr(symbol, '__doc__', '')
229 else:
230 doc = ''
231 object_info = dict(docstring = doc)
232 return object_info
233
234 def symbol_from_context(self, context):
235 if not context:
236 return None, context
237
238 base_symbol_string = context[0]
239 symbol = self.user_ns.get(base_symbol_string, None)
240 if symbol is None:
241 symbol = __builtin__.__dict__.get(base_symbol_string, None)
242 if symbol is None:
243 return None, context
244
245 context = context[1:]
246 for i, name in enumerate(context):
247 new_symbol = getattr(symbol, name, None)
248 if new_symbol is None:
249 return symbol, context[i:]
250 else:
251 symbol = new_symbol
252
253 return symbol, []
254
216 255 def start(self):
217 256 while True:
218 257 ident = self.reply_socket.recv()
219 258 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
220 259 msg = self.reply_socket.recv_json()
221 260 omsg = Message(msg)
222 261 print>>sys.__stdout__
223 262 print>>sys.__stdout__, omsg
224 263 handler = self.handlers.get(omsg.msg_type, None)
225 264 if handler is None:
226 265 print >> sys.__stderr__, "UNKNOWN MESSAGE TYPE:", omsg
227 266 else:
228 267 handler(ident, omsg)
229 268
230 269
231 270 def main():
232 271 c = zmq.Context()
233 272
234 273 ip = '127.0.0.1'
235 274 port_base = 5575
236 275 connection = ('tcp://%s' % ip) + ':%i'
237 276 rep_conn = connection % port_base
238 277 pub_conn = connection % (port_base+1)
239 278
240 279 print >>sys.__stdout__, "Starting the kernel..."
241 280 print >>sys.__stdout__, "XREP Channel:", rep_conn
242 281 print >>sys.__stdout__, "PUB Channel:", pub_conn
243 282
244 283 session = Session(username=u'kernel')
245 284
246 285 reply_socket = c.socket(zmq.XREP)
247 286 reply_socket.bind(rep_conn)
248 287
249 288 pub_socket = c.socket(zmq.PUB)
250 289 pub_socket.bind(pub_conn)
251 290
252 291 stdout = OutStream(session, pub_socket, u'stdout')
253 292 stderr = OutStream(session, pub_socket, u'stderr')
254 293 sys.stdout = stdout
255 294 sys.stderr = stderr
256 295
257 296 display_hook = DisplayHook(session, pub_socket)
258 297 sys.displayhook = display_hook
259 298
260 299 kernel = Kernel(session, reply_socket, pub_socket)
261 300
262 301 # For debugging convenience, put sleep and a string in the namespace, so we
263 302 # have them every time we start.
264 303 kernel.user_ns['sleep'] = time.sleep
265 304 kernel.user_ns['s'] = 'Test string'
266 305
267 306 print >>sys.__stdout__, "Use Ctrl-\\ (NOT Ctrl-C!) to terminate."
268 307 kernel.start()
269 308
270 309
271 310 if __name__ == '__main__':
272 311 main()
General Comments 0
You need to be logged in to leave comments. Login now