##// END OF EJS Templates
* Updated prompt request code to support the new silent execution mechanism....
epatters -
Show More
@@ -1,466 +1,479 b''
1 # Standard library imports
1 # Standard library imports
2 from collections import namedtuple
2 import signal
3 import signal
3 import sys
4 import sys
4
5
5 # System library imports
6 # System library imports
6 from pygments.lexers import PythonLexer
7 from pygments.lexers import PythonLexer
7 from PyQt4 import QtCore, QtGui
8 from PyQt4 import QtCore, QtGui
8
9
9 # Local imports
10 # Local imports
10 from IPython.core.inputsplitter import InputSplitter
11 from IPython.core.inputsplitter import InputSplitter
11 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
12 from IPython.utils.traitlets import Bool
13 from IPython.utils.traitlets import Bool
13 from bracket_matcher import BracketMatcher
14 from bracket_matcher import BracketMatcher
14 from call_tip_widget import CallTipWidget
15 from call_tip_widget import CallTipWidget
15 from completion_lexer import CompletionLexer
16 from completion_lexer import CompletionLexer
16 from console_widget import HistoryConsoleWidget
17 from console_widget import HistoryConsoleWidget
17 from pygments_highlighter import PygmentsHighlighter
18 from pygments_highlighter import PygmentsHighlighter
18
19
19
20
20 class FrontendHighlighter(PygmentsHighlighter):
21 class FrontendHighlighter(PygmentsHighlighter):
21 """ A PygmentsHighlighter that can be turned on and off and that ignores
22 """ A PygmentsHighlighter that can be turned on and off and that ignores
22 prompts.
23 prompts.
23 """
24 """
24
25
25 def __init__(self, frontend):
26 def __init__(self, frontend):
26 super(FrontendHighlighter, self).__init__(frontend._control.document())
27 super(FrontendHighlighter, self).__init__(frontend._control.document())
27 self._current_offset = 0
28 self._current_offset = 0
28 self._frontend = frontend
29 self._frontend = frontend
29 self.highlighting_on = False
30 self.highlighting_on = False
30
31
31 def highlightBlock(self, qstring):
32 def highlightBlock(self, qstring):
32 """ Highlight a block of text. Reimplemented to highlight selectively.
33 """ Highlight a block of text. Reimplemented to highlight selectively.
33 """
34 """
34 if not self.highlighting_on:
35 if not self.highlighting_on:
35 return
36 return
36
37
37 # The input to this function is unicode string that may contain
38 # The input to this function is unicode string that may contain
38 # paragraph break characters, non-breaking spaces, etc. Here we acquire
39 # paragraph break characters, non-breaking spaces, etc. Here we acquire
39 # the string as plain text so we can compare it.
40 # the string as plain text so we can compare it.
40 current_block = self.currentBlock()
41 current_block = self.currentBlock()
41 string = self._frontend._get_block_plain_text(current_block)
42 string = self._frontend._get_block_plain_text(current_block)
42
43
43 # Decide whether to check for the regular or continuation prompt.
44 # Decide whether to check for the regular or continuation prompt.
44 if current_block.contains(self._frontend._prompt_pos):
45 if current_block.contains(self._frontend._prompt_pos):
45 prompt = self._frontend._prompt
46 prompt = self._frontend._prompt
46 else:
47 else:
47 prompt = self._frontend._continuation_prompt
48 prompt = self._frontend._continuation_prompt
48
49
49 # Don't highlight the part of the string that contains the prompt.
50 # Don't highlight the part of the string that contains the prompt.
50 if string.startswith(prompt):
51 if string.startswith(prompt):
51 self._current_offset = len(prompt)
52 self._current_offset = len(prompt)
52 qstring.remove(0, len(prompt))
53 qstring.remove(0, len(prompt))
53 else:
54 else:
54 self._current_offset = 0
55 self._current_offset = 0
55
56
56 PygmentsHighlighter.highlightBlock(self, qstring)
57 PygmentsHighlighter.highlightBlock(self, qstring)
57
58
58 def rehighlightBlock(self, block):
59 def rehighlightBlock(self, block):
59 """ Reimplemented to temporarily enable highlighting if disabled.
60 """ Reimplemented to temporarily enable highlighting if disabled.
60 """
61 """
61 old = self.highlighting_on
62 old = self.highlighting_on
62 self.highlighting_on = True
63 self.highlighting_on = True
63 super(FrontendHighlighter, self).rehighlightBlock(block)
64 super(FrontendHighlighter, self).rehighlightBlock(block)
64 self.highlighting_on = old
65 self.highlighting_on = old
65
66
66 def setFormat(self, start, count, format):
67 def setFormat(self, start, count, format):
67 """ Reimplemented to highlight selectively.
68 """ Reimplemented to highlight selectively.
68 """
69 """
69 start += self._current_offset
70 start += self._current_offset
70 PygmentsHighlighter.setFormat(self, start, count, format)
71 PygmentsHighlighter.setFormat(self, start, count, format)
71
72
72
73
73 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
74 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
74 """ A Qt frontend for a generic Python kernel.
75 """ A Qt frontend for a generic Python kernel.
75 """
76 """
76
77
77 # An option and corresponding signal for overriding the default kernel
78 # An option and corresponding signal for overriding the default kernel
78 # interrupt behavior.
79 # interrupt behavior.
79 custom_interrupt = Bool(False)
80 custom_interrupt = Bool(False)
80 custom_interrupt_requested = QtCore.pyqtSignal()
81 custom_interrupt_requested = QtCore.pyqtSignal()
81
82
82 # An option and corresponding signals for overriding the default kernel
83 # An option and corresponding signals for overriding the default kernel
83 # restart behavior.
84 # restart behavior.
84 custom_restart = Bool(False)
85 custom_restart = Bool(False)
85 custom_restart_kernel_died = QtCore.pyqtSignal(float)
86 custom_restart_kernel_died = QtCore.pyqtSignal(float)
86 custom_restart_requested = QtCore.pyqtSignal()
87 custom_restart_requested = QtCore.pyqtSignal()
87
88
88 # Emitted when an 'execute_reply' has been received from the kernel and
89 # Emitted when an 'execute_reply' has been received from the kernel and
89 # processed by the FrontendWidget.
90 # processed by the FrontendWidget.
90 executed = QtCore.pyqtSignal(object)
91 executed = QtCore.pyqtSignal(object)
91
92
92 # Protected class variables.
93 # Protected class variables.
94 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
95 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
96 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
93 _input_splitter_class = InputSplitter
97 _input_splitter_class = InputSplitter
94
98
95 #---------------------------------------------------------------------------
99 #---------------------------------------------------------------------------
96 # 'object' interface
100 # 'object' interface
97 #---------------------------------------------------------------------------
101 #---------------------------------------------------------------------------
98
102
99 def __init__(self, *args, **kw):
103 def __init__(self, *args, **kw):
100 super(FrontendWidget, self).__init__(*args, **kw)
104 super(FrontendWidget, self).__init__(*args, **kw)
101
105
102 # FrontendWidget protected variables.
106 # FrontendWidget protected variables.
103 self._bracket_matcher = BracketMatcher(self._control)
107 self._bracket_matcher = BracketMatcher(self._control)
104 self._call_tip_widget = CallTipWidget(self._control)
108 self._call_tip_widget = CallTipWidget(self._control)
105 self._completion_lexer = CompletionLexer(PythonLexer())
109 self._completion_lexer = CompletionLexer(PythonLexer())
106 self._hidden = False
110 self._hidden = False
107 self._highlighter = FrontendHighlighter(self)
111 self._highlighter = FrontendHighlighter(self)
108 self._input_splitter = self._input_splitter_class(input_mode='block')
112 self._input_splitter = self._input_splitter_class(input_mode='block')
109 self._kernel_manager = None
113 self._kernel_manager = None
110 self._possible_kernel_restart = False
114 self._possible_kernel_restart = False
115 self._request_info = {}
111
116
112 # Configure the ConsoleWidget.
117 # Configure the ConsoleWidget.
113 self.tab_width = 4
118 self.tab_width = 4
114 self._set_continuation_prompt('... ')
119 self._set_continuation_prompt('... ')
115
120
116 # Connect signal handlers.
121 # Connect signal handlers.
117 document = self._control.document()
122 document = self._control.document()
118 document.contentsChange.connect(self._document_contents_change)
123 document.contentsChange.connect(self._document_contents_change)
119
124
120 #---------------------------------------------------------------------------
125 #---------------------------------------------------------------------------
121 # 'ConsoleWidget' abstract interface
126 # 'ConsoleWidget' abstract interface
122 #---------------------------------------------------------------------------
127 #---------------------------------------------------------------------------
123
128
124 def _is_complete(self, source, interactive):
129 def _is_complete(self, source, interactive):
125 """ Returns whether 'source' can be completely processed and a new
130 """ Returns whether 'source' can be completely processed and a new
126 prompt created. When triggered by an Enter/Return key press,
131 prompt created. When triggered by an Enter/Return key press,
127 'interactive' is True; otherwise, it is False.
132 'interactive' is True; otherwise, it is False.
128 """
133 """
129 complete = self._input_splitter.push(source.expandtabs(4))
134 complete = self._input_splitter.push(source.expandtabs(4))
130 if interactive:
135 if interactive:
131 complete = not self._input_splitter.push_accepts_more()
136 complete = not self._input_splitter.push_accepts_more()
132 return complete
137 return complete
133
138
134 def _execute(self, source, hidden, user_variables=None,
139 def _execute(self, source, hidden, user_variables=None,
135 user_expressions=None):
140 user_expressions=None):
136 """ Execute 'source'. If 'hidden', do not show any output.
141 """ Execute 'source'. If 'hidden', do not show any output.
137
142
138 See parent class :meth:`execute` docstring for full details.
143 See parent class :meth:`execute` docstring for full details.
139 """
144 """
140 # tmp code for testing, disable in real use with 'if 0'. Only delete
145 # tmp code for testing, disable in real use with 'if 0'. Only delete
141 # this code once we have automated tests for these fields.
146 # this code once we have automated tests for these fields.
142 if 0:
147 if 0:
143 user_variables = ['x', 'y', 'z']
148 user_variables = ['x', 'y', 'z']
144 user_expressions = {'sum' : '1+1',
149 user_expressions = {'sum' : '1+1',
145 'bad syntax' : 'klsdafj kasd f',
150 'bad syntax' : 'klsdafj kasd f',
146 'bad call' : 'range("hi")',
151 'bad call' : 'range("hi")',
147 'time' : 'time.time()',
152 'time' : 'time.time()',
148 }
153 }
149 # /end tmp code
154 # /end tmp code
150
155
151 # FIXME - user_variables/expressions are not visible in API above us.
156 # FIXME - user_variables/expressions are not visible in API above us.
152 self.kernel_manager.xreq_channel.execute(source, hidden,
157 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden,
153 user_variables,
158 user_variables,
154 user_expressions)
159 user_expressions)
160 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
155 self._hidden = hidden
161 self._hidden = hidden
156
162
157 def _prompt_started_hook(self):
163 def _prompt_started_hook(self):
158 """ Called immediately after a new prompt is displayed.
164 """ Called immediately after a new prompt is displayed.
159 """
165 """
160 if not self._reading:
166 if not self._reading:
161 self._highlighter.highlighting_on = True
167 self._highlighter.highlighting_on = True
162
168
163 def _prompt_finished_hook(self):
169 def _prompt_finished_hook(self):
164 """ Called immediately after a prompt is finished, i.e. when some input
170 """ Called immediately after a prompt is finished, i.e. when some input
165 will be processed and a new prompt displayed.
171 will be processed and a new prompt displayed.
166 """
172 """
167 if not self._reading:
173 if not self._reading:
168 self._highlighter.highlighting_on = False
174 self._highlighter.highlighting_on = False
169
175
170 def _tab_pressed(self):
176 def _tab_pressed(self):
171 """ Called when the tab key is pressed. Returns whether to continue
177 """ Called when the tab key is pressed. Returns whether to continue
172 processing the event.
178 processing the event.
173 """
179 """
174 # Perform tab completion if:
180 # Perform tab completion if:
175 # 1) The cursor is in the input buffer.
181 # 1) The cursor is in the input buffer.
176 # 2) There is a non-whitespace character before the cursor.
182 # 2) There is a non-whitespace character before the cursor.
177 text = self._get_input_buffer_cursor_line()
183 text = self._get_input_buffer_cursor_line()
178 if text is None:
184 if text is None:
179 return False
185 return False
180 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
186 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
181 if complete:
187 if complete:
182 self._complete()
188 self._complete()
183 return not complete
189 return not complete
184
190
185 #---------------------------------------------------------------------------
191 #---------------------------------------------------------------------------
186 # 'ConsoleWidget' protected interface
192 # 'ConsoleWidget' protected interface
187 #---------------------------------------------------------------------------
193 #---------------------------------------------------------------------------
188
194
189 def _event_filter_console_keypress(self, event):
195 def _event_filter_console_keypress(self, event):
190 """ Reimplemented to allow execution interruption.
196 """ Reimplemented to allow execution interruption.
191 """
197 """
192 key = event.key()
198 key = event.key()
193 if self._control_key_down(event.modifiers()):
199 if self._control_key_down(event.modifiers()):
194 if key == QtCore.Qt.Key_C and self._executing:
200 if key == QtCore.Qt.Key_C and self._executing:
195 self.interrupt_kernel()
201 self.interrupt_kernel()
196 return True
202 return True
197 elif key == QtCore.Qt.Key_Period:
203 elif key == QtCore.Qt.Key_Period:
198 message = 'Are you sure you want to restart the kernel?'
204 message = 'Are you sure you want to restart the kernel?'
199 self.restart_kernel(message)
205 self.restart_kernel(message)
200 return True
206 return True
201 return super(FrontendWidget, self)._event_filter_console_keypress(event)
207 return super(FrontendWidget, self)._event_filter_console_keypress(event)
202
208
203 def _insert_continuation_prompt(self, cursor):
209 def _insert_continuation_prompt(self, cursor):
204 """ Reimplemented for auto-indentation.
210 """ Reimplemented for auto-indentation.
205 """
211 """
206 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
212 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
207 spaces = self._input_splitter.indent_spaces
213 spaces = self._input_splitter.indent_spaces
208 cursor.insertText('\t' * (spaces / self.tab_width))
214 cursor.insertText('\t' * (spaces / self.tab_width))
209 cursor.insertText(' ' * (spaces % self.tab_width))
215 cursor.insertText(' ' * (spaces % self.tab_width))
210
216
211 #---------------------------------------------------------------------------
217 #---------------------------------------------------------------------------
212 # 'BaseFrontendMixin' abstract interface
218 # 'BaseFrontendMixin' abstract interface
213 #---------------------------------------------------------------------------
219 #---------------------------------------------------------------------------
214
220
215 def _handle_complete_reply(self, rep):
221 def _handle_complete_reply(self, rep):
216 """ Handle replies for tab completion.
222 """ Handle replies for tab completion.
217 """
223 """
218 cursor = self._get_cursor()
224 cursor = self._get_cursor()
219 if rep['parent_header']['msg_id'] == self._complete_id and \
225 info = self._request_info.get('complete')
220 cursor.position() == self._complete_pos:
226 if info and info.id == rep['parent_header']['msg_id'] and \
227 info.pos == cursor.position():
221 text = '.'.join(self._get_context())
228 text = '.'.join(self._get_context())
222 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
229 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
223 self._complete_with_items(cursor, rep['content']['matches'])
230 self._complete_with_items(cursor, rep['content']['matches'])
224
231
225 def _handle_execute_reply(self, msg):
232 def _handle_execute_reply(self, msg):
226 """ Handles replies for code execution.
233 """ Handles replies for code execution.
227 """
234 """
228 if not self._hidden:
235 info = self._request_info.get('execute')
236 if info and info.id == msg['parent_header']['msg_id'] and \
237 not self._hidden:
229 # Make sure that all output from the SUB channel has been processed
238 # Make sure that all output from the SUB channel has been processed
230 # before writing a new prompt.
239 # before writing a new prompt.
231 self.kernel_manager.sub_channel.flush()
240 self.kernel_manager.sub_channel.flush()
232
241
233 content = msg['content']
242 content = msg['content']
234 status = content['status']
243 status = content['status']
235 if status == 'ok':
244 if status == 'ok':
236 self._process_execute_ok(msg)
245 self._process_execute_ok(msg)
237 elif status == 'error':
246 elif status == 'error':
238 self._process_execute_error(msg)
247 self._process_execute_error(msg)
239 elif status == 'abort':
248 elif status == 'abort':
240 self._process_execute_abort(msg)
249 self._process_execute_abort(msg)
241
250
242 self._show_interpreter_prompt_for_reply(msg)
251 self._show_interpreter_prompt_for_reply(msg)
243 self.executed.emit(msg)
252 self.executed.emit(msg)
244
253
245 def _handle_input_request(self, msg):
254 def _handle_input_request(self, msg):
246 """ Handle requests for raw_input.
255 """ Handle requests for raw_input.
247 """
256 """
248 if self._hidden:
257 if self._hidden:
249 raise RuntimeError('Request for raw input during hidden execution.')
258 raise RuntimeError('Request for raw input during hidden execution.')
250
259
251 # Make sure that all output from the SUB channel has been processed
260 # Make sure that all output from the SUB channel has been processed
252 # before entering readline mode.
261 # before entering readline mode.
253 self.kernel_manager.sub_channel.flush()
262 self.kernel_manager.sub_channel.flush()
254
263
255 def callback(line):
264 def callback(line):
256 self.kernel_manager.rep_channel.input(line)
265 self.kernel_manager.rep_channel.input(line)
257 self._readline(msg['content']['prompt'], callback=callback)
266 self._readline(msg['content']['prompt'], callback=callback)
258
267
259 def _handle_kernel_died(self, since_last_heartbeat):
268 def _handle_kernel_died(self, since_last_heartbeat):
260 """ Handle the kernel's death by asking if the user wants to restart.
269 """ Handle the kernel's death by asking if the user wants to restart.
261 """
270 """
262 message = 'The kernel heartbeat has been inactive for %.2f ' \
271 message = 'The kernel heartbeat has been inactive for %.2f ' \
263 'seconds. Do you want to restart the kernel? You may ' \
272 'seconds. Do you want to restart the kernel? You may ' \
264 'first want to check the network connection.' % \
273 'first want to check the network connection.' % \
265 since_last_heartbeat
274 since_last_heartbeat
266 if self.custom_restart:
275 if self.custom_restart:
267 self.custom_restart_kernel_died.emit(since_last_heartbeat)
276 self.custom_restart_kernel_died.emit(since_last_heartbeat)
268 else:
277 else:
269 self.restart_kernel(message)
278 self.restart_kernel(message)
270
279
271 def _handle_object_info_reply(self, rep):
280 def _handle_object_info_reply(self, rep):
272 """ Handle replies for call tips.
281 """ Handle replies for call tips.
273 """
282 """
274 cursor = self._get_cursor()
283 cursor = self._get_cursor()
275 if rep['parent_header']['msg_id'] == self._call_tip_id and \
284 info = self._request_info.get('call_tip')
276 cursor.position() == self._call_tip_pos:
285 if info and info.id == rep['parent_header']['msg_id'] and \
286 info.pos == cursor.position():
277 doc = rep['content']['docstring']
287 doc = rep['content']['docstring']
278 if doc:
288 if doc:
279 self._call_tip_widget.show_docstring(doc)
289 self._call_tip_widget.show_docstring(doc)
280
290
281 def _handle_pyout(self, msg):
291 def _handle_pyout(self, msg):
282 """ Handle display hook output.
292 """ Handle display hook output.
283 """
293 """
284 if not self._hidden and self._is_from_this_session(msg):
294 if not self._hidden and self._is_from_this_session(msg):
285 self._append_plain_text(msg['content']['data'] + '\n')
295 self._append_plain_text(msg['content']['data'] + '\n')
286
296
287 def _handle_stream(self, msg):
297 def _handle_stream(self, msg):
288 """ Handle stdout, stderr, and stdin.
298 """ Handle stdout, stderr, and stdin.
289 """
299 """
290 if not self._hidden and self._is_from_this_session(msg):
300 if not self._hidden and self._is_from_this_session(msg):
291 self._append_plain_text(msg['content']['data'])
301 self._append_plain_text(msg['content']['data'])
292 self._control.moveCursor(QtGui.QTextCursor.End)
302 self._control.moveCursor(QtGui.QTextCursor.End)
293
303
294 def _started_channels(self):
304 def _started_channels(self):
295 """ Called when the KernelManager channels have started listening or
305 """ Called when the KernelManager channels have started listening or
296 when the frontend is assigned an already listening KernelManager.
306 when the frontend is assigned an already listening KernelManager.
297 """
307 """
298 self._control.clear()
308 self._control.clear()
299 self._append_plain_text(self._get_banner())
309 self._append_plain_text(self._get_banner())
300 self._show_interpreter_prompt()
310 self._show_interpreter_prompt()
301
311
302 def _stopped_channels(self):
312 def _stopped_channels(self):
303 """ Called when the KernelManager channels have stopped listening or
313 """ Called when the KernelManager channels have stopped listening or
304 when a listening KernelManager is removed from the frontend.
314 when a listening KernelManager is removed from the frontend.
305 """
315 """
306 self._executing = self._reading = False
316 self._executing = self._reading = False
307 self._highlighter.highlighting_on = False
317 self._highlighter.highlighting_on = False
308
318
309 #---------------------------------------------------------------------------
319 #---------------------------------------------------------------------------
310 # 'FrontendWidget' interface
320 # 'FrontendWidget' interface
311 #---------------------------------------------------------------------------
321 #---------------------------------------------------------------------------
312
322
313 def execute_file(self, path, hidden=False):
323 def execute_file(self, path, hidden=False):
314 """ Attempts to execute file with 'path'. If 'hidden', no output is
324 """ Attempts to execute file with 'path'. If 'hidden', no output is
315 shown.
325 shown.
316 """
326 """
317 self.execute('execfile("%s")' % path, hidden=hidden)
327 self.execute('execfile("%s")' % path, hidden=hidden)
318
328
319 def interrupt_kernel(self):
329 def interrupt_kernel(self):
320 """ Attempts to interrupt the running kernel.
330 """ Attempts to interrupt the running kernel.
321 """
331 """
322 if self.custom_interrupt:
332 if self.custom_interrupt:
323 self.custom_interrupt_requested.emit()
333 self.custom_interrupt_requested.emit()
324 elif self.kernel_manager.has_kernel:
334 elif self.kernel_manager.has_kernel:
325 self.kernel_manager.signal_kernel(signal.SIGINT)
335 self.kernel_manager.signal_kernel(signal.SIGINT)
326 else:
336 else:
327 self._append_plain_text('Kernel process is either remote or '
337 self._append_plain_text('Kernel process is either remote or '
328 'unspecified. Cannot interrupt.\n')
338 'unspecified. Cannot interrupt.\n')
329
339
330 def restart_kernel(self, message):
340 def restart_kernel(self, message):
331 """ Attempts to restart the running kernel.
341 """ Attempts to restart the running kernel.
332 """
342 """
333 # We want to make sure that if this dialog is already happening, that
343 # We want to make sure that if this dialog is already happening, that
334 # other signals don't trigger it again. This can happen when the
344 # other signals don't trigger it again. This can happen when the
335 # kernel_died heartbeat signal is emitted and the user is slow to
345 # kernel_died heartbeat signal is emitted and the user is slow to
336 # respond to the dialog.
346 # respond to the dialog.
337 if not self._possible_kernel_restart:
347 if not self._possible_kernel_restart:
338 if self.custom_restart:
348 if self.custom_restart:
339 self.custom_restart_requested.emit()
349 self.custom_restart_requested.emit()
340 elif self.kernel_manager.has_kernel:
350 elif self.kernel_manager.has_kernel:
341 # Setting this to True will prevent this logic from happening
351 # Setting this to True will prevent this logic from happening
342 # again until the current pass is completed.
352 # again until the current pass is completed.
343 self._possible_kernel_restart = True
353 self._possible_kernel_restart = True
344 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
354 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
345 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
355 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
346 message, buttons)
356 message, buttons)
347 if result == QtGui.QMessageBox.Yes:
357 if result == QtGui.QMessageBox.Yes:
348 try:
358 try:
349 self.kernel_manager.restart_kernel()
359 self.kernel_manager.restart_kernel()
350 except RuntimeError:
360 except RuntimeError:
351 message = 'Kernel started externally. Cannot restart.\n'
361 message = 'Kernel started externally. Cannot restart.\n'
352 self._append_plain_text(message)
362 self._append_plain_text(message)
353 else:
363 else:
354 self._stopped_channels()
364 self._stopped_channels()
355 self._append_plain_text('Kernel restarting...\n')
365 self._append_plain_text('Kernel restarting...\n')
356 self._show_interpreter_prompt()
366 self._show_interpreter_prompt()
357 # This might need to be moved to another location?
367 # This might need to be moved to another location?
358 self._possible_kernel_restart = False
368 self._possible_kernel_restart = False
359 else:
369 else:
360 self._append_plain_text('Kernel process is either remote or '
370 self._append_plain_text('Kernel process is either remote or '
361 'unspecified. Cannot restart.\n')
371 'unspecified. Cannot restart.\n')
362
372
363 #---------------------------------------------------------------------------
373 #---------------------------------------------------------------------------
364 # 'FrontendWidget' protected interface
374 # 'FrontendWidget' protected interface
365 #---------------------------------------------------------------------------
375 #---------------------------------------------------------------------------
366
376
367 def _call_tip(self):
377 def _call_tip(self):
368 """ Shows a call tip, if appropriate, at the current cursor location.
378 """ Shows a call tip, if appropriate, at the current cursor location.
369 """
379 """
370 # Decide if it makes sense to show a call tip
380 # Decide if it makes sense to show a call tip
371 cursor = self._get_cursor()
381 cursor = self._get_cursor()
372 cursor.movePosition(QtGui.QTextCursor.Left)
382 cursor.movePosition(QtGui.QTextCursor.Left)
373 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
383 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
374 return False
384 return False
375 context = self._get_context(cursor)
385 context = self._get_context(cursor)
376 if not context:
386 if not context:
377 return False
387 return False
378
388
379 # Send the metadata request to the kernel
389 # Send the metadata request to the kernel
380 name = '.'.join(context)
390 name = '.'.join(context)
381 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
391 msg_id = self.kernel_manager.xreq_channel.object_info(name)
382 self._call_tip_pos = self._get_cursor().position()
392 pos = self._get_cursor().position()
393 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
383 return True
394 return True
384
395
385 def _complete(self):
396 def _complete(self):
386 """ Performs completion at the current cursor location.
397 """ Performs completion at the current cursor location.
387 """
398 """
388 context = self._get_context()
399 context = self._get_context()
389 if context:
400 if context:
390 # Send the completion request to the kernel
401 # Send the completion request to the kernel
391 self._complete_id = self.kernel_manager.xreq_channel.complete(
402 msg_id = self.kernel_manager.xreq_channel.complete(
392 '.'.join(context), # text
403 '.'.join(context), # text
393 self._get_input_buffer_cursor_line(), # line
404 self._get_input_buffer_cursor_line(), # line
394 self._get_input_buffer_cursor_column(), # cursor_pos
405 self._get_input_buffer_cursor_column(), # cursor_pos
395 self.input_buffer) # block
406 self.input_buffer) # block
396 self._complete_pos = self._get_cursor().position()
407 pos = self._get_cursor().position()
408 info = self._CompletionRequest(msg_id, pos)
409 self._request_info['complete'] = info
397
410
398 def _get_banner(self):
411 def _get_banner(self):
399 """ Gets a banner to display at the beginning of a session.
412 """ Gets a banner to display at the beginning of a session.
400 """
413 """
401 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
414 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
402 '"license" for more information.'
415 '"license" for more information.'
403 return banner % (sys.version, sys.platform)
416 return banner % (sys.version, sys.platform)
404
417
405 def _get_context(self, cursor=None):
418 def _get_context(self, cursor=None):
406 """ Gets the context for the specified cursor (or the current cursor
419 """ Gets the context for the specified cursor (or the current cursor
407 if none is specified).
420 if none is specified).
408 """
421 """
409 if cursor is None:
422 if cursor is None:
410 cursor = self._get_cursor()
423 cursor = self._get_cursor()
411 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
424 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
412 QtGui.QTextCursor.KeepAnchor)
425 QtGui.QTextCursor.KeepAnchor)
413 text = str(cursor.selection().toPlainText())
426 text = str(cursor.selection().toPlainText())
414 return self._completion_lexer.get_context(text)
427 return self._completion_lexer.get_context(text)
415
428
416 def _process_execute_abort(self, msg):
429 def _process_execute_abort(self, msg):
417 """ Process a reply for an aborted execution request.
430 """ Process a reply for an aborted execution request.
418 """
431 """
419 self._append_plain_text("ERROR: execution aborted\n")
432 self._append_plain_text("ERROR: execution aborted\n")
420
433
421 def _process_execute_error(self, msg):
434 def _process_execute_error(self, msg):
422 """ Process a reply for an execution request that resulted in an error.
435 """ Process a reply for an execution request that resulted in an error.
423 """
436 """
424 content = msg['content']
437 content = msg['content']
425 traceback = ''.join(content['traceback'])
438 traceback = ''.join(content['traceback'])
426 self._append_plain_text(traceback)
439 self._append_plain_text(traceback)
427
440
428 def _process_execute_ok(self, msg):
441 def _process_execute_ok(self, msg):
429 """ Process a reply for a successful execution equest.
442 """ Process a reply for a successful execution equest.
430 """
443 """
431 payload = msg['content']['payload']
444 payload = msg['content']['payload']
432 for item in payload:
445 for item in payload:
433 if not self._process_execute_payload(item):
446 if not self._process_execute_payload(item):
434 warning = 'Received unknown payload of type %s\n'
447 warning = 'Received unknown payload of type %s\n'
435 self._append_plain_text(warning % repr(item['source']))
448 self._append_plain_text(warning % repr(item['source']))
436
449
437 def _process_execute_payload(self, item):
450 def _process_execute_payload(self, item):
438 """ Process a single payload item from the list of payload items in an
451 """ Process a single payload item from the list of payload items in an
439 execution reply. Returns whether the payload was handled.
452 execution reply. Returns whether the payload was handled.
440 """
453 """
441 # The basic FrontendWidget doesn't handle payloads, as they are a
454 # The basic FrontendWidget doesn't handle payloads, as they are a
442 # mechanism for going beyond the standard Python interpreter model.
455 # mechanism for going beyond the standard Python interpreter model.
443 return False
456 return False
444
457
445 def _show_interpreter_prompt(self):
458 def _show_interpreter_prompt(self):
446 """ Shows a prompt for the interpreter.
459 """ Shows a prompt for the interpreter.
447 """
460 """
448 self._show_prompt('>>> ')
461 self._show_prompt('>>> ')
449
462
450 def _show_interpreter_prompt_for_reply(self, msg):
463 def _show_interpreter_prompt_for_reply(self, msg):
451 """ Shows a prompt for the interpreter given an 'execute_reply' message.
464 """ Shows a prompt for the interpreter given an 'execute_reply' message.
452 """
465 """
453 self._show_interpreter_prompt()
466 self._show_interpreter_prompt()
454
467
455 #------ Signal handlers ----------------------------------------------------
468 #------ Signal handlers ----------------------------------------------------
456
469
457 def _document_contents_change(self, position, removed, added):
470 def _document_contents_change(self, position, removed, added):
458 """ Called whenever the document's content changes. Display a call tip
471 """ Called whenever the document's content changes. Display a call tip
459 if appropriate.
472 if appropriate.
460 """
473 """
461 # Calculate where the cursor should be *after* the change:
474 # Calculate where the cursor should be *after* the change:
462 position += added
475 position += added
463
476
464 document = self._control.document()
477 document = self._control.document()
465 if position == self._get_cursor().position():
478 if position == self._get_cursor().position():
466 self._call_tip()
479 self._call_tip()
@@ -1,408 +1,414 b''
1 """ A FrontendWidget that emulates the interface of the console IPython and
1 """ A FrontendWidget that emulates the interface of the console IPython and
2 supports the additional functionality provided by the IPython kernel.
2 supports the additional functionality provided by the IPython kernel.
3
3
4 TODO: Add support for retrieving the system default editor. Requires code
4 TODO: Add support for retrieving the system default editor. Requires code
5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
6 Linux (use the xdg system).
6 Linux (use the xdg system).
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Imports
10 # Imports
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 # Standard library imports
13 # Standard library imports
14 from collections import namedtuple
14 from collections import namedtuple
15 import re
15 import re
16 from subprocess import Popen
16 from subprocess import Popen
17
17
18 # System library imports
18 # System library imports
19 from PyQt4 import QtCore, QtGui
19 from PyQt4 import QtCore, QtGui
20
20
21 # Local imports
21 # Local imports
22 from IPython.core.inputsplitter import IPythonInputSplitter
22 from IPython.core.inputsplitter import IPythonInputSplitter
23 from IPython.core.usage import default_banner
23 from IPython.core.usage import default_banner
24 from IPython.utils import io
25 from IPython.utils.traitlets import Bool, Str
24 from IPython.utils.traitlets import Bool, Str
26 from frontend_widget import FrontendWidget
25 from frontend_widget import FrontendWidget
27
26
28 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
29 # Constants
28 # Constants
30 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
31
30
32 # The default light style sheet: black text on a white background.
31 # The default light style sheet: black text on a white background.
33 default_light_style_sheet = '''
32 default_light_style_sheet = '''
34 .error { color: red; }
33 .error { color: red; }
35 .in-prompt { color: navy; }
34 .in-prompt { color: navy; }
36 .in-prompt-number { font-weight: bold; }
35 .in-prompt-number { font-weight: bold; }
37 .out-prompt { color: darkred; }
36 .out-prompt { color: darkred; }
38 .out-prompt-number { font-weight: bold; }
37 .out-prompt-number { font-weight: bold; }
39 '''
38 '''
40 default_light_syntax_style = 'default'
39 default_light_syntax_style = 'default'
41
40
42 # The default dark style sheet: white text on a black background.
41 # The default dark style sheet: white text on a black background.
43 default_dark_style_sheet = '''
42 default_dark_style_sheet = '''
44 QPlainTextEdit, QTextEdit { background-color: black; color: white }
43 QPlainTextEdit, QTextEdit { background-color: black; color: white }
45 QFrame { border: 1px solid grey; }
44 QFrame { border: 1px solid grey; }
46 .error { color: red; }
45 .error { color: red; }
47 .in-prompt { color: lime; }
46 .in-prompt { color: lime; }
48 .in-prompt-number { color: lime; font-weight: bold; }
47 .in-prompt-number { color: lime; font-weight: bold; }
49 .out-prompt { color: red; }
48 .out-prompt { color: red; }
50 .out-prompt-number { color: red; font-weight: bold; }
49 .out-prompt-number { color: red; font-weight: bold; }
51 '''
50 '''
52 default_dark_syntax_style = 'monokai'
51 default_dark_syntax_style = 'monokai'
53
52
54 # Default strings to build and display input and output prompts (and separators
53 # Default strings to build and display input and output prompts (and separators
55 # in between)
54 # in between)
56 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
55 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
57 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
56 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
58 default_input_sep = '\n'
57 default_input_sep = '\n'
59 default_output_sep = ''
58 default_output_sep = ''
60 default_output_sep2 = ''
59 default_output_sep2 = ''
61
60
62 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
63 # IPythonWidget class
62 # IPythonWidget class
64 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
65
64
66 class IPythonWidget(FrontendWidget):
65 class IPythonWidget(FrontendWidget):
67 """ A FrontendWidget for an IPython kernel.
66 """ A FrontendWidget for an IPython kernel.
68 """
67 """
69
68
70 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
69 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
71 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
70 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
72 # settings.
71 # settings.
73 custom_edit = Bool(False)
72 custom_edit = Bool(False)
74 custom_edit_requested = QtCore.pyqtSignal(object, object)
73 custom_edit_requested = QtCore.pyqtSignal(object, object)
75
74
76 # A command for invoking a system text editor. If the string contains a
75 # A command for invoking a system text editor. If the string contains a
77 # {filename} format specifier, it will be used. Otherwise, the filename will
76 # {filename} format specifier, it will be used. Otherwise, the filename will
78 # be appended to the end the command.
77 # be appended to the end the command.
79 editor = Str('default', config=True)
78 editor = Str('default', config=True)
80
79
81 # The editor command to use when a specific line number is requested. The
80 # The editor command to use when a specific line number is requested. The
82 # string should contain two format specifiers: {line} and {filename}. If
81 # string should contain two format specifiers: {line} and {filename}. If
83 # this parameter is not specified, the line number option to the %edit magic
82 # this parameter is not specified, the line number option to the %edit magic
84 # will be ignored.
83 # will be ignored.
85 editor_line = Str(config=True)
84 editor_line = Str(config=True)
86
85
87 # A CSS stylesheet. The stylesheet can contain classes for:
86 # A CSS stylesheet. The stylesheet can contain classes for:
88 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
87 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
89 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
88 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
90 # 3. IPython: .error, .in-prompt, .out-prompt, etc
89 # 3. IPython: .error, .in-prompt, .out-prompt, etc
91 style_sheet = Str(config=True)
90 style_sheet = Str(config=True)
92
91
93 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
92 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
94 # the style sheet is queried for Pygments style information.
93 # the style sheet is queried for Pygments style information.
95 syntax_style = Str(config=True)
94 syntax_style = Str(config=True)
96
95
97 # Prompts.
96 # Prompts.
98 in_prompt = Str(default_in_prompt, config=True)
97 in_prompt = Str(default_in_prompt, config=True)
99 out_prompt = Str(default_out_prompt, config=True)
98 out_prompt = Str(default_out_prompt, config=True)
100 input_sep = Str(default_input_sep, config=True)
99 input_sep = Str(default_input_sep, config=True)
101 output_sep = Str(default_output_sep, config=True)
100 output_sep = Str(default_output_sep, config=True)
102 output_sep2 = Str(default_output_sep2, config=True)
101 output_sep2 = Str(default_output_sep2, config=True)
103
102
104 # FrontendWidget protected class variables.
103 # FrontendWidget protected class variables.
105 _input_splitter_class = IPythonInputSplitter
104 _input_splitter_class = IPythonInputSplitter
106
105
107 # IPythonWidget protected class variables.
106 # IPythonWidget protected class variables.
108 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
107 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
109 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
108 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
110 _payload_source_page = 'IPython.zmq.page.page'
109 _payload_source_page = 'IPython.zmq.page.page'
111
110
112 #---------------------------------------------------------------------------
111 #---------------------------------------------------------------------------
113 # 'object' interface
112 # 'object' interface
114 #---------------------------------------------------------------------------
113 #---------------------------------------------------------------------------
115
114
116 def __init__(self, *args, **kw):
115 def __init__(self, *args, **kw):
117 super(IPythonWidget, self).__init__(*args, **kw)
116 super(IPythonWidget, self).__init__(*args, **kw)
118
117
119 # IPythonWidget protected variables.
118 # IPythonWidget protected variables.
120 self._previous_prompt_obj = None
119 self._previous_prompt_obj = None
121
120
122 # Initialize widget styling.
121 # Initialize widget styling.
123 if self.style_sheet:
122 if self.style_sheet:
124 self._style_sheet_changed()
123 self._style_sheet_changed()
125 self._syntax_style_changed()
124 self._syntax_style_changed()
126 else:
125 else:
127 self.set_default_style()
126 self.set_default_style()
128
127
129 #---------------------------------------------------------------------------
128 #---------------------------------------------------------------------------
130 # 'BaseFrontendMixin' abstract interface
129 # 'BaseFrontendMixin' abstract interface
131 #---------------------------------------------------------------------------
130 #---------------------------------------------------------------------------
132
131
133 def _handle_complete_reply(self, rep):
132 def _handle_complete_reply(self, rep):
134 """ Reimplemented to support IPython's improved completion machinery.
133 """ Reimplemented to support IPython's improved completion machinery.
135 """
134 """
136 cursor = self._get_cursor()
135 cursor = self._get_cursor()
137 if rep['parent_header']['msg_id'] == self._complete_id and \
136 info = self._request_info.get('complete')
138 cursor.position() == self._complete_pos:
137 if info and info.id == rep['parent_header']['msg_id'] and \
138 info.pos == cursor.position():
139 matches = rep['content']['matches']
139 matches = rep['content']['matches']
140 text = rep['content']['matched_text']
140 text = rep['content']['matched_text']
141
141
142 # Clean up matches with '.'s and path separators.
142 # Clean up matches with '.'s and path separators.
143 parts = re.split(r'[./\\]', text)
143 parts = re.split(r'[./\\]', text)
144 sep_count = len(parts) - 1
144 sep_count = len(parts) - 1
145 if sep_count:
145 if sep_count:
146 chop_length = sum(map(len, parts[:sep_count])) + sep_count
146 chop_length = sum(map(len, parts[:sep_count])) + sep_count
147 matches = [ match[chop_length:] for match in matches ]
147 matches = [ match[chop_length:] for match in matches ]
148 text = text[chop_length:]
148 text = text[chop_length:]
149
149
150 # Move the cursor to the start of the match and complete.
150 # Move the cursor to the start of the match and complete.
151 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
151 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
152 self._complete_with_items(cursor, matches)
152 self._complete_with_items(cursor, matches)
153
153
154 def _handle_execute_reply(self, msg):
155 """ Reimplemented to support prompt requests.
156 """
157 info = self._request_info.get('execute')
158 if info and info.id == msg['parent_header']['msg_id']:
159 if info.kind == 'prompt':
160 number = msg['content']['execution_count'] + 1
161 self._show_interpreter_prompt(number)
162 else:
163 super(IPythonWidget, self)._handle_execute_reply(msg)
164
154 def _handle_history_reply(self, msg):
165 def _handle_history_reply(self, msg):
155 """ Implemented to handle history replies, which are only supported by
166 """ Implemented to handle history replies, which are only supported by
156 the IPython kernel.
167 the IPython kernel.
157 """
168 """
158 history_dict = msg['content']['history']
169 history_dict = msg['content']['history']
159 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
170 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
160 self._set_history(items)
171 self._set_history(items)
161
172
162 def _handle_prompt_reply(self, msg):
163 """ Implemented to handle prompt number replies, which are only
164 supported by the IPython kernel.
165 """
166 self._show_interpreter_prompt(msg['content']['execution_count'])
167
168 def _handle_pyout(self, msg):
173 def _handle_pyout(self, msg):
169 """ Reimplemented for IPython-style "display hook".
174 """ Reimplemented for IPython-style "display hook".
170 """
175 """
171 if not self._hidden and self._is_from_this_session(msg):
176 if not self._hidden and self._is_from_this_session(msg):
172 content = msg['content']
177 content = msg['content']
173 prompt_number = content['execution_count']
178 prompt_number = content['execution_count']
174 self._append_plain_text(self.output_sep)
179 self._append_plain_text(self.output_sep)
175 self._append_html(self._make_out_prompt(prompt_number))
180 self._append_html(self._make_out_prompt(prompt_number))
176 self._append_plain_text(content['data']+self.output_sep2)
181 self._append_plain_text(content['data']+self.output_sep2)
177
182
178 def _started_channels(self):
183 def _started_channels(self):
179 """ Reimplemented to make a history request.
184 """ Reimplemented to make a history request.
180 """
185 """
181 super(IPythonWidget, self)._started_channels()
186 super(IPythonWidget, self)._started_channels()
182 # FIXME: Disabled until history requests are properly implemented.
187 # FIXME: Disabled until history requests are properly implemented.
183 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
188 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
184
189
185 #---------------------------------------------------------------------------
190 #---------------------------------------------------------------------------
186 # 'FrontendWidget' interface
191 # 'FrontendWidget' interface
187 #---------------------------------------------------------------------------
192 #---------------------------------------------------------------------------
188
193
189 def execute_file(self, path, hidden=False):
194 def execute_file(self, path, hidden=False):
190 """ Reimplemented to use the 'run' magic.
195 """ Reimplemented to use the 'run' magic.
191 """
196 """
192 self.execute('%%run %s' % path, hidden=hidden)
197 self.execute('%%run %s' % path, hidden=hidden)
193
198
194 #---------------------------------------------------------------------------
199 #---------------------------------------------------------------------------
195 # 'FrontendWidget' protected interface
200 # 'FrontendWidget' protected interface
196 #---------------------------------------------------------------------------
201 #---------------------------------------------------------------------------
197
202
198 def _complete(self):
203 def _complete(self):
199 """ Reimplemented to support IPython's improved completion machinery.
204 """ Reimplemented to support IPython's improved completion machinery.
200 """
205 """
201 # We let the kernel split the input line, so we *always* send an empty
206 # We let the kernel split the input line, so we *always* send an empty
202 # text field. Readline-based frontends do get a real text field which
207 # text field. Readline-based frontends do get a real text field which
203 # they can use.
208 # they can use.
204 text = ''
209 text = ''
205
210
206 # Send the completion request to the kernel
211 # Send the completion request to the kernel
207 self._complete_id = self.kernel_manager.xreq_channel.complete(
212 msg_id = self.kernel_manager.xreq_channel.complete(
208 text, # text
213 text, # text
209 self._get_input_buffer_cursor_line(), # line
214 self._get_input_buffer_cursor_line(), # line
210 self._get_input_buffer_cursor_column(), # cursor_pos
215 self._get_input_buffer_cursor_column(), # cursor_pos
211 self.input_buffer) # block
216 self.input_buffer) # block
212 self._complete_pos = self._get_cursor().position()
217 pos = self._get_cursor().position()
218 info = self._CompletionRequest(msg_id, pos)
219 self._request_info['complete'] = info
213
220
214 def _get_banner(self):
221 def _get_banner(self):
215 """ Reimplemented to return IPython's default banner.
222 """ Reimplemented to return IPython's default banner.
216 """
223 """
217 return default_banner + '\n'
224 return default_banner + '\n'
218
225
219 def _process_execute_error(self, msg):
226 def _process_execute_error(self, msg):
220 """ Reimplemented for IPython-style traceback formatting.
227 """ Reimplemented for IPython-style traceback formatting.
221 """
228 """
222 content = msg['content']
229 content = msg['content']
223 traceback = '\n'.join(content['traceback']) + '\n'
230 traceback = '\n'.join(content['traceback']) + '\n'
224 if False:
231 if False:
225 # FIXME: For now, tracebacks come as plain text, so we can't use
232 # FIXME: For now, tracebacks come as plain text, so we can't use
226 # the html renderer yet. Once we refactor ultratb to produce
233 # the html renderer yet. Once we refactor ultratb to produce
227 # properly styled tracebacks, this branch should be the default
234 # properly styled tracebacks, this branch should be the default
228 traceback = traceback.replace(' ', '&nbsp;')
235 traceback = traceback.replace(' ', '&nbsp;')
229 traceback = traceback.replace('\n', '<br/>')
236 traceback = traceback.replace('\n', '<br/>')
230
237
231 ename = content['ename']
238 ename = content['ename']
232 ename_styled = '<span class="error">%s</span>' % ename
239 ename_styled = '<span class="error">%s</span>' % ename
233 traceback = traceback.replace(ename, ename_styled)
240 traceback = traceback.replace(ename, ename_styled)
234
241
235 self._append_html(traceback)
242 self._append_html(traceback)
236 else:
243 else:
237 # This is the fallback for now, using plain text with ansi escapes
244 # This is the fallback for now, using plain text with ansi escapes
238 self._append_plain_text(traceback)
245 self._append_plain_text(traceback)
239
246
240 def _process_execute_payload(self, item):
247 def _process_execute_payload(self, item):
241 """ Reimplemented to handle %edit and paging payloads.
248 """ Reimplemented to handle %edit and paging payloads.
242 """
249 """
243 if item['source'] == self._payload_source_edit:
250 if item['source'] == self._payload_source_edit:
244 self._edit(item['filename'], item['line_number'])
251 self._edit(item['filename'], item['line_number'])
245 return True
252 return True
246 elif item['source'] == self._payload_source_page:
253 elif item['source'] == self._payload_source_page:
247 self._page(item['data'])
254 self._page(item['data'])
248 return True
255 return True
249 else:
256 else:
250 return False
257 return False
251
258
252 def _show_interpreter_prompt(self, number=None):
259 def _show_interpreter_prompt(self, number=None):
253 """ Reimplemented for IPython-style prompts.
260 """ Reimplemented for IPython-style prompts.
254 """
261 """
255 # If a number was not specified, make a prompt number request.
262 # If a number was not specified, make a prompt number request.
256 if number is None:
263 if number is None:
257 # FIXME - fperez: this should be a silent code request
264 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
258 number = 1
265 info = self._ExecutionRequest(msg_id, 'prompt')
259 ##self.kernel_manager.xreq_channel.prompt()
266 self._request_info['execute'] = info
260 ##return
267 return
261
268
262 # Show a new prompt and save information about it so that it can be
269 # Show a new prompt and save information about it so that it can be
263 # updated later if the prompt number turns out to be wrong.
270 # updated later if the prompt number turns out to be wrong.
264 self._prompt_sep = self.input_sep
271 self._prompt_sep = self.input_sep
265 self._show_prompt(self._make_in_prompt(number), html=True)
272 self._show_prompt(self._make_in_prompt(number), html=True)
266 block = self._control.document().lastBlock()
273 block = self._control.document().lastBlock()
267 length = len(self._prompt)
274 length = len(self._prompt)
268 self._previous_prompt_obj = self._PromptBlock(block, length, number)
275 self._previous_prompt_obj = self._PromptBlock(block, length, number)
269
276
270 # Update continuation prompt to reflect (possibly) new prompt length.
277 # Update continuation prompt to reflect (possibly) new prompt length.
271 self._set_continuation_prompt(
278 self._set_continuation_prompt(
272 self._make_continuation_prompt(self._prompt), html=True)
279 self._make_continuation_prompt(self._prompt), html=True)
273
280
274 def _show_interpreter_prompt_for_reply(self, msg):
281 def _show_interpreter_prompt_for_reply(self, msg):
275 """ Reimplemented for IPython-style prompts.
282 """ Reimplemented for IPython-style prompts.
276 """
283 """
277 # Update the old prompt number if necessary.
284 # Update the old prompt number if necessary.
278 content = msg['content']
285 content = msg['content']
279 ##io.rprint('_show_interpreter_prompt_for_reply\n', content) # dbg
280 previous_prompt_number = content['execution_count']
286 previous_prompt_number = content['execution_count']
281 if self._previous_prompt_obj and \
287 if self._previous_prompt_obj and \
282 self._previous_prompt_obj.number != previous_prompt_number:
288 self._previous_prompt_obj.number != previous_prompt_number:
283 block = self._previous_prompt_obj.block
289 block = self._previous_prompt_obj.block
284
290
285 # Make sure the prompt block has not been erased.
291 # Make sure the prompt block has not been erased.
286 if block.isValid() and not block.text().isEmpty():
292 if block.isValid() and not block.text().isEmpty():
287
293
288 # Remove the old prompt and insert a new prompt.
294 # Remove the old prompt and insert a new prompt.
289 cursor = QtGui.QTextCursor(block)
295 cursor = QtGui.QTextCursor(block)
290 cursor.movePosition(QtGui.QTextCursor.Right,
296 cursor.movePosition(QtGui.QTextCursor.Right,
291 QtGui.QTextCursor.KeepAnchor,
297 QtGui.QTextCursor.KeepAnchor,
292 self._previous_prompt_obj.length)
298 self._previous_prompt_obj.length)
293 prompt = self._make_in_prompt(previous_prompt_number)
299 prompt = self._make_in_prompt(previous_prompt_number)
294 self._prompt = self._insert_html_fetching_plain_text(
300 self._prompt = self._insert_html_fetching_plain_text(
295 cursor, prompt)
301 cursor, prompt)
296
302
297 # When the HTML is inserted, Qt blows away the syntax
303 # When the HTML is inserted, Qt blows away the syntax
298 # highlighting for the line, so we need to rehighlight it.
304 # highlighting for the line, so we need to rehighlight it.
299 self._highlighter.rehighlightBlock(cursor.block())
305 self._highlighter.rehighlightBlock(cursor.block())
300
306
301 self._previous_prompt_obj = None
307 self._previous_prompt_obj = None
302
308
303 # Show a new prompt with the kernel's estimated prompt number.
309 # Show a new prompt with the kernel's estimated prompt number.
304 self._show_interpreter_prompt(previous_prompt_number+1)
310 self._show_interpreter_prompt(previous_prompt_number+1)
305
311
306 #---------------------------------------------------------------------------
312 #---------------------------------------------------------------------------
307 # 'IPythonWidget' interface
313 # 'IPythonWidget' interface
308 #---------------------------------------------------------------------------
314 #---------------------------------------------------------------------------
309
315
310 def set_default_style(self, lightbg=True):
316 def set_default_style(self, lightbg=True):
311 """ Sets the widget style to the class defaults.
317 """ Sets the widget style to the class defaults.
312
318
313 Parameters:
319 Parameters:
314 -----------
320 -----------
315 lightbg : bool, optional (default True)
321 lightbg : bool, optional (default True)
316 Whether to use the default IPython light background or dark
322 Whether to use the default IPython light background or dark
317 background style.
323 background style.
318 """
324 """
319 if lightbg:
325 if lightbg:
320 self.style_sheet = default_light_style_sheet
326 self.style_sheet = default_light_style_sheet
321 self.syntax_style = default_light_syntax_style
327 self.syntax_style = default_light_syntax_style
322 else:
328 else:
323 self.style_sheet = default_dark_style_sheet
329 self.style_sheet = default_dark_style_sheet
324 self.syntax_style = default_dark_syntax_style
330 self.syntax_style = default_dark_syntax_style
325
331
326 #---------------------------------------------------------------------------
332 #---------------------------------------------------------------------------
327 # 'IPythonWidget' protected interface
333 # 'IPythonWidget' protected interface
328 #---------------------------------------------------------------------------
334 #---------------------------------------------------------------------------
329
335
330 def _edit(self, filename, line=None):
336 def _edit(self, filename, line=None):
331 """ Opens a Python script for editing.
337 """ Opens a Python script for editing.
332
338
333 Parameters:
339 Parameters:
334 -----------
340 -----------
335 filename : str
341 filename : str
336 A path to a local system file.
342 A path to a local system file.
337
343
338 line : int, optional
344 line : int, optional
339 A line of interest in the file.
345 A line of interest in the file.
340 """
346 """
341 if self.custom_edit:
347 if self.custom_edit:
342 self.custom_edit_requested.emit(filename, line)
348 self.custom_edit_requested.emit(filename, line)
343 elif self.editor == 'default':
349 elif self.editor == 'default':
344 self._append_plain_text('No default editor available.\n')
350 self._append_plain_text('No default editor available.\n')
345 else:
351 else:
346 try:
352 try:
347 filename = '"%s"' % filename
353 filename = '"%s"' % filename
348 if line and self.editor_line:
354 if line and self.editor_line:
349 command = self.editor_line.format(filename=filename,
355 command = self.editor_line.format(filename=filename,
350 line=line)
356 line=line)
351 else:
357 else:
352 try:
358 try:
353 command = self.editor.format()
359 command = self.editor.format()
354 except KeyError:
360 except KeyError:
355 command = self.editor.format(filename=filename)
361 command = self.editor.format(filename=filename)
356 else:
362 else:
357 command += ' ' + filename
363 command += ' ' + filename
358 except KeyError:
364 except KeyError:
359 self._append_plain_text('Invalid editor command.\n')
365 self._append_plain_text('Invalid editor command.\n')
360 else:
366 else:
361 try:
367 try:
362 Popen(command, shell=True)
368 Popen(command, shell=True)
363 except OSError:
369 except OSError:
364 msg = 'Opening editor with command "%s" failed.\n'
370 msg = 'Opening editor with command "%s" failed.\n'
365 self._append_plain_text(msg % command)
371 self._append_plain_text(msg % command)
366
372
367 def _make_in_prompt(self, number):
373 def _make_in_prompt(self, number):
368 """ Given a prompt number, returns an HTML In prompt.
374 """ Given a prompt number, returns an HTML In prompt.
369 """
375 """
370 body = self.in_prompt % number
376 body = self.in_prompt % number
371 return '<span class="in-prompt">%s</span>' % body
377 return '<span class="in-prompt">%s</span>' % body
372
378
373 def _make_continuation_prompt(self, prompt):
379 def _make_continuation_prompt(self, prompt):
374 """ Given a plain text version of an In prompt, returns an HTML
380 """ Given a plain text version of an In prompt, returns an HTML
375 continuation prompt.
381 continuation prompt.
376 """
382 """
377 end_chars = '...: '
383 end_chars = '...: '
378 space_count = len(prompt.lstrip('\n')) - len(end_chars)
384 space_count = len(prompt.lstrip('\n')) - len(end_chars)
379 body = '&nbsp;' * space_count + end_chars
385 body = '&nbsp;' * space_count + end_chars
380 return '<span class="in-prompt">%s</span>' % body
386 return '<span class="in-prompt">%s</span>' % body
381
387
382 def _make_out_prompt(self, number):
388 def _make_out_prompt(self, number):
383 """ Given a prompt number, returns an HTML Out prompt.
389 """ Given a prompt number, returns an HTML Out prompt.
384 """
390 """
385 body = self.out_prompt % number
391 body = self.out_prompt % number
386 return '<span class="out-prompt">%s</span>' % body
392 return '<span class="out-prompt">%s</span>' % body
387
393
388 #------ Trait change handlers ---------------------------------------------
394 #------ Trait change handlers ---------------------------------------------
389
395
390 def _style_sheet_changed(self):
396 def _style_sheet_changed(self):
391 """ Set the style sheets of the underlying widgets.
397 """ Set the style sheets of the underlying widgets.
392 """
398 """
393 self.setStyleSheet(self.style_sheet)
399 self.setStyleSheet(self.style_sheet)
394 self._control.document().setDefaultStyleSheet(self.style_sheet)
400 self._control.document().setDefaultStyleSheet(self.style_sheet)
395 if self._page_control:
401 if self._page_control:
396 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
402 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
397
403
398 bg_color = self._control.palette().background().color()
404 bg_color = self._control.palette().background().color()
399 self._ansi_processor.set_background_color(bg_color)
405 self._ansi_processor.set_background_color(bg_color)
400
406
401 def _syntax_style_changed(self):
407 def _syntax_style_changed(self):
402 """ Set the style for the syntax highlighter.
408 """ Set the style for the syntax highlighter.
403 """
409 """
404 if self.syntax_style:
410 if self.syntax_style:
405 self._highlighter.set_style(self.syntax_style)
411 self._highlighter.set_style(self.syntax_style)
406 else:
412 else:
407 self._highlighter.set_style_sheet(self.style_sheet)
413 self._highlighter.set_style_sheet(self.style_sheet)
408
414
@@ -1,786 +1,785 b''
1 """Base classes to manage the interaction with a running kernel.
1 """Base classes to manage the interaction with a running kernel.
2
2
3 Todo
3 Todo
4 ====
4 ====
5
5
6 * Create logger to handle debugging and console messages.
6 * Create logger to handle debugging and console messages.
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2008-2010 The IPython Development Team
10 # Copyright (C) 2008-2010 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 # Standard library imports.
20 # Standard library imports.
21 from Queue import Queue, Empty
21 from Queue import Queue, Empty
22 from subprocess import Popen
22 from subprocess import Popen
23 from threading import Thread
23 from threading import Thread
24 import time
24 import time
25
25
26 # System library imports.
26 # System library imports.
27 import zmq
27 import zmq
28 from zmq import POLLIN, POLLOUT, POLLERR
28 from zmq import POLLIN, POLLOUT, POLLERR
29 from zmq.eventloop import ioloop
29 from zmq.eventloop import ioloop
30
30
31 # Local imports.
31 # Local imports.
32 from IPython.utils import io
32 from IPython.utils import io
33 from IPython.utils.traitlets import HasTraits, Any, Instance, Type, TCPAddress
33 from IPython.utils.traitlets import HasTraits, Any, Instance, Type, TCPAddress
34 from session import Session
34 from session import Session
35
35
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37 # Constants and exceptions
37 # Constants and exceptions
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39
39
40 LOCALHOST = '127.0.0.1'
40 LOCALHOST = '127.0.0.1'
41
41
42 class InvalidPortNumber(Exception):
42 class InvalidPortNumber(Exception):
43 pass
43 pass
44
44
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46 # Utility functions
46 # Utility functions
47 #-----------------------------------------------------------------------------
47 #-----------------------------------------------------------------------------
48
48
49 # some utilities to validate message structure, these might get moved elsewhere
49 # some utilities to validate message structure, these might get moved elsewhere
50 # if they prove to have more generic utility
50 # if they prove to have more generic utility
51
51
52 def validate_string_list(lst):
52 def validate_string_list(lst):
53 """Validate that the input is a list of strings.
53 """Validate that the input is a list of strings.
54
54
55 Raises ValueError if not."""
55 Raises ValueError if not."""
56 if not isinstance(lst, list):
56 if not isinstance(lst, list):
57 raise ValueError('input %r must be a list' % lst)
57 raise ValueError('input %r must be a list' % lst)
58 for x in lst:
58 for x in lst:
59 if not isinstance(x, basestring):
59 if not isinstance(x, basestring):
60 raise ValueError('element %r in list must be a string' % x)
60 raise ValueError('element %r in list must be a string' % x)
61
61
62
62
63 def validate_string_dict(dct):
63 def validate_string_dict(dct):
64 """Validate that the input is a dict with string keys and values.
64 """Validate that the input is a dict with string keys and values.
65
65
66 Raises ValueError if not."""
66 Raises ValueError if not."""
67 for k,v in dct.iteritems():
67 for k,v in dct.iteritems():
68 if not isinstance(k, basestring):
68 if not isinstance(k, basestring):
69 raise ValueError('key %r in dict must be a string' % k)
69 raise ValueError('key %r in dict must be a string' % k)
70 if not isinstance(v, basestring):
70 if not isinstance(v, basestring):
71 raise ValueError('value %r in dict must be a string' % v)
71 raise ValueError('value %r in dict must be a string' % v)
72
72
73
73
74 #-----------------------------------------------------------------------------
74 #-----------------------------------------------------------------------------
75 # ZMQ Socket Channel classes
75 # ZMQ Socket Channel classes
76 #-----------------------------------------------------------------------------
76 #-----------------------------------------------------------------------------
77
77
78 class ZmqSocketChannel(Thread):
78 class ZmqSocketChannel(Thread):
79 """The base class for the channels that use ZMQ sockets.
79 """The base class for the channels that use ZMQ sockets.
80 """
80 """
81 context = None
81 context = None
82 session = None
82 session = None
83 socket = None
83 socket = None
84 ioloop = None
84 ioloop = None
85 iostate = None
85 iostate = None
86 _address = None
86 _address = None
87
87
88 def __init__(self, context, session, address):
88 def __init__(self, context, session, address):
89 """Create a channel
89 """Create a channel
90
90
91 Parameters
91 Parameters
92 ----------
92 ----------
93 context : :class:`zmq.Context`
93 context : :class:`zmq.Context`
94 The ZMQ context to use.
94 The ZMQ context to use.
95 session : :class:`session.Session`
95 session : :class:`session.Session`
96 The session to use.
96 The session to use.
97 address : tuple
97 address : tuple
98 Standard (ip, port) tuple that the kernel is listening on.
98 Standard (ip, port) tuple that the kernel is listening on.
99 """
99 """
100 super(ZmqSocketChannel, self).__init__()
100 super(ZmqSocketChannel, self).__init__()
101 self.daemon = True
101 self.daemon = True
102
102
103 self.context = context
103 self.context = context
104 self.session = session
104 self.session = session
105 if address[1] == 0:
105 if address[1] == 0:
106 message = 'The port number for a channel cannot be 0.'
106 message = 'The port number for a channel cannot be 0.'
107 raise InvalidPortNumber(message)
107 raise InvalidPortNumber(message)
108 self._address = address
108 self._address = address
109
109
110 def stop(self):
110 def stop(self):
111 """Stop the channel's activity.
111 """Stop the channel's activity.
112
112
113 This calls :method:`Thread.join` and returns when the thread
113 This calls :method:`Thread.join` and returns when the thread
114 terminates. :class:`RuntimeError` will be raised if
114 terminates. :class:`RuntimeError` will be raised if
115 :method:`self.start` is called again.
115 :method:`self.start` is called again.
116 """
116 """
117 self.join()
117 self.join()
118
118
119 @property
119 @property
120 def address(self):
120 def address(self):
121 """Get the channel's address as an (ip, port) tuple.
121 """Get the channel's address as an (ip, port) tuple.
122
122
123 By the default, the address is (localhost, 0), where 0 means a random
123 By the default, the address is (localhost, 0), where 0 means a random
124 port.
124 port.
125 """
125 """
126 return self._address
126 return self._address
127
127
128 def add_io_state(self, state):
128 def add_io_state(self, state):
129 """Add IO state to the eventloop.
129 """Add IO state to the eventloop.
130
130
131 Parameters
131 Parameters
132 ----------
132 ----------
133 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
133 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
134 The IO state flag to set.
134 The IO state flag to set.
135
135
136 This is thread safe as it uses the thread safe IOLoop.add_callback.
136 This is thread safe as it uses the thread safe IOLoop.add_callback.
137 """
137 """
138 def add_io_state_callback():
138 def add_io_state_callback():
139 if not self.iostate & state:
139 if not self.iostate & state:
140 self.iostate = self.iostate | state
140 self.iostate = self.iostate | state
141 self.ioloop.update_handler(self.socket, self.iostate)
141 self.ioloop.update_handler(self.socket, self.iostate)
142 self.ioloop.add_callback(add_io_state_callback)
142 self.ioloop.add_callback(add_io_state_callback)
143
143
144 def drop_io_state(self, state):
144 def drop_io_state(self, state):
145 """Drop IO state from the eventloop.
145 """Drop IO state from the eventloop.
146
146
147 Parameters
147 Parameters
148 ----------
148 ----------
149 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
149 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
150 The IO state flag to set.
150 The IO state flag to set.
151
151
152 This is thread safe as it uses the thread safe IOLoop.add_callback.
152 This is thread safe as it uses the thread safe IOLoop.add_callback.
153 """
153 """
154 def drop_io_state_callback():
154 def drop_io_state_callback():
155 if self.iostate & state:
155 if self.iostate & state:
156 self.iostate = self.iostate & (~state)
156 self.iostate = self.iostate & (~state)
157 self.ioloop.update_handler(self.socket, self.iostate)
157 self.ioloop.update_handler(self.socket, self.iostate)
158 self.ioloop.add_callback(drop_io_state_callback)
158 self.ioloop.add_callback(drop_io_state_callback)
159
159
160
160
161 class XReqSocketChannel(ZmqSocketChannel):
161 class XReqSocketChannel(ZmqSocketChannel):
162 """The XREQ channel for issues request/replies to the kernel.
162 """The XREQ channel for issues request/replies to the kernel.
163 """
163 """
164
164
165 command_queue = None
165 command_queue = None
166
166
167 def __init__(self, context, session, address):
167 def __init__(self, context, session, address):
168 self.command_queue = Queue()
168 self.command_queue = Queue()
169 super(XReqSocketChannel, self).__init__(context, session, address)
169 super(XReqSocketChannel, self).__init__(context, session, address)
170
170
171 def run(self):
171 def run(self):
172 """The thread's main activity. Call start() instead."""
172 """The thread's main activity. Call start() instead."""
173 self.socket = self.context.socket(zmq.XREQ)
173 self.socket = self.context.socket(zmq.XREQ)
174 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
174 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
175 self.socket.connect('tcp://%s:%i' % self.address)
175 self.socket.connect('tcp://%s:%i' % self.address)
176 self.ioloop = ioloop.IOLoop()
176 self.ioloop = ioloop.IOLoop()
177 self.iostate = POLLERR|POLLIN
177 self.iostate = POLLERR|POLLIN
178 self.ioloop.add_handler(self.socket, self._handle_events,
178 self.ioloop.add_handler(self.socket, self._handle_events,
179 self.iostate)
179 self.iostate)
180 self.ioloop.start()
180 self.ioloop.start()
181
181
182 def stop(self):
182 def stop(self):
183 self.ioloop.stop()
183 self.ioloop.stop()
184 super(XReqSocketChannel, self).stop()
184 super(XReqSocketChannel, self).stop()
185
185
186 def call_handlers(self, msg):
186 def call_handlers(self, msg):
187 """This method is called in the ioloop thread when a message arrives.
187 """This method is called in the ioloop thread when a message arrives.
188
188
189 Subclasses should override this method to handle incoming messages.
189 Subclasses should override this method to handle incoming messages.
190 It is important to remember that this method is called in the thread
190 It is important to remember that this method is called in the thread
191 so that some logic must be done to ensure that the application leve
191 so that some logic must be done to ensure that the application leve
192 handlers are called in the application thread.
192 handlers are called in the application thread.
193 """
193 """
194 raise NotImplementedError('call_handlers must be defined in a subclass.')
194 raise NotImplementedError('call_handlers must be defined in a subclass.')
195
195
196 def execute(self, code, silent=False,
196 def execute(self, code, silent=False,
197 user_variables=None, user_expressions=None):
197 user_variables=None, user_expressions=None):
198 """Execute code in the kernel.
198 """Execute code in the kernel.
199
199
200 Parameters
200 Parameters
201 ----------
201 ----------
202 code : str
202 code : str
203 A string of Python code.
203 A string of Python code.
204
204
205 silent : bool, optional (default False)
205 silent : bool, optional (default False)
206 If set, the kernel will execute the code as quietly possible.
206 If set, the kernel will execute the code as quietly possible.
207
207
208 user_variables : list, optional
208 user_variables : list, optional
209
210 A list of variable names to pull from the user's namespace. They
209 A list of variable names to pull from the user's namespace. They
211 will come back as a dict with these names as keys and their
210 will come back as a dict with these names as keys and their
212 :func:`repr` as values.
211 :func:`repr` as values.
213
212
214 user_expressions : dict, optional
213 user_expressions : dict, optional
215 A dict with string keys and to pull from the user's
214 A dict with string keys and to pull from the user's
216 namespace. They will come back as a dict with these names as keys
215 namespace. They will come back as a dict with these names as keys
217 and their :func:`repr` as values.
216 and their :func:`repr` as values.
218
217
219 Returns
218 Returns
220 -------
219 -------
221 The msg_id of the message sent.
220 The msg_id of the message sent.
222 """
221 """
223 if user_variables is None:
222 if user_variables is None:
224 user_variables = []
223 user_variables = []
225 if user_expressions is None:
224 if user_expressions is None:
226 user_expressions = {}
225 user_expressions = {}
227
226
228 # Don't waste network traffic if inputs are invalid
227 # Don't waste network traffic if inputs are invalid
229 if not isinstance(code, basestring):
228 if not isinstance(code, basestring):
230 raise ValueError('code %r must be a string' % code)
229 raise ValueError('code %r must be a string' % code)
231 validate_string_list(user_variables)
230 validate_string_list(user_variables)
232 validate_string_dict(user_expressions)
231 validate_string_dict(user_expressions)
233
232
234 # Create class for content/msg creation. Related to, but possibly
233 # Create class for content/msg creation. Related to, but possibly
235 # not in Session.
234 # not in Session.
236 content = dict(code=code, silent=silent,
235 content = dict(code=code, silent=silent,
237 user_variables=user_variables,
236 user_variables=user_variables,
238 user_expressions=user_expressions)
237 user_expressions=user_expressions)
239 msg = self.session.msg('execute_request', content)
238 msg = self.session.msg('execute_request', content)
240 self._queue_request(msg)
239 self._queue_request(msg)
241 return msg['header']['msg_id']
240 return msg['header']['msg_id']
242
241
243 def complete(self, text, line, cursor_pos, block=None):
242 def complete(self, text, line, cursor_pos, block=None):
244 """Tab complete text in the kernel's namespace.
243 """Tab complete text in the kernel's namespace.
245
244
246 Parameters
245 Parameters
247 ----------
246 ----------
248 text : str
247 text : str
249 The text to complete.
248 The text to complete.
250 line : str
249 line : str
251 The full line of text that is the surrounding context for the
250 The full line of text that is the surrounding context for the
252 text to complete.
251 text to complete.
253 cursor_pos : int
252 cursor_pos : int
254 The position of the cursor in the line where the completion was
253 The position of the cursor in the line where the completion was
255 requested.
254 requested.
256 block : str, optional
255 block : str, optional
257 The full block of code in which the completion is being requested.
256 The full block of code in which the completion is being requested.
258
257
259 Returns
258 Returns
260 -------
259 -------
261 The msg_id of the message sent.
260 The msg_id of the message sent.
262 """
261 """
263 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
262 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
264 msg = self.session.msg('complete_request', content)
263 msg = self.session.msg('complete_request', content)
265 self._queue_request(msg)
264 self._queue_request(msg)
266 return msg['header']['msg_id']
265 return msg['header']['msg_id']
267
266
268 def object_info(self, oname):
267 def object_info(self, oname):
269 """Get metadata information about an object.
268 """Get metadata information about an object.
270
269
271 Parameters
270 Parameters
272 ----------
271 ----------
273 oname : str
272 oname : str
274 A string specifying the object name.
273 A string specifying the object name.
275
274
276 Returns
275 Returns
277 -------
276 -------
278 The msg_id of the message sent.
277 The msg_id of the message sent.
279 """
278 """
280 content = dict(oname=oname)
279 content = dict(oname=oname)
281 msg = self.session.msg('object_info_request', content)
280 msg = self.session.msg('object_info_request', content)
282 self._queue_request(msg)
281 self._queue_request(msg)
283 return msg['header']['msg_id']
282 return msg['header']['msg_id']
284
283
285 def history(self, index=None, raw=False, output=True):
284 def history(self, index=None, raw=False, output=True):
286 """Get the history list.
285 """Get the history list.
287
286
288 Parameters
287 Parameters
289 ----------
288 ----------
290 index : n or (n1, n2) or None
289 index : n or (n1, n2) or None
291 If n, then the last entries. If a tuple, then all in
290 If n, then the last entries. If a tuple, then all in
292 range(n1, n2). If None, then all entries. Raises IndexError if
291 range(n1, n2). If None, then all entries. Raises IndexError if
293 the format of index is incorrect.
292 the format of index is incorrect.
294 raw : bool
293 raw : bool
295 If True, return the raw input.
294 If True, return the raw input.
296 output : bool
295 output : bool
297 If True, then return the output as well.
296 If True, then return the output as well.
298
297
299 Returns
298 Returns
300 -------
299 -------
301 The msg_id of the message sent.
300 The msg_id of the message sent.
302 """
301 """
303 content = dict(index=index, raw=raw, output=output)
302 content = dict(index=index, raw=raw, output=output)
304 msg = self.session.msg('history_request', content)
303 msg = self.session.msg('history_request', content)
305 self._queue_request(msg)
304 self._queue_request(msg)
306 return msg['header']['msg_id']
305 return msg['header']['msg_id']
307
306
308 def _handle_events(self, socket, events):
307 def _handle_events(self, socket, events):
309 if events & POLLERR:
308 if events & POLLERR:
310 self._handle_err()
309 self._handle_err()
311 if events & POLLOUT:
310 if events & POLLOUT:
312 self._handle_send()
311 self._handle_send()
313 if events & POLLIN:
312 if events & POLLIN:
314 self._handle_recv()
313 self._handle_recv()
315
314
316 def _handle_recv(self):
315 def _handle_recv(self):
317 msg = self.socket.recv_json()
316 msg = self.socket.recv_json()
318 self.call_handlers(msg)
317 self.call_handlers(msg)
319
318
320 def _handle_send(self):
319 def _handle_send(self):
321 try:
320 try:
322 msg = self.command_queue.get(False)
321 msg = self.command_queue.get(False)
323 except Empty:
322 except Empty:
324 pass
323 pass
325 else:
324 else:
326 self.socket.send_json(msg)
325 self.socket.send_json(msg)
327 if self.command_queue.empty():
326 if self.command_queue.empty():
328 self.drop_io_state(POLLOUT)
327 self.drop_io_state(POLLOUT)
329
328
330 def _handle_err(self):
329 def _handle_err(self):
331 # We don't want to let this go silently, so eventually we should log.
330 # We don't want to let this go silently, so eventually we should log.
332 raise zmq.ZMQError()
331 raise zmq.ZMQError()
333
332
334 def _queue_request(self, msg):
333 def _queue_request(self, msg):
335 self.command_queue.put(msg)
334 self.command_queue.put(msg)
336 self.add_io_state(POLLOUT)
335 self.add_io_state(POLLOUT)
337
336
338
337
339 class SubSocketChannel(ZmqSocketChannel):
338 class SubSocketChannel(ZmqSocketChannel):
340 """The SUB channel which listens for messages that the kernel publishes.
339 """The SUB channel which listens for messages that the kernel publishes.
341 """
340 """
342
341
343 def __init__(self, context, session, address):
342 def __init__(self, context, session, address):
344 super(SubSocketChannel, self).__init__(context, session, address)
343 super(SubSocketChannel, self).__init__(context, session, address)
345
344
346 def run(self):
345 def run(self):
347 """The thread's main activity. Call start() instead."""
346 """The thread's main activity. Call start() instead."""
348 self.socket = self.context.socket(zmq.SUB)
347 self.socket = self.context.socket(zmq.SUB)
349 self.socket.setsockopt(zmq.SUBSCRIBE,'')
348 self.socket.setsockopt(zmq.SUBSCRIBE,'')
350 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
349 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
351 self.socket.connect('tcp://%s:%i' % self.address)
350 self.socket.connect('tcp://%s:%i' % self.address)
352 self.ioloop = ioloop.IOLoop()
351 self.ioloop = ioloop.IOLoop()
353 self.iostate = POLLIN|POLLERR
352 self.iostate = POLLIN|POLLERR
354 self.ioloop.add_handler(self.socket, self._handle_events,
353 self.ioloop.add_handler(self.socket, self._handle_events,
355 self.iostate)
354 self.iostate)
356 self.ioloop.start()
355 self.ioloop.start()
357
356
358 def stop(self):
357 def stop(self):
359 self.ioloop.stop()
358 self.ioloop.stop()
360 super(SubSocketChannel, self).stop()
359 super(SubSocketChannel, self).stop()
361
360
362 def call_handlers(self, msg):
361 def call_handlers(self, msg):
363 """This method is called in the ioloop thread when a message arrives.
362 """This method is called in the ioloop thread when a message arrives.
364
363
365 Subclasses should override this method to handle incoming messages.
364 Subclasses should override this method to handle incoming messages.
366 It is important to remember that this method is called in the thread
365 It is important to remember that this method is called in the thread
367 so that some logic must be done to ensure that the application leve
366 so that some logic must be done to ensure that the application leve
368 handlers are called in the application thread.
367 handlers are called in the application thread.
369 """
368 """
370 raise NotImplementedError('call_handlers must be defined in a subclass.')
369 raise NotImplementedError('call_handlers must be defined in a subclass.')
371
370
372 def flush(self, timeout=1.0):
371 def flush(self, timeout=1.0):
373 """Immediately processes all pending messages on the SUB channel.
372 """Immediately processes all pending messages on the SUB channel.
374
373
375 Callers should use this method to ensure that :method:`call_handlers`
374 Callers should use this method to ensure that :method:`call_handlers`
376 has been called for all messages that have been received on the
375 has been called for all messages that have been received on the
377 0MQ SUB socket of this channel.
376 0MQ SUB socket of this channel.
378
377
379 This method is thread safe.
378 This method is thread safe.
380
379
381 Parameters
380 Parameters
382 ----------
381 ----------
383 timeout : float, optional
382 timeout : float, optional
384 The maximum amount of time to spend flushing, in seconds. The
383 The maximum amount of time to spend flushing, in seconds. The
385 default is one second.
384 default is one second.
386 """
385 """
387 # We do the IOLoop callback process twice to ensure that the IOLoop
386 # We do the IOLoop callback process twice to ensure that the IOLoop
388 # gets to perform at least one full poll.
387 # gets to perform at least one full poll.
389 stop_time = time.time() + timeout
388 stop_time = time.time() + timeout
390 for i in xrange(2):
389 for i in xrange(2):
391 self._flushed = False
390 self._flushed = False
392 self.ioloop.add_callback(self._flush)
391 self.ioloop.add_callback(self._flush)
393 while not self._flushed and time.time() < stop_time:
392 while not self._flushed and time.time() < stop_time:
394 time.sleep(0.01)
393 time.sleep(0.01)
395
394
396 def _handle_events(self, socket, events):
395 def _handle_events(self, socket, events):
397 # Turn on and off POLLOUT depending on if we have made a request
396 # Turn on and off POLLOUT depending on if we have made a request
398 if events & POLLERR:
397 if events & POLLERR:
399 self._handle_err()
398 self._handle_err()
400 if events & POLLIN:
399 if events & POLLIN:
401 self._handle_recv()
400 self._handle_recv()
402
401
403 def _handle_err(self):
402 def _handle_err(self):
404 # We don't want to let this go silently, so eventually we should log.
403 # We don't want to let this go silently, so eventually we should log.
405 raise zmq.ZMQError()
404 raise zmq.ZMQError()
406
405
407 def _handle_recv(self):
406 def _handle_recv(self):
408 # Get all of the messages we can
407 # Get all of the messages we can
409 while True:
408 while True:
410 try:
409 try:
411 msg = self.socket.recv_json(zmq.NOBLOCK)
410 msg = self.socket.recv_json(zmq.NOBLOCK)
412 except zmq.ZMQError:
411 except zmq.ZMQError:
413 # Check the errno?
412 # Check the errno?
414 # Will this trigger POLLERR?
413 # Will this trigger POLLERR?
415 break
414 break
416 else:
415 else:
417 self.call_handlers(msg)
416 self.call_handlers(msg)
418
417
419 def _flush(self):
418 def _flush(self):
420 """Callback for :method:`self.flush`."""
419 """Callback for :method:`self.flush`."""
421 self._flushed = True
420 self._flushed = True
422
421
423
422
424 class RepSocketChannel(ZmqSocketChannel):
423 class RepSocketChannel(ZmqSocketChannel):
425 """A reply channel to handle raw_input requests that the kernel makes."""
424 """A reply channel to handle raw_input requests that the kernel makes."""
426
425
427 msg_queue = None
426 msg_queue = None
428
427
429 def __init__(self, context, session, address):
428 def __init__(self, context, session, address):
430 self.msg_queue = Queue()
429 self.msg_queue = Queue()
431 super(RepSocketChannel, self).__init__(context, session, address)
430 super(RepSocketChannel, self).__init__(context, session, address)
432
431
433 def run(self):
432 def run(self):
434 """The thread's main activity. Call start() instead."""
433 """The thread's main activity. Call start() instead."""
435 self.socket = self.context.socket(zmq.XREQ)
434 self.socket = self.context.socket(zmq.XREQ)
436 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
435 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
437 self.socket.connect('tcp://%s:%i' % self.address)
436 self.socket.connect('tcp://%s:%i' % self.address)
438 self.ioloop = ioloop.IOLoop()
437 self.ioloop = ioloop.IOLoop()
439 self.iostate = POLLERR|POLLIN
438 self.iostate = POLLERR|POLLIN
440 self.ioloop.add_handler(self.socket, self._handle_events,
439 self.ioloop.add_handler(self.socket, self._handle_events,
441 self.iostate)
440 self.iostate)
442 self.ioloop.start()
441 self.ioloop.start()
443
442
444 def stop(self):
443 def stop(self):
445 self.ioloop.stop()
444 self.ioloop.stop()
446 super(RepSocketChannel, self).stop()
445 super(RepSocketChannel, self).stop()
447
446
448 def call_handlers(self, msg):
447 def call_handlers(self, msg):
449 """This method is called in the ioloop thread when a message arrives.
448 """This method is called in the ioloop thread when a message arrives.
450
449
451 Subclasses should override this method to handle incoming messages.
450 Subclasses should override this method to handle incoming messages.
452 It is important to remember that this method is called in the thread
451 It is important to remember that this method is called in the thread
453 so that some logic must be done to ensure that the application leve
452 so that some logic must be done to ensure that the application leve
454 handlers are called in the application thread.
453 handlers are called in the application thread.
455 """
454 """
456 raise NotImplementedError('call_handlers must be defined in a subclass.')
455 raise NotImplementedError('call_handlers must be defined in a subclass.')
457
456
458 def input(self, string):
457 def input(self, string):
459 """Send a string of raw input to the kernel."""
458 """Send a string of raw input to the kernel."""
460 content = dict(value=string)
459 content = dict(value=string)
461 msg = self.session.msg('input_reply', content)
460 msg = self.session.msg('input_reply', content)
462 self._queue_reply(msg)
461 self._queue_reply(msg)
463
462
464 def _handle_events(self, socket, events):
463 def _handle_events(self, socket, events):
465 if events & POLLERR:
464 if events & POLLERR:
466 self._handle_err()
465 self._handle_err()
467 if events & POLLOUT:
466 if events & POLLOUT:
468 self._handle_send()
467 self._handle_send()
469 if events & POLLIN:
468 if events & POLLIN:
470 self._handle_recv()
469 self._handle_recv()
471
470
472 def _handle_recv(self):
471 def _handle_recv(self):
473 msg = self.socket.recv_json()
472 msg = self.socket.recv_json()
474 self.call_handlers(msg)
473 self.call_handlers(msg)
475
474
476 def _handle_send(self):
475 def _handle_send(self):
477 try:
476 try:
478 msg = self.msg_queue.get(False)
477 msg = self.msg_queue.get(False)
479 except Empty:
478 except Empty:
480 pass
479 pass
481 else:
480 else:
482 self.socket.send_json(msg)
481 self.socket.send_json(msg)
483 if self.msg_queue.empty():
482 if self.msg_queue.empty():
484 self.drop_io_state(POLLOUT)
483 self.drop_io_state(POLLOUT)
485
484
486 def _handle_err(self):
485 def _handle_err(self):
487 # We don't want to let this go silently, so eventually we should log.
486 # We don't want to let this go silently, so eventually we should log.
488 raise zmq.ZMQError()
487 raise zmq.ZMQError()
489
488
490 def _queue_reply(self, msg):
489 def _queue_reply(self, msg):
491 self.msg_queue.put(msg)
490 self.msg_queue.put(msg)
492 self.add_io_state(POLLOUT)
491 self.add_io_state(POLLOUT)
493
492
494
493
495 class HBSocketChannel(ZmqSocketChannel):
494 class HBSocketChannel(ZmqSocketChannel):
496 """The heartbeat channel which monitors the kernel heartbeat."""
495 """The heartbeat channel which monitors the kernel heartbeat."""
497
496
498 time_to_dead = 3.0
497 time_to_dead = 3.0
499 socket = None
498 socket = None
500 poller = None
499 poller = None
501
500
502 def __init__(self, context, session, address):
501 def __init__(self, context, session, address):
503 super(HBSocketChannel, self).__init__(context, session, address)
502 super(HBSocketChannel, self).__init__(context, session, address)
504 self._running = False
503 self._running = False
505
504
506 def _create_socket(self):
505 def _create_socket(self):
507 self.socket = self.context.socket(zmq.REQ)
506 self.socket = self.context.socket(zmq.REQ)
508 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
507 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
509 self.socket.connect('tcp://%s:%i' % self.address)
508 self.socket.connect('tcp://%s:%i' % self.address)
510 self.poller = zmq.Poller()
509 self.poller = zmq.Poller()
511 self.poller.register(self.socket, zmq.POLLIN)
510 self.poller.register(self.socket, zmq.POLLIN)
512
511
513 def run(self):
512 def run(self):
514 """The thread's main activity. Call start() instead."""
513 """The thread's main activity. Call start() instead."""
515 self._create_socket()
514 self._create_socket()
516 self._running = True
515 self._running = True
517 # Wait 2 seconds for the kernel to come up and the sockets to auto
516 # Wait 2 seconds for the kernel to come up and the sockets to auto
518 # connect. If we don't we will see the kernel as dead. Also, before
517 # connect. If we don't we will see the kernel as dead. Also, before
519 # the sockets are connected, the poller.poll line below is returning
518 # the sockets are connected, the poller.poll line below is returning
520 # too fast. This avoids that because the polling doesn't start until
519 # too fast. This avoids that because the polling doesn't start until
521 # after the sockets are connected.
520 # after the sockets are connected.
522 time.sleep(2.0)
521 time.sleep(2.0)
523 while self._running:
522 while self._running:
524 since_last_heartbeat = 0.0
523 since_last_heartbeat = 0.0
525 request_time = time.time()
524 request_time = time.time()
526 try:
525 try:
527 #io.rprint('Ping from HB channel') # dbg
526 #io.rprint('Ping from HB channel') # dbg
528 self.socket.send_json('ping')
527 self.socket.send_json('ping')
529 except zmq.ZMQError, e:
528 except zmq.ZMQError, e:
530 #io.rprint('*** HB Error:', e) # dbg
529 #io.rprint('*** HB Error:', e) # dbg
531 if e.errno == zmq.EFSM:
530 if e.errno == zmq.EFSM:
532 #io.rprint('sleep...', self.time_to_dead) # dbg
531 #io.rprint('sleep...', self.time_to_dead) # dbg
533 time.sleep(self.time_to_dead)
532 time.sleep(self.time_to_dead)
534 self._create_socket()
533 self._create_socket()
535 else:
534 else:
536 raise
535 raise
537 else:
536 else:
538 while True:
537 while True:
539 try:
538 try:
540 self.socket.recv_json(zmq.NOBLOCK)
539 self.socket.recv_json(zmq.NOBLOCK)
541 except zmq.ZMQError, e:
540 except zmq.ZMQError, e:
542 #io.rprint('*** HB Error 2:', e) # dbg
541 #io.rprint('*** HB Error 2:', e) # dbg
543 if e.errno == zmq.EAGAIN:
542 if e.errno == zmq.EAGAIN:
544 before_poll = time.time()
543 before_poll = time.time()
545 until_dead = self.time_to_dead - (before_poll -
544 until_dead = self.time_to_dead - (before_poll -
546 request_time)
545 request_time)
547
546
548 # When the return value of poll() is an empty list,
547 # When the return value of poll() is an empty list,
549 # that is when things have gone wrong (zeromq bug).
548 # that is when things have gone wrong (zeromq bug).
550 # As long as it is not an empty list, poll is
549 # As long as it is not an empty list, poll is
551 # working correctly even if it returns quickly.
550 # working correctly even if it returns quickly.
552 # Note: poll timeout is in milliseconds.
551 # Note: poll timeout is in milliseconds.
553 self.poller.poll(1000*until_dead)
552 self.poller.poll(1000*until_dead)
554
553
555 since_last_heartbeat = time.time() - request_time
554 since_last_heartbeat = time.time() - request_time
556 if since_last_heartbeat > self.time_to_dead:
555 if since_last_heartbeat > self.time_to_dead:
557 self.call_handlers(since_last_heartbeat)
556 self.call_handlers(since_last_heartbeat)
558 break
557 break
559 else:
558 else:
560 # FIXME: We should probably log this instead.
559 # FIXME: We should probably log this instead.
561 raise
560 raise
562 else:
561 else:
563 until_dead = self.time_to_dead - (time.time() -
562 until_dead = self.time_to_dead - (time.time() -
564 request_time)
563 request_time)
565 if until_dead > 0.0:
564 if until_dead > 0.0:
566 #io.rprint('sleep...', self.time_to_dead) # dbg
565 #io.rprint('sleep...', self.time_to_dead) # dbg
567 time.sleep(until_dead)
566 time.sleep(until_dead)
568 break
567 break
569
568
570 def stop(self):
569 def stop(self):
571 self._running = False
570 self._running = False
572 super(HBSocketChannel, self).stop()
571 super(HBSocketChannel, self).stop()
573
572
574 def call_handlers(self, since_last_heartbeat):
573 def call_handlers(self, since_last_heartbeat):
575 """This method is called in the ioloop thread when a message arrives.
574 """This method is called in the ioloop thread when a message arrives.
576
575
577 Subclasses should override this method to handle incoming messages.
576 Subclasses should override this method to handle incoming messages.
578 It is important to remember that this method is called in the thread
577 It is important to remember that this method is called in the thread
579 so that some logic must be done to ensure that the application leve
578 so that some logic must be done to ensure that the application leve
580 handlers are called in the application thread.
579 handlers are called in the application thread.
581 """
580 """
582 raise NotImplementedError('call_handlers must be defined in a subclass.')
581 raise NotImplementedError('call_handlers must be defined in a subclass.')
583
582
584
583
585 #-----------------------------------------------------------------------------
584 #-----------------------------------------------------------------------------
586 # Main kernel manager class
585 # Main kernel manager class
587 #-----------------------------------------------------------------------------
586 #-----------------------------------------------------------------------------
588
587
589 class KernelManager(HasTraits):
588 class KernelManager(HasTraits):
590 """ Manages a kernel for a frontend.
589 """ Manages a kernel for a frontend.
591
590
592 The SUB channel is for the frontend to receive messages published by the
591 The SUB channel is for the frontend to receive messages published by the
593 kernel.
592 kernel.
594
593
595 The REQ channel is for the frontend to make requests of the kernel.
594 The REQ channel is for the frontend to make requests of the kernel.
596
595
597 The REP channel is for the kernel to request stdin (raw_input) from the
596 The REP channel is for the kernel to request stdin (raw_input) from the
598 frontend.
597 frontend.
599 """
598 """
600 # The PyZMQ Context to use for communication with the kernel.
599 # The PyZMQ Context to use for communication with the kernel.
601 context = Instance(zmq.Context,(),{})
600 context = Instance(zmq.Context,(),{})
602
601
603 # The Session to use for communication with the kernel.
602 # The Session to use for communication with the kernel.
604 session = Instance(Session,(),{})
603 session = Instance(Session,(),{})
605
604
606 # The kernel process with which the KernelManager is communicating.
605 # The kernel process with which the KernelManager is communicating.
607 kernel = Instance(Popen)
606 kernel = Instance(Popen)
608
607
609 # The addresses for the communication channels.
608 # The addresses for the communication channels.
610 xreq_address = TCPAddress((LOCALHOST, 0))
609 xreq_address = TCPAddress((LOCALHOST, 0))
611 sub_address = TCPAddress((LOCALHOST, 0))
610 sub_address = TCPAddress((LOCALHOST, 0))
612 rep_address = TCPAddress((LOCALHOST, 0))
611 rep_address = TCPAddress((LOCALHOST, 0))
613 hb_address = TCPAddress((LOCALHOST, 0))
612 hb_address = TCPAddress((LOCALHOST, 0))
614
613
615 # The classes to use for the various channels.
614 # The classes to use for the various channels.
616 xreq_channel_class = Type(XReqSocketChannel)
615 xreq_channel_class = Type(XReqSocketChannel)
617 sub_channel_class = Type(SubSocketChannel)
616 sub_channel_class = Type(SubSocketChannel)
618 rep_channel_class = Type(RepSocketChannel)
617 rep_channel_class = Type(RepSocketChannel)
619 hb_channel_class = Type(HBSocketChannel)
618 hb_channel_class = Type(HBSocketChannel)
620
619
621 # Protected traits.
620 # Protected traits.
622 _launch_args = Any
621 _launch_args = Any
623 _xreq_channel = Any
622 _xreq_channel = Any
624 _sub_channel = Any
623 _sub_channel = Any
625 _rep_channel = Any
624 _rep_channel = Any
626 _hb_channel = Any
625 _hb_channel = Any
627
626
628 #--------------------------------------------------------------------------
627 #--------------------------------------------------------------------------
629 # Channel management methods:
628 # Channel management methods:
630 #--------------------------------------------------------------------------
629 #--------------------------------------------------------------------------
631
630
632 def start_channels(self):
631 def start_channels(self):
633 """Starts the channels for this kernel.
632 """Starts the channels for this kernel.
634
633
635 This will create the channels if they do not exist and then start
634 This will create the channels if they do not exist and then start
636 them. If port numbers of 0 are being used (random ports) then you
635 them. If port numbers of 0 are being used (random ports) then you
637 must first call :method:`start_kernel`. If the channels have been
636 must first call :method:`start_kernel`. If the channels have been
638 stopped and you call this, :class:`RuntimeError` will be raised.
637 stopped and you call this, :class:`RuntimeError` will be raised.
639 """
638 """
640 self.xreq_channel.start()
639 self.xreq_channel.start()
641 self.sub_channel.start()
640 self.sub_channel.start()
642 self.rep_channel.start()
641 self.rep_channel.start()
643 self.hb_channel.start()
642 self.hb_channel.start()
644
643
645 def stop_channels(self):
644 def stop_channels(self):
646 """Stops the channels for this kernel.
645 """Stops the channels for this kernel.
647
646
648 This stops the channels by joining their threads. If the channels
647 This stops the channels by joining their threads. If the channels
649 were not started, :class:`RuntimeError` will be raised.
648 were not started, :class:`RuntimeError` will be raised.
650 """
649 """
651 self.xreq_channel.stop()
650 self.xreq_channel.stop()
652 self.sub_channel.stop()
651 self.sub_channel.stop()
653 self.rep_channel.stop()
652 self.rep_channel.stop()
654 self.hb_channel.stop()
653 self.hb_channel.stop()
655
654
656 @property
655 @property
657 def channels_running(self):
656 def channels_running(self):
658 """Are all of the channels created and running?"""
657 """Are all of the channels created and running?"""
659 return self.xreq_channel.is_alive() \
658 return self.xreq_channel.is_alive() \
660 and self.sub_channel.is_alive() \
659 and self.sub_channel.is_alive() \
661 and self.rep_channel.is_alive() \
660 and self.rep_channel.is_alive() \
662 and self.hb_channel.is_alive()
661 and self.hb_channel.is_alive()
663
662
664 #--------------------------------------------------------------------------
663 #--------------------------------------------------------------------------
665 # Kernel process management methods:
664 # Kernel process management methods:
666 #--------------------------------------------------------------------------
665 #--------------------------------------------------------------------------
667
666
668 def start_kernel(self, **kw):
667 def start_kernel(self, **kw):
669 """Starts a kernel process and configures the manager to use it.
668 """Starts a kernel process and configures the manager to use it.
670
669
671 If random ports (port=0) are being used, this method must be called
670 If random ports (port=0) are being used, this method must be called
672 before the channels are created.
671 before the channels are created.
673
672
674 Parameters:
673 Parameters:
675 -----------
674 -----------
676 ipython : bool, optional (default True)
675 ipython : bool, optional (default True)
677 Whether to use an IPython kernel instead of a plain Python kernel.
676 Whether to use an IPython kernel instead of a plain Python kernel.
678 """
677 """
679 xreq, sub, rep, hb = self.xreq_address, self.sub_address, \
678 xreq, sub, rep, hb = self.xreq_address, self.sub_address, \
680 self.rep_address, self.hb_address
679 self.rep_address, self.hb_address
681 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or \
680 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or \
682 rep[0] != LOCALHOST or hb[0] != LOCALHOST:
681 rep[0] != LOCALHOST or hb[0] != LOCALHOST:
683 raise RuntimeError("Can only launch a kernel on localhost."
682 raise RuntimeError("Can only launch a kernel on localhost."
684 "Make sure that the '*_address' attributes are "
683 "Make sure that the '*_address' attributes are "
685 "configured properly.")
684 "configured properly.")
686
685
687 self._launch_args = kw.copy()
686 self._launch_args = kw.copy()
688 if kw.pop('ipython', True):
687 if kw.pop('ipython', True):
689 from ipkernel import launch_kernel as launch
688 from ipkernel import launch_kernel as launch
690 else:
689 else:
691 from pykernel import launch_kernel as launch
690 from pykernel import launch_kernel as launch
692 self.kernel, xrep, pub, req, hb = launch(
691 self.kernel, xrep, pub, req, hb = launch(
693 xrep_port=xreq[1], pub_port=sub[1],
692 xrep_port=xreq[1], pub_port=sub[1],
694 req_port=rep[1], hb_port=hb[1], **kw)
693 req_port=rep[1], hb_port=hb[1], **kw)
695 self.xreq_address = (LOCALHOST, xrep)
694 self.xreq_address = (LOCALHOST, xrep)
696 self.sub_address = (LOCALHOST, pub)
695 self.sub_address = (LOCALHOST, pub)
697 self.rep_address = (LOCALHOST, req)
696 self.rep_address = (LOCALHOST, req)
698 self.hb_address = (LOCALHOST, hb)
697 self.hb_address = (LOCALHOST, hb)
699
698
700 def restart_kernel(self):
699 def restart_kernel(self):
701 """Restarts a kernel with the same arguments that were used to launch
700 """Restarts a kernel with the same arguments that were used to launch
702 it. If the old kernel was launched with random ports, the same ports
701 it. If the old kernel was launched with random ports, the same ports
703 will be used for the new kernel.
702 will be used for the new kernel.
704 """
703 """
705 if self._launch_args is None:
704 if self._launch_args is None:
706 raise RuntimeError("Cannot restart the kernel. "
705 raise RuntimeError("Cannot restart the kernel. "
707 "No previous call to 'start_kernel'.")
706 "No previous call to 'start_kernel'.")
708 else:
707 else:
709 if self.has_kernel:
708 if self.has_kernel:
710 self.kill_kernel()
709 self.kill_kernel()
711 self.start_kernel(**self._launch_args)
710 self.start_kernel(**self._launch_args)
712
711
713 @property
712 @property
714 def has_kernel(self):
713 def has_kernel(self):
715 """Returns whether a kernel process has been specified for the kernel
714 """Returns whether a kernel process has been specified for the kernel
716 manager.
715 manager.
717 """
716 """
718 return self.kernel is not None
717 return self.kernel is not None
719
718
720 def kill_kernel(self):
719 def kill_kernel(self):
721 """ Kill the running kernel. """
720 """ Kill the running kernel. """
722 if self.kernel is not None:
721 if self.kernel is not None:
723 self.kernel.kill()
722 self.kernel.kill()
724 self.kernel = None
723 self.kernel = None
725 else:
724 else:
726 raise RuntimeError("Cannot kill kernel. No kernel is running!")
725 raise RuntimeError("Cannot kill kernel. No kernel is running!")
727
726
728 def signal_kernel(self, signum):
727 def signal_kernel(self, signum):
729 """ Sends a signal to the kernel. """
728 """ Sends a signal to the kernel. """
730 if self.kernel is not None:
729 if self.kernel is not None:
731 self.kernel.send_signal(signum)
730 self.kernel.send_signal(signum)
732 else:
731 else:
733 raise RuntimeError("Cannot signal kernel. No kernel is running!")
732 raise RuntimeError("Cannot signal kernel. No kernel is running!")
734
733
735 @property
734 @property
736 def is_alive(self):
735 def is_alive(self):
737 """Is the kernel process still running?"""
736 """Is the kernel process still running?"""
738 if self.kernel is not None:
737 if self.kernel is not None:
739 if self.kernel.poll() is None:
738 if self.kernel.poll() is None:
740 return True
739 return True
741 else:
740 else:
742 return False
741 return False
743 else:
742 else:
744 # We didn't start the kernel with this KernelManager so we don't
743 # We didn't start the kernel with this KernelManager so we don't
745 # know if it is running. We should use a heartbeat for this case.
744 # know if it is running. We should use a heartbeat for this case.
746 return True
745 return True
747
746
748 #--------------------------------------------------------------------------
747 #--------------------------------------------------------------------------
749 # Channels used for communication with the kernel:
748 # Channels used for communication with the kernel:
750 #--------------------------------------------------------------------------
749 #--------------------------------------------------------------------------
751
750
752 @property
751 @property
753 def xreq_channel(self):
752 def xreq_channel(self):
754 """Get the REQ socket channel object to make requests of the kernel."""
753 """Get the REQ socket channel object to make requests of the kernel."""
755 if self._xreq_channel is None:
754 if self._xreq_channel is None:
756 self._xreq_channel = self.xreq_channel_class(self.context,
755 self._xreq_channel = self.xreq_channel_class(self.context,
757 self.session,
756 self.session,
758 self.xreq_address)
757 self.xreq_address)
759 return self._xreq_channel
758 return self._xreq_channel
760
759
761 @property
760 @property
762 def sub_channel(self):
761 def sub_channel(self):
763 """Get the SUB socket channel object."""
762 """Get the SUB socket channel object."""
764 if self._sub_channel is None:
763 if self._sub_channel is None:
765 self._sub_channel = self.sub_channel_class(self.context,
764 self._sub_channel = self.sub_channel_class(self.context,
766 self.session,
765 self.session,
767 self.sub_address)
766 self.sub_address)
768 return self._sub_channel
767 return self._sub_channel
769
768
770 @property
769 @property
771 def rep_channel(self):
770 def rep_channel(self):
772 """Get the REP socket channel object to handle stdin (raw_input)."""
771 """Get the REP socket channel object to handle stdin (raw_input)."""
773 if self._rep_channel is None:
772 if self._rep_channel is None:
774 self._rep_channel = self.rep_channel_class(self.context,
773 self._rep_channel = self.rep_channel_class(self.context,
775 self.session,
774 self.session,
776 self.rep_address)
775 self.rep_address)
777 return self._rep_channel
776 return self._rep_channel
778
777
779 @property
778 @property
780 def hb_channel(self):
779 def hb_channel(self):
781 """Get the REP socket channel object to handle stdin (raw_input)."""
780 """Get the REP socket channel object to handle stdin (raw_input)."""
782 if self._hb_channel is None:
781 if self._hb_channel is None:
783 self._hb_channel = self.hb_channel_class(self.context,
782 self._hb_channel = self.hb_channel_class(self.context,
784 self.session,
783 self.session,
785 self.hb_address)
784 self.hb_address)
786 return self._hb_channel
785 return self._hb_channel
General Comments 0
You need to be logged in to leave comments. Login now