##// END OF EJS Templates
cleanup channel names to match function not socket...
MinRK -
Show More
@@ -1,109 +1,109 b''
1 1 """ Defines a convenient mix-in class for implementing Qt frontends.
2 2 """
3 3
4 4 class BaseFrontendMixin(object):
5 5 """ A mix-in class for implementing Qt frontends.
6 6
7 7 To handle messages of a particular type, frontends need only define an
8 8 appropriate handler method. For example, to handle 'stream' messaged, define
9 9 a '_handle_stream(msg)' method.
10 10 """
11 11
12 12 #---------------------------------------------------------------------------
13 13 # 'BaseFrontendMixin' concrete interface
14 14 #---------------------------------------------------------------------------
15 15
16 16 def _get_kernel_manager(self):
17 17 """ Returns the current kernel manager.
18 18 """
19 19 return self._kernel_manager
20 20
21 21 def _set_kernel_manager(self, kernel_manager):
22 22 """ Disconnect from the current kernel manager (if any) and set a new
23 23 kernel manager.
24 24 """
25 25 # Disconnect the old kernel manager, if necessary.
26 26 old_manager = self._kernel_manager
27 27 if old_manager is not None:
28 28 old_manager.started_channels.disconnect(self._started_channels)
29 29 old_manager.stopped_channels.disconnect(self._stopped_channels)
30 30
31 31 # Disconnect the old kernel manager's channels.
32 32 old_manager.sub_channel.message_received.disconnect(self._dispatch)
33 old_manager.xreq_channel.message_received.disconnect(self._dispatch)
34 old_manager.rep_channel.message_received.disconnect(self._dispatch)
33 old_manager.shell_channel.message_received.disconnect(self._dispatch)
34 old_manager.stdin_channel.message_received.disconnect(self._dispatch)
35 35 old_manager.hb_channel.kernel_died.disconnect(
36 36 self._handle_kernel_died)
37 37
38 38 # Handle the case where the old kernel manager is still listening.
39 39 if old_manager.channels_running:
40 40 self._stopped_channels()
41 41
42 42 # Set the new kernel manager.
43 43 self._kernel_manager = kernel_manager
44 44 if kernel_manager is None:
45 45 return
46 46
47 47 # Connect the new kernel manager.
48 48 kernel_manager.started_channels.connect(self._started_channels)
49 49 kernel_manager.stopped_channels.connect(self._stopped_channels)
50 50
51 51 # Connect the new kernel manager's channels.
52 52 kernel_manager.sub_channel.message_received.connect(self._dispatch)
53 kernel_manager.xreq_channel.message_received.connect(self._dispatch)
54 kernel_manager.rep_channel.message_received.connect(self._dispatch)
53 kernel_manager.shell_channel.message_received.connect(self._dispatch)
54 kernel_manager.stdin_channel.message_received.connect(self._dispatch)
55 55 kernel_manager.hb_channel.kernel_died.connect(self._handle_kernel_died)
56 56
57 57 # Handle the case where the kernel manager started channels before
58 58 # we connected.
59 59 if kernel_manager.channels_running:
60 60 self._started_channels()
61 61
62 62 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
63 63
64 64 #---------------------------------------------------------------------------
65 65 # 'BaseFrontendMixin' abstract interface
66 66 #---------------------------------------------------------------------------
67 67
68 68 def _handle_kernel_died(self, since_last_heartbeat):
69 69 """ This is called when the ``kernel_died`` signal is emitted.
70 70
71 71 This method is called when the kernel heartbeat has not been
72 72 active for a certain amount of time. The typical action will be to
73 73 give the user the option of restarting the kernel.
74 74
75 75 Parameters
76 76 ----------
77 77 since_last_heartbeat : float
78 78 The time since the heartbeat was last received.
79 79 """
80 80
81 81 def _started_channels(self):
82 82 """ Called when the KernelManager channels have started listening or
83 83 when the frontend is assigned an already listening KernelManager.
84 84 """
85 85
86 86 def _stopped_channels(self):
87 87 """ Called when the KernelManager channels have stopped listening or
88 88 when a listening KernelManager is removed from the frontend.
89 89 """
90 90
91 91 #---------------------------------------------------------------------------
92 92 # 'BaseFrontendMixin' protected interface
93 93 #---------------------------------------------------------------------------
94 94
95 95 def _dispatch(self, msg):
96 96 """ Calls the frontend handler associated with the message type of the
97 97 given message.
98 98 """
99 99 msg_type = msg['msg_type']
100 100 handler = getattr(self, '_handle_' + msg_type, None)
101 101 if handler:
102 102 handler(msg)
103 103
104 104 def _is_from_this_session(self, msg):
105 105 """ Returns whether a reply from the kernel originated from a request
106 106 from this frontend.
107 107 """
108 108 session = self._kernel_manager.session.session
109 109 return msg['parent_header']['session'] == session
@@ -1,626 +1,626 b''
1 1 from __future__ import print_function
2 2
3 3 # Standard library imports
4 4 from collections import namedtuple
5 5 import sys
6 6 import time
7 7
8 8 # System library imports
9 9 from pygments.lexers import PythonLexer
10 10 from IPython.external.qt import QtCore, QtGui
11 11
12 12 # Local imports
13 13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
14 14 from IPython.core.oinspect import call_tip
15 15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
16 16 from IPython.utils.traitlets import Bool, Instance
17 17 from bracket_matcher import BracketMatcher
18 18 from call_tip_widget import CallTipWidget
19 19 from completion_lexer import CompletionLexer
20 20 from history_console_widget import HistoryConsoleWidget
21 21 from pygments_highlighter import PygmentsHighlighter
22 22
23 23
24 24 class FrontendHighlighter(PygmentsHighlighter):
25 25 """ A PygmentsHighlighter that can be turned on and off and that ignores
26 26 prompts.
27 27 """
28 28
29 29 def __init__(self, frontend):
30 30 super(FrontendHighlighter, self).__init__(frontend._control.document())
31 31 self._current_offset = 0
32 32 self._frontend = frontend
33 33 self.highlighting_on = False
34 34
35 35 def highlightBlock(self, string):
36 36 """ Highlight a block of text. Reimplemented to highlight selectively.
37 37 """
38 38 if not self.highlighting_on:
39 39 return
40 40
41 41 # The input to this function is a unicode string that may contain
42 42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
43 43 # the string as plain text so we can compare it.
44 44 current_block = self.currentBlock()
45 45 string = self._frontend._get_block_plain_text(current_block)
46 46
47 47 # Decide whether to check for the regular or continuation prompt.
48 48 if current_block.contains(self._frontend._prompt_pos):
49 49 prompt = self._frontend._prompt
50 50 else:
51 51 prompt = self._frontend._continuation_prompt
52 52
53 53 # Don't highlight the part of the string that contains the prompt.
54 54 if string.startswith(prompt):
55 55 self._current_offset = len(prompt)
56 56 string = string[len(prompt):]
57 57 else:
58 58 self._current_offset = 0
59 59
60 60 PygmentsHighlighter.highlightBlock(self, string)
61 61
62 62 def rehighlightBlock(self, block):
63 63 """ Reimplemented to temporarily enable highlighting if disabled.
64 64 """
65 65 old = self.highlighting_on
66 66 self.highlighting_on = True
67 67 super(FrontendHighlighter, self).rehighlightBlock(block)
68 68 self.highlighting_on = old
69 69
70 70 def setFormat(self, start, count, format):
71 71 """ Reimplemented to highlight selectively.
72 72 """
73 73 start += self._current_offset
74 74 PygmentsHighlighter.setFormat(self, start, count, format)
75 75
76 76
77 77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 78 """ A Qt frontend for a generic Python kernel.
79 79 """
80 80
81 81 # An option and corresponding signal for overriding the default kernel
82 82 # interrupt behavior.
83 83 custom_interrupt = Bool(False)
84 84 custom_interrupt_requested = QtCore.Signal()
85 85
86 86 # An option and corresponding signals for overriding the default kernel
87 87 # restart behavior.
88 88 custom_restart = Bool(False)
89 89 custom_restart_kernel_died = QtCore.Signal(float)
90 90 custom_restart_requested = QtCore.Signal()
91 91
92 92 # Emitted when a user visible 'execute_request' has been submitted to the
93 93 # kernel from the FrontendWidget. Contains the code to be executed.
94 94 executing = QtCore.Signal(object)
95 95
96 96 # Emitted when a user-visible 'execute_reply' has been received from the
97 97 # kernel and processed by the FrontendWidget. Contains the response message.
98 98 executed = QtCore.Signal(object)
99 99
100 100 # Emitted when an exit request has been received from the kernel.
101 101 exit_requested = QtCore.Signal()
102 102
103 103 # Protected class variables.
104 104 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
105 105 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
106 106 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
107 107 _input_splitter_class = InputSplitter
108 108 _local_kernel = False
109 109 _highlighter = Instance(FrontendHighlighter)
110 110
111 111 #---------------------------------------------------------------------------
112 112 # 'object' interface
113 113 #---------------------------------------------------------------------------
114 114
115 115 def __init__(self, *args, **kw):
116 116 super(FrontendWidget, self).__init__(*args, **kw)
117 117
118 118 # FrontendWidget protected variables.
119 119 self._bracket_matcher = BracketMatcher(self._control)
120 120 self._call_tip_widget = CallTipWidget(self._control)
121 121 self._completion_lexer = CompletionLexer(PythonLexer())
122 122 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
123 123 self._hidden = False
124 124 self._highlighter = FrontendHighlighter(self)
125 125 self._input_splitter = self._input_splitter_class(input_mode='cell')
126 126 self._kernel_manager = None
127 127 self._request_info = {}
128 128
129 129 # Configure the ConsoleWidget.
130 130 self.tab_width = 4
131 131 self._set_continuation_prompt('... ')
132 132
133 133 # Configure the CallTipWidget.
134 134 self._call_tip_widget.setFont(self.font)
135 135 self.font_changed.connect(self._call_tip_widget.setFont)
136 136
137 137 # Configure actions.
138 138 action = self._copy_raw_action
139 139 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
140 140 action.setEnabled(False)
141 141 action.setShortcut(QtGui.QKeySequence(key))
142 142 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
143 143 action.triggered.connect(self.copy_raw)
144 144 self.copy_available.connect(action.setEnabled)
145 145 self.addAction(action)
146 146
147 147 # Connect signal handlers.
148 148 document = self._control.document()
149 149 document.contentsChange.connect(self._document_contents_change)
150 150
151 151 # Set flag for whether we are connected via localhost.
152 152 self._local_kernel = kw.get('local_kernel',
153 153 FrontendWidget._local_kernel)
154 154
155 155 #---------------------------------------------------------------------------
156 156 # 'ConsoleWidget' public interface
157 157 #---------------------------------------------------------------------------
158 158
159 159 def copy(self):
160 160 """ Copy the currently selected text to the clipboard, removing prompts.
161 161 """
162 162 text = self._control.textCursor().selection().toPlainText()
163 163 if text:
164 164 lines = map(transform_classic_prompt, text.splitlines())
165 165 text = '\n'.join(lines)
166 166 QtGui.QApplication.clipboard().setText(text)
167 167
168 168 #---------------------------------------------------------------------------
169 169 # 'ConsoleWidget' abstract interface
170 170 #---------------------------------------------------------------------------
171 171
172 172 def _is_complete(self, source, interactive):
173 173 """ Returns whether 'source' can be completely processed and a new
174 174 prompt created. When triggered by an Enter/Return key press,
175 175 'interactive' is True; otherwise, it is False.
176 176 """
177 177 complete = self._input_splitter.push(source)
178 178 if interactive:
179 179 complete = not self._input_splitter.push_accepts_more()
180 180 return complete
181 181
182 182 def _execute(self, source, hidden):
183 183 """ Execute 'source'. If 'hidden', do not show any output.
184 184
185 185 See parent class :meth:`execute` docstring for full details.
186 186 """
187 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
187 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
188 188 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
189 189 self._hidden = hidden
190 190 if not hidden:
191 191 self.executing.emit(source)
192 192
193 193 def _prompt_started_hook(self):
194 194 """ Called immediately after a new prompt is displayed.
195 195 """
196 196 if not self._reading:
197 197 self._highlighter.highlighting_on = True
198 198
199 199 def _prompt_finished_hook(self):
200 200 """ Called immediately after a prompt is finished, i.e. when some input
201 201 will be processed and a new prompt displayed.
202 202 """
203 203 # Flush all state from the input splitter so the next round of
204 204 # reading input starts with a clean buffer.
205 205 self._input_splitter.reset()
206 206
207 207 if not self._reading:
208 208 self._highlighter.highlighting_on = False
209 209
210 210 def _tab_pressed(self):
211 211 """ Called when the tab key is pressed. Returns whether to continue
212 212 processing the event.
213 213 """
214 214 # Perform tab completion if:
215 215 # 1) The cursor is in the input buffer.
216 216 # 2) There is a non-whitespace character before the cursor.
217 217 text = self._get_input_buffer_cursor_line()
218 218 if text is None:
219 219 return False
220 220 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
221 221 if complete:
222 222 self._complete()
223 223 return not complete
224 224
225 225 #---------------------------------------------------------------------------
226 226 # 'ConsoleWidget' protected interface
227 227 #---------------------------------------------------------------------------
228 228
229 229 def _context_menu_make(self, pos):
230 230 """ Reimplemented to add an action for raw copy.
231 231 """
232 232 menu = super(FrontendWidget, self)._context_menu_make(pos)
233 233 for before_action in menu.actions():
234 234 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
235 235 QtGui.QKeySequence.ExactMatch:
236 236 menu.insertAction(before_action, self._copy_raw_action)
237 237 break
238 238 return menu
239 239
240 240 def _event_filter_console_keypress(self, event):
241 241 """ Reimplemented for execution interruption and smart backspace.
242 242 """
243 243 key = event.key()
244 244 if self._control_key_down(event.modifiers(), include_command=False):
245 245
246 246 if key == QtCore.Qt.Key_C and self._executing:
247 247 self.interrupt_kernel()
248 248 return True
249 249
250 250 elif key == QtCore.Qt.Key_Period:
251 251 message = 'Are you sure you want to restart the kernel?'
252 252 self.restart_kernel(message, now=False)
253 253 return True
254 254
255 255 elif not event.modifiers() & QtCore.Qt.AltModifier:
256 256
257 257 # Smart backspace: remove four characters in one backspace if:
258 258 # 1) everything left of the cursor is whitespace
259 259 # 2) the four characters immediately left of the cursor are spaces
260 260 if key == QtCore.Qt.Key_Backspace:
261 261 col = self._get_input_buffer_cursor_column()
262 262 cursor = self._control.textCursor()
263 263 if col > 3 and not cursor.hasSelection():
264 264 text = self._get_input_buffer_cursor_line()[:col]
265 265 if text.endswith(' ') and not text.strip():
266 266 cursor.movePosition(QtGui.QTextCursor.Left,
267 267 QtGui.QTextCursor.KeepAnchor, 4)
268 268 cursor.removeSelectedText()
269 269 return True
270 270
271 271 return super(FrontendWidget, self)._event_filter_console_keypress(event)
272 272
273 273 def _insert_continuation_prompt(self, cursor):
274 274 """ Reimplemented for auto-indentation.
275 275 """
276 276 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
277 277 cursor.insertText(' ' * self._input_splitter.indent_spaces)
278 278
279 279 #---------------------------------------------------------------------------
280 280 # 'BaseFrontendMixin' abstract interface
281 281 #---------------------------------------------------------------------------
282 282
283 283 def _handle_complete_reply(self, rep):
284 284 """ Handle replies for tab completion.
285 285 """
286 286 cursor = self._get_cursor()
287 287 info = self._request_info.get('complete')
288 288 if info and info.id == rep['parent_header']['msg_id'] and \
289 289 info.pos == cursor.position():
290 290 text = '.'.join(self._get_context())
291 291 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
292 292 self._complete_with_items(cursor, rep['content']['matches'])
293 293
294 294 def _handle_execute_reply(self, msg):
295 295 """ Handles replies for code execution.
296 296 """
297 297 info = self._request_info.get('execute')
298 298 if info and info.id == msg['parent_header']['msg_id'] and \
299 299 info.kind == 'user' and not self._hidden:
300 300 # Make sure that all output from the SUB channel has been processed
301 301 # before writing a new prompt.
302 302 self.kernel_manager.sub_channel.flush()
303 303
304 304 # Reset the ANSI style information to prevent bad text in stdout
305 305 # from messing up our colors. We're not a true terminal so we're
306 306 # allowed to do this.
307 307 if self.ansi_codes:
308 308 self._ansi_processor.reset_sgr()
309 309
310 310 content = msg['content']
311 311 status = content['status']
312 312 if status == 'ok':
313 313 self._process_execute_ok(msg)
314 314 elif status == 'error':
315 315 self._process_execute_error(msg)
316 316 elif status == 'abort':
317 317 self._process_execute_abort(msg)
318 318
319 319 self._show_interpreter_prompt_for_reply(msg)
320 320 self.executed.emit(msg)
321 321
322 322 def _handle_input_request(self, msg):
323 323 """ Handle requests for raw_input.
324 324 """
325 325 if self._hidden:
326 326 raise RuntimeError('Request for raw input during hidden execution.')
327 327
328 328 # Make sure that all output from the SUB channel has been processed
329 329 # before entering readline mode.
330 330 self.kernel_manager.sub_channel.flush()
331 331
332 332 def callback(line):
333 self.kernel_manager.rep_channel.input(line)
333 self.kernel_manager.stdin_channel.input(line)
334 334 self._readline(msg['content']['prompt'], callback=callback)
335 335
336 336 def _handle_kernel_died(self, since_last_heartbeat):
337 337 """ Handle the kernel's death by asking if the user wants to restart.
338 338 """
339 339 if self.custom_restart:
340 340 self.custom_restart_kernel_died.emit(since_last_heartbeat)
341 341 else:
342 342 message = 'The kernel heartbeat has been inactive for %.2f ' \
343 343 'seconds. Do you want to restart the kernel? You may ' \
344 344 'first want to check the network connection.' % \
345 345 since_last_heartbeat
346 346 self.restart_kernel(message, now=True)
347 347
348 348 def _handle_object_info_reply(self, rep):
349 349 """ Handle replies for call tips.
350 350 """
351 351 cursor = self._get_cursor()
352 352 info = self._request_info.get('call_tip')
353 353 if info and info.id == rep['parent_header']['msg_id'] and \
354 354 info.pos == cursor.position():
355 355 # Get the information for a call tip. For now we format the call
356 356 # line as string, later we can pass False to format_call and
357 357 # syntax-highlight it ourselves for nicer formatting in the
358 358 # calltip.
359 359 content = rep['content']
360 360 # if this is from pykernel, 'docstring' will be the only key
361 361 if content.get('ismagic', False):
362 362 # Don't generate a call-tip for magics. Ideally, we should
363 363 # generate a tooltip, but not on ( like we do for actual
364 364 # callables.
365 365 call_info, doc = None, None
366 366 else:
367 367 call_info, doc = call_tip(content, format_call=True)
368 368 if call_info or doc:
369 369 self._call_tip_widget.show_call_info(call_info, doc)
370 370
371 371 def _handle_pyout(self, msg):
372 372 """ Handle display hook output.
373 373 """
374 374 if not self._hidden and self._is_from_this_session(msg):
375 375 data = msg['content']['data']
376 376 if isinstance(data, basestring):
377 377 # plaintext data from pure Python kernel
378 378 text = data
379 379 else:
380 380 # formatted output from DisplayFormatter (IPython kernel)
381 381 text = data.get('text/plain', '')
382 382 self._append_plain_text(text + '\n')
383 383
384 384 def _handle_stream(self, msg):
385 385 """ Handle stdout, stderr, and stdin.
386 386 """
387 387 if not self._hidden and self._is_from_this_session(msg):
388 388 # Most consoles treat tabs as being 8 space characters. Convert tabs
389 389 # to spaces so that output looks as expected regardless of this
390 390 # widget's tab width.
391 391 text = msg['content']['data'].expandtabs(8)
392 392
393 393 self._append_plain_text(text)
394 394 self._control.moveCursor(QtGui.QTextCursor.End)
395 395
396 396 def _handle_shutdown_reply(self, msg):
397 397 """ Handle shutdown signal, only if from other console.
398 398 """
399 399 if not self._hidden and not self._is_from_this_session(msg):
400 400 if self._local_kernel:
401 401 if not msg['content']['restart']:
402 402 sys.exit(0)
403 403 else:
404 404 # we just got notified of a restart!
405 405 time.sleep(0.25) # wait 1/4 sec to reset
406 406 # lest the request for a new prompt
407 407 # goes to the old kernel
408 408 self.reset()
409 409 else: # remote kernel, prompt on Kernel shutdown/reset
410 410 title = self.window().windowTitle()
411 411 if not msg['content']['restart']:
412 412 reply = QtGui.QMessageBox.question(self, title,
413 413 "Kernel has been shutdown permanently. "
414 414 "Close the Console?",
415 415 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
416 416 if reply == QtGui.QMessageBox.Yes:
417 417 sys.exit(0)
418 418 else:
419 419 reply = QtGui.QMessageBox.question(self, title,
420 420 "Kernel has been reset. Clear the Console?",
421 421 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
422 422 if reply == QtGui.QMessageBox.Yes:
423 423 time.sleep(0.25) # wait 1/4 sec to reset
424 424 # lest the request for a new prompt
425 425 # goes to the old kernel
426 426 self.reset()
427 427
428 428 def _started_channels(self):
429 429 """ Called when the KernelManager channels have started listening or
430 430 when the frontend is assigned an already listening KernelManager.
431 431 """
432 432 self.reset()
433 433
434 434 #---------------------------------------------------------------------------
435 435 # 'FrontendWidget' public interface
436 436 #---------------------------------------------------------------------------
437 437
438 438 def copy_raw(self):
439 439 """ Copy the currently selected text to the clipboard without attempting
440 440 to remove prompts or otherwise alter the text.
441 441 """
442 442 self._control.copy()
443 443
444 444 def execute_file(self, path, hidden=False):
445 445 """ Attempts to execute file with 'path'. If 'hidden', no output is
446 446 shown.
447 447 """
448 448 self.execute('execfile(%r)' % path, hidden=hidden)
449 449
450 450 def interrupt_kernel(self):
451 451 """ Attempts to interrupt the running kernel.
452 452 """
453 453 if self.custom_interrupt:
454 454 self.custom_interrupt_requested.emit()
455 455 elif self.kernel_manager.has_kernel:
456 456 self.kernel_manager.interrupt_kernel()
457 457 else:
458 458 self._append_plain_text('Kernel process is either remote or '
459 459 'unspecified. Cannot interrupt.\n')
460 460
461 461 def reset(self):
462 462 """ Resets the widget to its initial state. Similar to ``clear``, but
463 463 also re-writes the banner and aborts execution if necessary.
464 464 """
465 465 if self._executing:
466 466 self._executing = False
467 467 self._request_info['execute'] = None
468 468 self._reading = False
469 469 self._highlighter.highlighting_on = False
470 470
471 471 self._control.clear()
472 472 self._append_plain_text(self._get_banner())
473 473 self._show_interpreter_prompt()
474 474
475 475 def restart_kernel(self, message, now=False):
476 476 """ Attempts to restart the running kernel.
477 477 """
478 478 # FIXME: now should be configurable via a checkbox in the dialog. Right
479 479 # now at least the heartbeat path sets it to True and the manual restart
480 480 # to False. But those should just be the pre-selected states of a
481 481 # checkbox that the user could override if so desired. But I don't know
482 482 # enough Qt to go implementing the checkbox now.
483 483
484 484 if self.custom_restart:
485 485 self.custom_restart_requested.emit()
486 486
487 487 elif self.kernel_manager.has_kernel:
488 488 # Pause the heart beat channel to prevent further warnings.
489 489 self.kernel_manager.hb_channel.pause()
490 490
491 491 # Prompt the user to restart the kernel. Un-pause the heartbeat if
492 492 # they decline. (If they accept, the heartbeat will be un-paused
493 493 # automatically when the kernel is restarted.)
494 494 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
495 495 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
496 496 message, buttons)
497 497 if result == QtGui.QMessageBox.Yes:
498 498 try:
499 499 self.kernel_manager.restart_kernel(now=now)
500 500 except RuntimeError:
501 501 self._append_plain_text('Kernel started externally. '
502 502 'Cannot restart.\n')
503 503 else:
504 504 self.reset()
505 505 else:
506 506 self.kernel_manager.hb_channel.unpause()
507 507
508 508 else:
509 509 self._append_plain_text('Kernel process is either remote or '
510 510 'unspecified. Cannot restart.\n')
511 511
512 512 #---------------------------------------------------------------------------
513 513 # 'FrontendWidget' protected interface
514 514 #---------------------------------------------------------------------------
515 515
516 516 def _call_tip(self):
517 517 """ Shows a call tip, if appropriate, at the current cursor location.
518 518 """
519 519 # Decide if it makes sense to show a call tip
520 520 cursor = self._get_cursor()
521 521 cursor.movePosition(QtGui.QTextCursor.Left)
522 522 if cursor.document().characterAt(cursor.position()) != '(':
523 523 return False
524 524 context = self._get_context(cursor)
525 525 if not context:
526 526 return False
527 527
528 528 # Send the metadata request to the kernel
529 529 name = '.'.join(context)
530 msg_id = self.kernel_manager.xreq_channel.object_info(name)
530 msg_id = self.kernel_manager.shell_channel.object_info(name)
531 531 pos = self._get_cursor().position()
532 532 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
533 533 return True
534 534
535 535 def _complete(self):
536 536 """ Performs completion at the current cursor location.
537 537 """
538 538 context = self._get_context()
539 539 if context:
540 540 # Send the completion request to the kernel
541 msg_id = self.kernel_manager.xreq_channel.complete(
541 msg_id = self.kernel_manager.shell_channel.complete(
542 542 '.'.join(context), # text
543 543 self._get_input_buffer_cursor_line(), # line
544 544 self._get_input_buffer_cursor_column(), # cursor_pos
545 545 self.input_buffer) # block
546 546 pos = self._get_cursor().position()
547 547 info = self._CompletionRequest(msg_id, pos)
548 548 self._request_info['complete'] = info
549 549
550 550 def _get_banner(self):
551 551 """ Gets a banner to display at the beginning of a session.
552 552 """
553 553 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
554 554 '"license" for more information.'
555 555 return banner % (sys.version, sys.platform)
556 556
557 557 def _get_context(self, cursor=None):
558 558 """ Gets the context for the specified cursor (or the current cursor
559 559 if none is specified).
560 560 """
561 561 if cursor is None:
562 562 cursor = self._get_cursor()
563 563 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
564 564 QtGui.QTextCursor.KeepAnchor)
565 565 text = cursor.selection().toPlainText()
566 566 return self._completion_lexer.get_context(text)
567 567
568 568 def _process_execute_abort(self, msg):
569 569 """ Process a reply for an aborted execution request.
570 570 """
571 571 self._append_plain_text("ERROR: execution aborted\n")
572 572
573 573 def _process_execute_error(self, msg):
574 574 """ Process a reply for an execution request that resulted in an error.
575 575 """
576 576 content = msg['content']
577 577 # If a SystemExit is passed along, this means exit() was called - also
578 578 # all the ipython %exit magic syntax of '-k' to be used to keep
579 579 # the kernel running
580 580 if content['ename']=='SystemExit':
581 581 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
582 582 self._keep_kernel_on_exit = keepkernel
583 583 self.exit_requested.emit()
584 584 else:
585 585 traceback = ''.join(content['traceback'])
586 586 self._append_plain_text(traceback)
587 587
588 588 def _process_execute_ok(self, msg):
589 589 """ Process a reply for a successful execution equest.
590 590 """
591 591 payload = msg['content']['payload']
592 592 for item in payload:
593 593 if not self._process_execute_payload(item):
594 594 warning = 'Warning: received unknown payload of type %s'
595 595 print(warning % repr(item['source']))
596 596
597 597 def _process_execute_payload(self, item):
598 598 """ Process a single payload item from the list of payload items in an
599 599 execution reply. Returns whether the payload was handled.
600 600 """
601 601 # The basic FrontendWidget doesn't handle payloads, as they are a
602 602 # mechanism for going beyond the standard Python interpreter model.
603 603 return False
604 604
605 605 def _show_interpreter_prompt(self):
606 606 """ Shows a prompt for the interpreter.
607 607 """
608 608 self._show_prompt('>>> ')
609 609
610 610 def _show_interpreter_prompt_for_reply(self, msg):
611 611 """ Shows a prompt for the interpreter given an 'execute_reply' message.
612 612 """
613 613 self._show_interpreter_prompt()
614 614
615 615 #------ Signal handlers ----------------------------------------------------
616 616
617 617 def _document_contents_change(self, position, removed, added):
618 618 """ Called whenever the document's content changes. Display a call tip
619 619 if appropriate.
620 620 """
621 621 # Calculate where the cursor should be *after* the change:
622 622 position += added
623 623
624 624 document = self._control.document()
625 625 if position == self._get_cursor().position():
626 626 self._call_tip()
@@ -1,503 +1,503 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Imports
7 7 #-----------------------------------------------------------------------------
8 8
9 9 # Standard library imports
10 10 from collections import namedtuple
11 11 import os.path
12 12 import re
13 13 from subprocess import Popen
14 14 import sys
15 15 from textwrap import dedent
16 16
17 17 # System library imports
18 18 from IPython.external.qt import QtCore, QtGui
19 19
20 20 # Local imports
21 21 from IPython.core.inputsplitter import IPythonInputSplitter, \
22 22 transform_ipy_prompt
23 23 from IPython.core.usage import default_gui_banner
24 24 from IPython.utils.traitlets import Bool, Str, Unicode
25 25 from frontend_widget import FrontendWidget
26 26 import styles
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Constants
30 30 #-----------------------------------------------------------------------------
31 31
32 32 # Default strings to build and display input and output prompts (and separators
33 33 # in between)
34 34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 36 default_input_sep = '\n'
37 37 default_output_sep = ''
38 38 default_output_sep2 = ''
39 39
40 40 # Base path for most payload sources.
41 41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
42 42
43 43 #-----------------------------------------------------------------------------
44 44 # IPythonWidget class
45 45 #-----------------------------------------------------------------------------
46 46
47 47 class IPythonWidget(FrontendWidget):
48 48 """ A FrontendWidget for an IPython kernel.
49 49 """
50 50
51 51 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
52 52 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
53 53 # settings.
54 54 custom_edit = Bool(False)
55 55 custom_edit_requested = QtCore.Signal(object, object)
56 56
57 57 editor = Unicode('default', config=True,
58 58 help="""
59 59 A command for invoking a system text editor. If the string contains a
60 60 {filename} format specifier, it will be used. Otherwise, the filename will
61 61 be appended to the end the command.
62 62 """)
63 63
64 64 editor_line = Unicode(config=True,
65 65 help="""
66 66 The editor command to use when a specific line number is requested. The
67 67 string should contain two format specifiers: {line} and {filename}. If
68 68 this parameter is not specified, the line number option to the %edit magic
69 69 will be ignored.
70 70 """)
71 71
72 72 style_sheet = Unicode(config=True,
73 73 help="""
74 74 A CSS stylesheet. The stylesheet can contain classes for:
75 75 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
76 76 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
77 77 3. IPython: .error, .in-prompt, .out-prompt, etc
78 78 """)
79 79
80 80
81 81 syntax_style = Str(config=True,
82 82 help="""
83 83 If not empty, use this Pygments style for syntax highlighting. Otherwise,
84 84 the style sheet is queried for Pygments style information.
85 85 """)
86 86
87 87 # Prompts.
88 88 in_prompt = Str(default_in_prompt, config=True)
89 89 out_prompt = Str(default_out_prompt, config=True)
90 90 input_sep = Str(default_input_sep, config=True)
91 91 output_sep = Str(default_output_sep, config=True)
92 92 output_sep2 = Str(default_output_sep2, config=True)
93 93
94 94 # FrontendWidget protected class variables.
95 95 _input_splitter_class = IPythonInputSplitter
96 96
97 97 # IPythonWidget protected class variables.
98 98 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
99 99 _payload_source_edit = zmq_shell_source + '.edit_magic'
100 100 _payload_source_exit = zmq_shell_source + '.ask_exit'
101 101 _payload_source_next_input = zmq_shell_source + '.set_next_input'
102 102 _payload_source_page = 'IPython.zmq.page.page'
103 103
104 104 #---------------------------------------------------------------------------
105 105 # 'object' interface
106 106 #---------------------------------------------------------------------------
107 107
108 108 def __init__(self, *args, **kw):
109 109 super(IPythonWidget, self).__init__(*args, **kw)
110 110
111 111 # IPythonWidget protected variables.
112 112 self._payload_handlers = {
113 113 self._payload_source_edit : self._handle_payload_edit,
114 114 self._payload_source_exit : self._handle_payload_exit,
115 115 self._payload_source_page : self._handle_payload_page,
116 116 self._payload_source_next_input : self._handle_payload_next_input }
117 117 self._previous_prompt_obj = None
118 118 self._keep_kernel_on_exit = None
119 119
120 120 # Initialize widget styling.
121 121 if self.style_sheet:
122 122 self._style_sheet_changed()
123 123 self._syntax_style_changed()
124 124 else:
125 125 self.set_default_style()
126 126
127 127 #---------------------------------------------------------------------------
128 128 # 'BaseFrontendMixin' abstract interface
129 129 #---------------------------------------------------------------------------
130 130
131 131 def _handle_complete_reply(self, rep):
132 132 """ Reimplemented to support IPython's improved completion machinery.
133 133 """
134 134 cursor = self._get_cursor()
135 135 info = self._request_info.get('complete')
136 136 if info and info.id == rep['parent_header']['msg_id'] and \
137 137 info.pos == cursor.position():
138 138 matches = rep['content']['matches']
139 139 text = rep['content']['matched_text']
140 140 offset = len(text)
141 141
142 142 # Clean up matches with period and path separators if the matched
143 143 # text has not been transformed. This is done by truncating all
144 144 # but the last component and then suitably decreasing the offset
145 145 # between the current cursor position and the start of completion.
146 146 if len(matches) > 1 and matches[0][:offset] == text:
147 147 parts = re.split(r'[./\\]', text)
148 148 sep_count = len(parts) - 1
149 149 if sep_count:
150 150 chop_length = sum(map(len, parts[:sep_count])) + sep_count
151 151 matches = [ match[chop_length:] for match in matches ]
152 152 offset -= chop_length
153 153
154 154 # Move the cursor to the start of the match and complete.
155 155 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
156 156 self._complete_with_items(cursor, matches)
157 157
158 158 def _handle_execute_reply(self, msg):
159 159 """ Reimplemented to support prompt requests.
160 160 """
161 161 info = self._request_info.get('execute')
162 162 if info and info.id == msg['parent_header']['msg_id']:
163 163 if info.kind == 'prompt':
164 164 number = msg['content']['execution_count'] + 1
165 165 self._show_interpreter_prompt(number)
166 166 else:
167 167 super(IPythonWidget, self)._handle_execute_reply(msg)
168 168
169 169 def _handle_history_reply(self, msg):
170 170 """ Implemented to handle history tail replies, which are only supported
171 171 by the IPython kernel.
172 172 """
173 173 history_items = msg['content']['history']
174 174 items = [ line.rstrip() for _, _, line in history_items ]
175 175 self._set_history(items)
176 176
177 177 def _handle_pyout(self, msg):
178 178 """ Reimplemented for IPython-style "display hook".
179 179 """
180 180 if not self._hidden and self._is_from_this_session(msg):
181 181 content = msg['content']
182 182 prompt_number = content['execution_count']
183 183 data = content['data']
184 184 if data.has_key('text/html'):
185 185 self._append_plain_text(self.output_sep)
186 186 self._append_html(self._make_out_prompt(prompt_number))
187 187 html = data['text/html']
188 188 self._append_plain_text('\n')
189 189 self._append_html(html + self.output_sep2)
190 190 elif data.has_key('text/plain'):
191 191 self._append_plain_text(self.output_sep)
192 192 self._append_html(self._make_out_prompt(prompt_number))
193 193 text = data['text/plain']
194 194 # If the repr is multiline, make sure we start on a new line,
195 195 # so that its lines are aligned.
196 196 if "\n" in text and not self.output_sep.endswith("\n"):
197 197 self._append_plain_text('\n')
198 198 self._append_plain_text(text + self.output_sep2)
199 199
200 200 def _handle_display_data(self, msg):
201 201 """ The base handler for the ``display_data`` message.
202 202 """
203 203 # For now, we don't display data from other frontends, but we
204 204 # eventually will as this allows all frontends to monitor the display
205 205 # data. But we need to figure out how to handle this in the GUI.
206 206 if not self._hidden and self._is_from_this_session(msg):
207 207 source = msg['content']['source']
208 208 data = msg['content']['data']
209 209 metadata = msg['content']['metadata']
210 210 # In the regular IPythonWidget, we simply print the plain text
211 211 # representation.
212 212 if data.has_key('text/html'):
213 213 html = data['text/html']
214 214 self._append_html(html)
215 215 elif data.has_key('text/plain'):
216 216 text = data['text/plain']
217 217 self._append_plain_text(text)
218 218 # This newline seems to be needed for text and html output.
219 219 self._append_plain_text(u'\n')
220 220
221 221 def _started_channels(self):
222 222 """ Reimplemented to make a history request.
223 223 """
224 224 super(IPythonWidget, self)._started_channels()
225 self.kernel_manager.xreq_channel.history(hist_access_type='tail', n=1000)
225 self.kernel_manager.shell_channel.history(hist_access_type='tail', n=1000)
226 226
227 227 #---------------------------------------------------------------------------
228 228 # 'ConsoleWidget' public interface
229 229 #---------------------------------------------------------------------------
230 230
231 231 def copy(self):
232 232 """ Copy the currently selected text to the clipboard, removing prompts
233 233 if possible.
234 234 """
235 235 text = self._control.textCursor().selection().toPlainText()
236 236 if text:
237 237 lines = map(transform_ipy_prompt, text.splitlines())
238 238 text = '\n'.join(lines)
239 239 QtGui.QApplication.clipboard().setText(text)
240 240
241 241 #---------------------------------------------------------------------------
242 242 # 'FrontendWidget' public interface
243 243 #---------------------------------------------------------------------------
244 244
245 245 def execute_file(self, path, hidden=False):
246 246 """ Reimplemented to use the 'run' magic.
247 247 """
248 248 # Use forward slashes on Windows to avoid escaping each separator.
249 249 if sys.platform == 'win32':
250 250 path = os.path.normpath(path).replace('\\', '/')
251 251
252 252 self.execute('%%run %s' % path, hidden=hidden)
253 253
254 254 #---------------------------------------------------------------------------
255 255 # 'FrontendWidget' protected interface
256 256 #---------------------------------------------------------------------------
257 257
258 258 def _complete(self):
259 259 """ Reimplemented to support IPython's improved completion machinery.
260 260 """
261 261 # We let the kernel split the input line, so we *always* send an empty
262 262 # text field. Readline-based frontends do get a real text field which
263 263 # they can use.
264 264 text = ''
265 265
266 266 # Send the completion request to the kernel
267 msg_id = self.kernel_manager.xreq_channel.complete(
267 msg_id = self.kernel_manager.shell_channel.complete(
268 268 text, # text
269 269 self._get_input_buffer_cursor_line(), # line
270 270 self._get_input_buffer_cursor_column(), # cursor_pos
271 271 self.input_buffer) # block
272 272 pos = self._get_cursor().position()
273 273 info = self._CompletionRequest(msg_id, pos)
274 274 self._request_info['complete'] = info
275 275
276 276 def _get_banner(self):
277 277 """ Reimplemented to return IPython's default banner.
278 278 """
279 279 return default_gui_banner
280 280
281 281 def _process_execute_error(self, msg):
282 282 """ Reimplemented for IPython-style traceback formatting.
283 283 """
284 284 content = msg['content']
285 285 traceback = '\n'.join(content['traceback']) + '\n'
286 286 if False:
287 287 # FIXME: For now, tracebacks come as plain text, so we can't use
288 288 # the html renderer yet. Once we refactor ultratb to produce
289 289 # properly styled tracebacks, this branch should be the default
290 290 traceback = traceback.replace(' ', '&nbsp;')
291 291 traceback = traceback.replace('\n', '<br/>')
292 292
293 293 ename = content['ename']
294 294 ename_styled = '<span class="error">%s</span>' % ename
295 295 traceback = traceback.replace(ename, ename_styled)
296 296
297 297 self._append_html(traceback)
298 298 else:
299 299 # This is the fallback for now, using plain text with ansi escapes
300 300 self._append_plain_text(traceback)
301 301
302 302 def _process_execute_payload(self, item):
303 303 """ Reimplemented to dispatch payloads to handler methods.
304 304 """
305 305 handler = self._payload_handlers.get(item['source'])
306 306 if handler is None:
307 307 # We have no handler for this type of payload, simply ignore it
308 308 return False
309 309 else:
310 310 handler(item)
311 311 return True
312 312
313 313 def _show_interpreter_prompt(self, number=None):
314 314 """ Reimplemented for IPython-style prompts.
315 315 """
316 316 # If a number was not specified, make a prompt number request.
317 317 if number is None:
318 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
318 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
319 319 info = self._ExecutionRequest(msg_id, 'prompt')
320 320 self._request_info['execute'] = info
321 321 return
322 322
323 323 # Show a new prompt and save information about it so that it can be
324 324 # updated later if the prompt number turns out to be wrong.
325 325 self._prompt_sep = self.input_sep
326 326 self._show_prompt(self._make_in_prompt(number), html=True)
327 327 block = self._control.document().lastBlock()
328 328 length = len(self._prompt)
329 329 self._previous_prompt_obj = self._PromptBlock(block, length, number)
330 330
331 331 # Update continuation prompt to reflect (possibly) new prompt length.
332 332 self._set_continuation_prompt(
333 333 self._make_continuation_prompt(self._prompt), html=True)
334 334
335 335 def _show_interpreter_prompt_for_reply(self, msg):
336 336 """ Reimplemented for IPython-style prompts.
337 337 """
338 338 # Update the old prompt number if necessary.
339 339 content = msg['content']
340 340 previous_prompt_number = content['execution_count']
341 341 if self._previous_prompt_obj and \
342 342 self._previous_prompt_obj.number != previous_prompt_number:
343 343 block = self._previous_prompt_obj.block
344 344
345 345 # Make sure the prompt block has not been erased.
346 346 if block.isValid() and block.text():
347 347
348 348 # Remove the old prompt and insert a new prompt.
349 349 cursor = QtGui.QTextCursor(block)
350 350 cursor.movePosition(QtGui.QTextCursor.Right,
351 351 QtGui.QTextCursor.KeepAnchor,
352 352 self._previous_prompt_obj.length)
353 353 prompt = self._make_in_prompt(previous_prompt_number)
354 354 self._prompt = self._insert_html_fetching_plain_text(
355 355 cursor, prompt)
356 356
357 357 # When the HTML is inserted, Qt blows away the syntax
358 358 # highlighting for the line, so we need to rehighlight it.
359 359 self._highlighter.rehighlightBlock(cursor.block())
360 360
361 361 self._previous_prompt_obj = None
362 362
363 363 # Show a new prompt with the kernel's estimated prompt number.
364 364 self._show_interpreter_prompt(previous_prompt_number + 1)
365 365
366 366 #---------------------------------------------------------------------------
367 367 # 'IPythonWidget' interface
368 368 #---------------------------------------------------------------------------
369 369
370 370 def set_default_style(self, colors='lightbg'):
371 371 """ Sets the widget style to the class defaults.
372 372
373 373 Parameters:
374 374 -----------
375 375 colors : str, optional (default lightbg)
376 376 Whether to use the default IPython light background or dark
377 377 background or B&W style.
378 378 """
379 379 colors = colors.lower()
380 380 if colors=='lightbg':
381 381 self.style_sheet = styles.default_light_style_sheet
382 382 self.syntax_style = styles.default_light_syntax_style
383 383 elif colors=='linux':
384 384 self.style_sheet = styles.default_dark_style_sheet
385 385 self.syntax_style = styles.default_dark_syntax_style
386 386 elif colors=='nocolor':
387 387 self.style_sheet = styles.default_bw_style_sheet
388 388 self.syntax_style = styles.default_bw_syntax_style
389 389 else:
390 390 raise KeyError("No such color scheme: %s"%colors)
391 391
392 392 #---------------------------------------------------------------------------
393 393 # 'IPythonWidget' protected interface
394 394 #---------------------------------------------------------------------------
395 395
396 396 def _edit(self, filename, line=None):
397 397 """ Opens a Python script for editing.
398 398
399 399 Parameters:
400 400 -----------
401 401 filename : str
402 402 A path to a local system file.
403 403
404 404 line : int, optional
405 405 A line of interest in the file.
406 406 """
407 407 if self.custom_edit:
408 408 self.custom_edit_requested.emit(filename, line)
409 409 elif self.editor == 'default':
410 410 self._append_plain_text('No default editor available.\n')
411 411 else:
412 412 try:
413 413 filename = '"%s"' % filename
414 414 if line and self.editor_line:
415 415 command = self.editor_line.format(filename=filename,
416 416 line=line)
417 417 else:
418 418 try:
419 419 command = self.editor.format()
420 420 except KeyError:
421 421 command = self.editor.format(filename=filename)
422 422 else:
423 423 command += ' ' + filename
424 424 except KeyError:
425 425 self._append_plain_text('Invalid editor command.\n')
426 426 else:
427 427 try:
428 428 Popen(command, shell=True)
429 429 except OSError:
430 430 msg = 'Opening editor with command "%s" failed.\n'
431 431 self._append_plain_text(msg % command)
432 432
433 433 def _make_in_prompt(self, number):
434 434 """ Given a prompt number, returns an HTML In prompt.
435 435 """
436 436 body = self.in_prompt % number
437 437 return '<span class="in-prompt">%s</span>' % body
438 438
439 439 def _make_continuation_prompt(self, prompt):
440 440 """ Given a plain text version of an In prompt, returns an HTML
441 441 continuation prompt.
442 442 """
443 443 end_chars = '...: '
444 444 space_count = len(prompt.lstrip('\n')) - len(end_chars)
445 445 body = '&nbsp;' * space_count + end_chars
446 446 return '<span class="in-prompt">%s</span>' % body
447 447
448 448 def _make_out_prompt(self, number):
449 449 """ Given a prompt number, returns an HTML Out prompt.
450 450 """
451 451 body = self.out_prompt % number
452 452 return '<span class="out-prompt">%s</span>' % body
453 453
454 454 #------ Payload handlers --------------------------------------------------
455 455
456 456 # Payload handlers with a generic interface: each takes the opaque payload
457 457 # dict, unpacks it and calls the underlying functions with the necessary
458 458 # arguments.
459 459
460 460 def _handle_payload_edit(self, item):
461 461 self._edit(item['filename'], item['line_number'])
462 462
463 463 def _handle_payload_exit(self, item):
464 464 self._keep_kernel_on_exit = item['keepkernel']
465 465 self.exit_requested.emit()
466 466
467 467 def _handle_payload_next_input(self, item):
468 468 self.input_buffer = dedent(item['text'].rstrip())
469 469
470 470 def _handle_payload_page(self, item):
471 471 # Since the plain text widget supports only a very small subset of HTML
472 472 # and we have no control over the HTML source, we only page HTML
473 473 # payloads in the rich text widget.
474 474 if item['html'] and self.kind == 'rich':
475 475 self._page(item['html'], html=True)
476 476 else:
477 477 self._page(item['text'], html=False)
478 478
479 479 #------ Trait change handlers --------------------------------------------
480 480
481 481 def _style_sheet_changed(self):
482 482 """ Set the style sheets of the underlying widgets.
483 483 """
484 484 self.setStyleSheet(self.style_sheet)
485 485 self._control.document().setDefaultStyleSheet(self.style_sheet)
486 486 if self._page_control:
487 487 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
488 488
489 489 bg_color = self._control.palette().window().color()
490 490 self._ansi_processor.set_background_color(bg_color)
491 491
492 492
493 493 def _syntax_style_changed(self):
494 494 """ Set the style for the syntax highlighter.
495 495 """
496 496 if self._highlighter is None:
497 497 # ignore premature calls
498 498 return
499 499 if self.syntax_style:
500 500 self._highlighter.set_style(self.syntax_style)
501 501 else:
502 502 self._highlighter.set_style_sheet(self.style_sheet)
503 503
@@ -1,372 +1,372 b''
1 1 """ A minimal application using the Qt console-style IPython frontend.
2 2 """
3 3
4 4 #-----------------------------------------------------------------------------
5 5 # Imports
6 6 #-----------------------------------------------------------------------------
7 7
8 8 # stdlib imports
9 9 import os
10 10 import signal
11 11 import sys
12 12
13 13 # System library imports
14 14 from IPython.external.qt import QtGui
15 15 from pygments.styles import get_all_styles
16 16
17 17 # Local imports
18 18 from IPython.core.newapplication import ProfileDir, BaseIPythonApplication
19 19 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
20 20 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
21 21 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
22 22 from IPython.frontend.qt.console import styles
23 23 from IPython.frontend.qt.kernelmanager import QtKernelManager
24 24 from IPython.utils.traitlets import (
25 25 Dict, List, Unicode, Int, CaselessStrEnum, Bool, Any
26 26 )
27 27 from IPython.zmq.ipkernel import (
28 28 flags as ipkernel_flags,
29 29 aliases as ipkernel_aliases,
30 30 IPKernelApp
31 31 )
32 32 from IPython.zmq.zmqshell import ZMQInteractiveShell
33 33
34 34
35 35 #-----------------------------------------------------------------------------
36 36 # Network Constants
37 37 #-----------------------------------------------------------------------------
38 38
39 39 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
40 40
41 41 #-----------------------------------------------------------------------------
42 42 # Classes
43 43 #-----------------------------------------------------------------------------
44 44
45 45 class MainWindow(QtGui.QMainWindow):
46 46
47 47 #---------------------------------------------------------------------------
48 48 # 'object' interface
49 49 #---------------------------------------------------------------------------
50 50
51 51 def __init__(self, app, frontend, existing=False, may_close=True):
52 52 """ Create a MainWindow for the specified FrontendWidget.
53 53
54 54 The app is passed as an argument to allow for different
55 55 closing behavior depending on whether we are the Kernel's parent.
56 56
57 57 If existing is True, then this Console does not own the Kernel.
58 58
59 59 If may_close is True, then this Console is permitted to close the kernel
60 60 """
61 61 super(MainWindow, self).__init__()
62 62 self._app = app
63 63 self._frontend = frontend
64 64 self._existing = existing
65 65 if existing:
66 66 self._may_close = may_close
67 67 else:
68 68 self._may_close = True
69 69 self._frontend.exit_requested.connect(self.close)
70 70 self.setCentralWidget(frontend)
71 71
72 72 #---------------------------------------------------------------------------
73 73 # QWidget interface
74 74 #---------------------------------------------------------------------------
75 75
76 76 def closeEvent(self, event):
77 77 """ Close the window and the kernel (if necessary).
78 78
79 79 This will prompt the user if they are finished with the kernel, and if
80 80 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
81 81 it closes without prompt.
82 82 """
83 83 keepkernel = None #Use the prompt by default
84 84 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
85 85 keepkernel = self._frontend._keep_kernel_on_exit
86 86
87 87 kernel_manager = self._frontend.kernel_manager
88 88
89 89 if keepkernel is None: #show prompt
90 90 if kernel_manager and kernel_manager.channels_running:
91 91 title = self.window().windowTitle()
92 92 cancel = QtGui.QMessageBox.Cancel
93 93 okay = QtGui.QMessageBox.Ok
94 94 if self._may_close:
95 95 msg = "You are closing this Console window."
96 96 info = "Would you like to quit the Kernel and all attached Consoles as well?"
97 97 justthis = QtGui.QPushButton("&No, just this Console", self)
98 98 justthis.setShortcut('N')
99 99 closeall = QtGui.QPushButton("&Yes, quit everything", self)
100 100 closeall.setShortcut('Y')
101 101 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
102 102 title, msg)
103 103 box.setInformativeText(info)
104 104 box.addButton(cancel)
105 105 box.addButton(justthis, QtGui.QMessageBox.NoRole)
106 106 box.addButton(closeall, QtGui.QMessageBox.YesRole)
107 107 box.setDefaultButton(closeall)
108 108 box.setEscapeButton(cancel)
109 109 reply = box.exec_()
110 110 if reply == 1: # close All
111 111 kernel_manager.shutdown_kernel()
112 112 #kernel_manager.stop_channels()
113 113 event.accept()
114 114 elif reply == 0: # close Console
115 115 if not self._existing:
116 116 # Have kernel: don't quit, just close the window
117 117 self._app.setQuitOnLastWindowClosed(False)
118 118 self.deleteLater()
119 119 event.accept()
120 120 else:
121 121 event.ignore()
122 122 else:
123 123 reply = QtGui.QMessageBox.question(self, title,
124 124 "Are you sure you want to close this Console?"+
125 125 "\nThe Kernel and other Consoles will remain active.",
126 126 okay|cancel,
127 127 defaultButton=okay
128 128 )
129 129 if reply == okay:
130 130 event.accept()
131 131 else:
132 132 event.ignore()
133 133 elif keepkernel: #close console but leave kernel running (no prompt)
134 134 if kernel_manager and kernel_manager.channels_running:
135 135 if not self._existing:
136 136 # I have the kernel: don't quit, just close the window
137 137 self._app.setQuitOnLastWindowClosed(False)
138 138 event.accept()
139 139 else: #close console and kernel (no prompt)
140 140 if kernel_manager and kernel_manager.channels_running:
141 141 kernel_manager.shutdown_kernel()
142 142 event.accept()
143 143
144 144 #-----------------------------------------------------------------------------
145 145 # Aliases and Flags
146 146 #-----------------------------------------------------------------------------
147 147
148 148 flags = dict(ipkernel_flags)
149 149
150 150 flags.update({
151 151 'existing' : ({'IPythonQtConsoleApp' : {'existing' : True}},
152 152 "Connect to an existing kernel."),
153 153 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
154 154 "Use a pure Python kernel instead of an IPython kernel."),
155 155 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
156 156 "Disable rich text support."),
157 157 'gui-completion' : ({'FrontendWidget' : {'gui_completion' : True}},
158 158 "use a GUI widget for tab completion"),
159 159 })
160 160
161 161 qt_flags = ['existing', 'pure', 'plain', 'gui-completion']
162 162
163 163 aliases = dict(ipkernel_aliases)
164 164
165 165 aliases.update(dict(
166 166 hb = 'IPythonQtConsoleApp.hb_port',
167 167 shell = 'IPythonQtConsoleApp.shell_port',
168 168 iopub = 'IPythonQtConsoleApp.iopub_port',
169 169 stdin = 'IPythonQtConsoleApp.stdin_port',
170 170 ip = 'IPythonQtConsoleApp.ip',
171 171
172 172 plain = 'IPythonQtConsoleApp.plain',
173 173 pure = 'IPythonQtConsoleApp.pure',
174 174 gui_completion = 'FrontendWidget.gui_completion',
175 175 style = 'IPythonWidget.syntax_style',
176 176 stylesheet = 'IPythonQtConsoleApp.stylesheet',
177 177 colors = 'ZMQInteractiveShell.colors',
178 178
179 179 editor = 'IPythonWidget.editor',
180 180 pi = 'IPythonWidget.in_prompt',
181 181 po = 'IPythonWidget.out_prompt',
182 182 si = 'IPythonWidget.input_sep',
183 183 so = 'IPythonWidget.output_sep',
184 184 so2 = 'IPythonWidget.output_sep2',
185 185 ))
186 186
187 187 #-----------------------------------------------------------------------------
188 188 # IPythonQtConsole
189 189 #-----------------------------------------------------------------------------
190 190
191 191 class IPythonQtConsoleApp(BaseIPythonApplication):
192 192 name = 'ipython-qtconsole'
193 193 default_config_file_name='ipython_config.py'
194 194 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir]
195 195 flags = Dict(flags)
196 196 aliases = Dict(aliases)
197 197
198 198 kernel_argv = List(Unicode)
199 199
200 200 # connection info:
201 201 ip = Unicode(LOCALHOST, config=True,
202 202 help="""Set the kernel\'s IP address [default localhost].
203 203 If the IP address is something other than localhost, then
204 204 Consoles on other machines will be able to connect
205 205 to the Kernel, so be careful!"""
206 206 )
207 207 hb_port = Int(0, config=True,
208 208 help="set the heartbeat port [default: random]")
209 209 shell_port = Int(0, config=True,
210 210 help="set the shell (XREP) port [default: random]")
211 211 iopub_port = Int(0, config=True,
212 212 help="set the iopub (PUB) port [default: random]")
213 213 stdin_port = Int(0, config=True,
214 214 help="set the stdin (XREQ) port [default: random]")
215 215
216 216 existing = Bool(False, config=True,
217 217 help="Whether to connect to an already running Kernel.")
218 218
219 219 stylesheet = Unicode('', config=True,
220 220 help="path to a custom CSS stylesheet")
221 221
222 222 pure = Bool(False, config=True,
223 223 help="Use a pure Python kernel instead of an IPython kernel.")
224 224 plain = Bool(False, config=True,
225 225 help="Use a pure Python kernel instead of an IPython kernel.")
226 226
227 227 def _pure_changed(self, name, old, new):
228 228 kind = 'plain' if self.plain else 'rich'
229 229 self.config.ConsoleWidget.kind = kind
230 230 if self.pure:
231 231 self.widget_factory = FrontendWidget
232 232 elif self.plain:
233 233 self.widget_factory = IPythonWidget
234 234 else:
235 235 self.widget_factory = RichIPythonWidget
236 236
237 237 _plain_changed = _pure_changed
238 238
239 239 # the factory for creating a widget
240 240 widget_factory = Any(RichIPythonWidget)
241 241
242 242 def parse_command_line(self, argv=None):
243 243 super(IPythonQtConsoleApp, self).parse_command_line(argv)
244 244 if argv is None:
245 245 argv = sys.argv[1:]
246 246
247 247 self.kernel_argv = list(argv) # copy
248 248
249 249 # scrub frontend-specific flags
250 250 for a in argv:
251 251 if a.startswith('--') and a[2:] in qt_flags:
252 252 self.kernel_argv.remove(a)
253 253
254 254 def init_kernel_manager(self):
255 255 # Don't let Qt or ZMQ swallow KeyboardInterupts.
256 256 signal.signal(signal.SIGINT, signal.SIG_DFL)
257 257
258 258 # Create a KernelManager and start a kernel.
259 259 self.kernel_manager = QtKernelManager(
260 xreq_address=(self.ip, self.shell_port),
260 shell_address=(self.ip, self.shell_port),
261 261 sub_address=(self.ip, self.iopub_port),
262 rep_address=(self.ip, self.stdin_port),
262 stdin_address=(self.ip, self.stdin_port),
263 263 hb_address=(self.ip, self.hb_port)
264 264 )
265 265 # start the kernel
266 266 if not self.existing:
267 267 kwargs = dict(ip=self.ip, ipython=not self.pure)
268 268 kwargs['extra_arguments'] = self.kernel_argv
269 269 self.kernel_manager.start_kernel(**kwargs)
270 270 self.kernel_manager.start_channels()
271 271
272 272
273 273 def init_qt_elements(self):
274 274 # Create the widget.
275 275 self.app = QtGui.QApplication([])
276 276 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
277 277 self.widget = self.widget_factory(config=self.config,
278 278 local_kernel=local_kernel)
279 279 self.widget.kernel_manager = self.kernel_manager
280 280 self.window = MainWindow(self.app, self.widget, self.existing,
281 281 may_close=local_kernel)
282 282 self.window.setWindowTitle('Python' if self.pure else 'IPython')
283 283
284 284 def init_colors(self):
285 285 """Configure the coloring of the widget"""
286 286 # Note: This will be dramatically simplified when colors
287 287 # are removed from the backend.
288 288
289 289 if self.pure:
290 290 # only IPythonWidget supports styling
291 291 return
292 292
293 293 # parse the colors arg down to current known labels
294 294 try:
295 295 colors = self.config.ZMQInteractiveShell.colors
296 296 except AttributeError:
297 297 colors = None
298 298 try:
299 299 style = self.config.IPythonWidget.colors
300 300 except AttributeError:
301 301 style = None
302 302
303 303 # find the value for colors:
304 304 if colors:
305 305 colors=colors.lower()
306 306 if colors in ('lightbg', 'light'):
307 307 colors='lightbg'
308 308 elif colors in ('dark', 'linux'):
309 309 colors='linux'
310 310 else:
311 311 colors='nocolor'
312 312 elif style:
313 313 if style=='bw':
314 314 colors='nocolor'
315 315 elif styles.dark_style(style):
316 316 colors='linux'
317 317 else:
318 318 colors='lightbg'
319 319 else:
320 320 colors=None
321 321
322 322 # Configure the style.
323 323 widget = self.widget
324 324 if style:
325 325 widget.style_sheet = styles.sheet_from_template(style, colors)
326 326 widget.syntax_style = style
327 327 widget._syntax_style_changed()
328 328 widget._style_sheet_changed()
329 329 elif colors:
330 330 # use a default style
331 331 widget.set_default_style(colors=colors)
332 332 else:
333 333 # this is redundant for now, but allows the widget's
334 334 # defaults to change
335 335 widget.set_default_style()
336 336
337 337 if self.stylesheet:
338 338 # we got an expicit stylesheet
339 339 if os.path.isfile(self.stylesheet):
340 340 with open(self.stylesheet) as f:
341 341 sheet = f.read()
342 342 widget.style_sheet = sheet
343 343 widget._style_sheet_changed()
344 344 else:
345 345 raise IOError("Stylesheet %r not found."%self.stylesheet)
346 346
347 347 def initialize(self, argv=None):
348 348 super(IPythonQtConsoleApp, self).initialize(argv)
349 349 self.init_kernel_manager()
350 350 self.init_qt_elements()
351 351 self.init_colors()
352 352
353 353 def start(self):
354 354
355 355 # draw the window
356 356 self.window.show()
357 357
358 358 # Start the application main loop.
359 359 self.app.exec_()
360 360
361 361 #-----------------------------------------------------------------------------
362 362 # Main entry point
363 363 #-----------------------------------------------------------------------------
364 364
365 365 def main():
366 366 app = IPythonQtConsoleApp()
367 367 app.initialize()
368 368 app.start()
369 369
370 370
371 371 if __name__ == '__main__':
372 372 main()
@@ -1,243 +1,243 b''
1 1 """ Defines a KernelManager that provides signals and slots.
2 2 """
3 3
4 4 # System library imports.
5 5 from IPython.external.qt import QtCore
6 6
7 7 # IPython imports.
8 8 from IPython.utils.traitlets import Type
9 9 from IPython.zmq.kernelmanager import KernelManager, SubSocketChannel, \
10 XReqSocketChannel, RepSocketChannel, HBSocketChannel
10 ShellSocketChannel, StdInSocketChannel, HBSocketChannel
11 11 from util import MetaQObjectHasTraits, SuperQObject
12 12
13 13
14 14 class SocketChannelQObject(SuperQObject):
15 15
16 16 # Emitted when the channel is started.
17 17 started = QtCore.Signal()
18 18
19 19 # Emitted when the channel is stopped.
20 20 stopped = QtCore.Signal()
21 21
22 22 #---------------------------------------------------------------------------
23 # 'ZmqSocketChannel' interface
23 # 'ZMQSocketChannel' interface
24 24 #---------------------------------------------------------------------------
25 25
26 26 def start(self):
27 27 """ Reimplemented to emit signal.
28 28 """
29 29 super(SocketChannelQObject, self).start()
30 30 self.started.emit()
31 31
32 32 def stop(self):
33 33 """ Reimplemented to emit signal.
34 34 """
35 35 super(SocketChannelQObject, self).stop()
36 36 self.stopped.emit()
37 37
38 38
39 class QtXReqSocketChannel(SocketChannelQObject, XReqSocketChannel):
39 class QtShellSocketChannel(SocketChannelQObject, ShellSocketChannel):
40 40
41 41 # Emitted when any message is received.
42 42 message_received = QtCore.Signal(object)
43 43
44 44 # Emitted when a reply has been received for the corresponding request
45 45 # type.
46 46 execute_reply = QtCore.Signal(object)
47 47 complete_reply = QtCore.Signal(object)
48 48 object_info_reply = QtCore.Signal(object)
49 49 history_reply = QtCore.Signal(object)
50 50
51 51 # Emitted when the first reply comes back.
52 52 first_reply = QtCore.Signal()
53 53
54 54 # Used by the first_reply signal logic to determine if a reply is the
55 55 # first.
56 56 _handlers_called = False
57 57
58 58 #---------------------------------------------------------------------------
59 # 'XReqSocketChannel' interface
59 # 'ShellSocketChannel' interface
60 60 #---------------------------------------------------------------------------
61 61
62 62 def call_handlers(self, msg):
63 63 """ Reimplemented to emit signals instead of making callbacks.
64 64 """
65 65 # Emit the generic signal.
66 66 self.message_received.emit(msg)
67 67
68 68 # Emit signals for specialized message types.
69 69 msg_type = msg['msg_type']
70 70 signal = getattr(self, msg_type, None)
71 71 if signal:
72 72 signal.emit(msg)
73 73
74 74 if not self._handlers_called:
75 75 self.first_reply.emit()
76 76 self._handlers_called = True
77 77
78 78 #---------------------------------------------------------------------------
79 # 'QtXReqSocketChannel' interface
79 # 'QtShellSocketChannel' interface
80 80 #---------------------------------------------------------------------------
81 81
82 82 def reset_first_reply(self):
83 83 """ Reset the first_reply signal to fire again on the next reply.
84 84 """
85 85 self._handlers_called = False
86 86
87 87
88 88 class QtSubSocketChannel(SocketChannelQObject, SubSocketChannel):
89 89
90 90 # Emitted when any message is received.
91 91 message_received = QtCore.Signal(object)
92 92
93 93 # Emitted when a message of type 'stream' is received.
94 94 stream_received = QtCore.Signal(object)
95 95
96 96 # Emitted when a message of type 'pyin' is received.
97 97 pyin_received = QtCore.Signal(object)
98 98
99 99 # Emitted when a message of type 'pyout' is received.
100 100 pyout_received = QtCore.Signal(object)
101 101
102 102 # Emitted when a message of type 'pyerr' is received.
103 103 pyerr_received = QtCore.Signal(object)
104 104
105 105 # Emitted when a message of type 'display_data' is received
106 106 display_data_received = QtCore.Signal(object)
107 107
108 108 # Emitted when a crash report message is received from the kernel's
109 109 # last-resort sys.excepthook.
110 110 crash_received = QtCore.Signal(object)
111 111
112 112 # Emitted when a shutdown is noticed.
113 113 shutdown_reply_received = QtCore.Signal(object)
114 114
115 115 #---------------------------------------------------------------------------
116 116 # 'SubSocketChannel' interface
117 117 #---------------------------------------------------------------------------
118 118
119 119 def call_handlers(self, msg):
120 120 """ Reimplemented to emit signals instead of making callbacks.
121 121 """
122 122 # Emit the generic signal.
123 123 self.message_received.emit(msg)
124 124 # Emit signals for specialized message types.
125 125 msg_type = msg['msg_type']
126 126 signal = getattr(self, msg_type + '_received', None)
127 127 if signal:
128 128 signal.emit(msg)
129 129 elif msg_type in ('stdout', 'stderr'):
130 130 self.stream_received.emit(msg)
131 131
132 132 def flush(self):
133 133 """ Reimplemented to ensure that signals are dispatched immediately.
134 134 """
135 135 super(QtSubSocketChannel, self).flush()
136 136 QtCore.QCoreApplication.instance().processEvents()
137 137
138 138
139 class QtRepSocketChannel(SocketChannelQObject, RepSocketChannel):
139 class QtStdInSocketChannel(SocketChannelQObject, StdInSocketChannel):
140 140
141 141 # Emitted when any message is received.
142 142 message_received = QtCore.Signal(object)
143 143
144 144 # Emitted when an input request is received.
145 145 input_requested = QtCore.Signal(object)
146 146
147 147 #---------------------------------------------------------------------------
148 # 'RepSocketChannel' interface
148 # 'StdInSocketChannel' interface
149 149 #---------------------------------------------------------------------------
150 150
151 151 def call_handlers(self, msg):
152 152 """ Reimplemented to emit signals instead of making callbacks.
153 153 """
154 154 # Emit the generic signal.
155 155 self.message_received.emit(msg)
156 156
157 157 # Emit signals for specialized message types.
158 158 msg_type = msg['msg_type']
159 159 if msg_type == 'input_request':
160 160 self.input_requested.emit(msg)
161 161
162 162
163 163 class QtHBSocketChannel(SocketChannelQObject, HBSocketChannel):
164 164
165 165 # Emitted when the kernel has died.
166 166 kernel_died = QtCore.Signal(object)
167 167
168 168 #---------------------------------------------------------------------------
169 169 # 'HBSocketChannel' interface
170 170 #---------------------------------------------------------------------------
171 171
172 172 def call_handlers(self, since_last_heartbeat):
173 173 """ Reimplemented to emit signals instead of making callbacks.
174 174 """
175 175 # Emit the generic signal.
176 176 self.kernel_died.emit(since_last_heartbeat)
177 177
178 178
179 179 class QtKernelManager(KernelManager, SuperQObject):
180 180 """ A KernelManager that provides signals and slots.
181 181 """
182 182
183 183 __metaclass__ = MetaQObjectHasTraits
184 184
185 185 # Emitted when the kernel manager has started listening.
186 186 started_channels = QtCore.Signal()
187 187
188 188 # Emitted when the kernel manager has stopped listening.
189 189 stopped_channels = QtCore.Signal()
190 190
191 191 # Use Qt-specific channel classes that emit signals.
192 192 sub_channel_class = Type(QtSubSocketChannel)
193 xreq_channel_class = Type(QtXReqSocketChannel)
194 rep_channel_class = Type(QtRepSocketChannel)
193 shell_channel_class = Type(QtShellSocketChannel)
194 stdin_channel_class = Type(QtStdInSocketChannel)
195 195 hb_channel_class = Type(QtHBSocketChannel)
196 196
197 197 #---------------------------------------------------------------------------
198 198 # 'KernelManager' interface
199 199 #---------------------------------------------------------------------------
200 200
201 201 #------ Kernel process management ------------------------------------------
202 202
203 203 def start_kernel(self, *args, **kw):
204 204 """ Reimplemented for proper heartbeat management.
205 205 """
206 if self._xreq_channel is not None:
207 self._xreq_channel.reset_first_reply()
206 if self._shell_channel is not None:
207 self._shell_channel.reset_first_reply()
208 208 super(QtKernelManager, self).start_kernel(*args, **kw)
209 209
210 210 #------ Channel management -------------------------------------------------
211 211
212 212 def start_channels(self, *args, **kw):
213 213 """ Reimplemented to emit signal.
214 214 """
215 215 super(QtKernelManager, self).start_channels(*args, **kw)
216 216 self.started_channels.emit()
217 217
218 218 def stop_channels(self):
219 219 """ Reimplemented to emit signal.
220 220 """
221 221 super(QtKernelManager, self).stop_channels()
222 222 self.stopped_channels.emit()
223 223
224 224 @property
225 def xreq_channel(self):
225 def shell_channel(self):
226 226 """ Reimplemented for proper heartbeat management.
227 227 """
228 if self._xreq_channel is None:
229 self._xreq_channel = super(QtKernelManager, self).xreq_channel
230 self._xreq_channel.first_reply.connect(self._first_reply)
231 return self._xreq_channel
228 if self._shell_channel is None:
229 self._shell_channel = super(QtKernelManager, self).shell_channel
230 self._shell_channel.first_reply.connect(self._first_reply)
231 return self._shell_channel
232 232
233 233 #---------------------------------------------------------------------------
234 234 # Protected interface
235 235 #---------------------------------------------------------------------------
236 236
237 237 def _first_reply(self):
238 238 """ Unpauses the heartbeat channel when the first reply is received on
239 239 the execute channel. Note that this will *not* start the heartbeat
240 240 channel if it is not already running!
241 241 """
242 242 if self._hb_channel is not None:
243 243 self._hb_channel.unpause()
@@ -1,121 +1,121 b''
1 1 """Implement a fully blocking kernel manager.
2 2
3 3 Useful for test suites and blocking terminal interfaces.
4 4 """
5 5 #-----------------------------------------------------------------------------
6 6 # Copyright (C) 2010 The IPython Development Team
7 7 #
8 8 # Distributed under the terms of the BSD License. The full license is in
9 9 # the file COPYING.txt, distributed as part of this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15 from __future__ import print_function
16 16
17 17 # Stdlib
18 18 from Queue import Queue, Empty
19 19
20 20 # Our own
21 21 from IPython.utils import io
22 22 from IPython.utils.traitlets import Type
23 23
24 from .kernelmanager import (KernelManager, SubSocketChannel,
25 XReqSocketChannel, RepSocketChannel, HBSocketChannel)
24 from .kernelmanager import (KernelManager, SubSocketChannel, HBSocketChannel,
25 ShellSocketChannel, StdInSocketChannel)
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Functions and classes
29 29 #-----------------------------------------------------------------------------
30 30
31 31 class BlockingSubSocketChannel(SubSocketChannel):
32 32
33 33 def __init__(self, context, session, address=None):
34 34 super(BlockingSubSocketChannel, self).__init__(context, session,
35 35 address)
36 36 self._in_queue = Queue()
37 37
38 38 def call_handlers(self, msg):
39 39 #io.rprint('[[Sub]]', msg) # dbg
40 40 self._in_queue.put(msg)
41 41
42 42 def msg_ready(self):
43 43 """Is there a message that has been received?"""
44 44 if self._in_queue.qsize() == 0:
45 45 return False
46 46 else:
47 47 return True
48 48
49 49 def get_msg(self, block=True, timeout=None):
50 50 """Get a message if there is one that is ready."""
51 51 return self._in_queue.get(block, timeout)
52 52
53 53 def get_msgs(self):
54 54 """Get all messages that are currently ready."""
55 55 msgs = []
56 56 while True:
57 57 try:
58 58 msgs.append(self.get_msg(block=False))
59 59 except Empty:
60 60 break
61 61 return msgs
62 62
63 63
64 class BlockingXReqSocketChannel(XReqSocketChannel):
64 class BlockingShellSocketChannel(ShellSocketChannel):
65 65
66 66 def __init__(self, context, session, address=None):
67 super(BlockingXReqSocketChannel, self).__init__(context, session,
67 super(BlockingShellSocketChannel, self).__init__(context, session,
68 68 address)
69 69 self._in_queue = Queue()
70 70
71 71 def call_handlers(self, msg):
72 #io.rprint('[[XReq]]', msg) # dbg
72 #io.rprint('[[Shell]]', msg) # dbg
73 73 self._in_queue.put(msg)
74 74
75 75 def msg_ready(self):
76 76 """Is there a message that has been received?"""
77 77 if self._in_queue.qsize() == 0:
78 78 return False
79 79 else:
80 80 return True
81 81
82 82 def get_msg(self, block=True, timeout=None):
83 83 """Get a message if there is one that is ready."""
84 84 return self._in_queue.get(block, timeout)
85 85
86 86 def get_msgs(self):
87 87 """Get all messages that are currently ready."""
88 88 msgs = []
89 89 while True:
90 90 try:
91 91 msgs.append(self.get_msg(block=False))
92 92 except Empty:
93 93 break
94 94 return msgs
95 95
96 96
97 class BlockingRepSocketChannel(RepSocketChannel):
97 class BlockingStdInSocketChannel(StdInSocketChannel):
98 98
99 99 def call_handlers(self, msg):
100 100 #io.rprint('[[Rep]]', msg) # dbg
101 101 pass
102 102
103 103
104 104 class BlockingHBSocketChannel(HBSocketChannel):
105 105
106 106 # This kernel needs rapid monitoring capabilities
107 107 time_to_dead = 0.2
108 108
109 109 def call_handlers(self, since_last_heartbeat):
110 110 #io.rprint('[[Heart]]', since_last_heartbeat) # dbg
111 111 pass
112 112
113 113
114 114 class BlockingKernelManager(KernelManager):
115 115
116 116 # The classes to use for the various channels.
117 xreq_channel_class = Type(BlockingXReqSocketChannel)
117 shell_channel_class = Type(BlockingShellSocketChannel)
118 118 sub_channel_class = Type(BlockingSubSocketChannel)
119 rep_channel_class = Type(BlockingRepSocketChannel)
119 stdin_channel_class = Type(BlockingStdInSocketChannel)
120 120 hb_channel_class = Type(BlockingHBSocketChannel)
121 121
@@ -1,968 +1,968 b''
1 1 """Base classes to manage the interaction with a running kernel.
2 2
3 3 TODO
4 4 * Create logger to handle debugging and console messages.
5 5 """
6 6
7 7 #-----------------------------------------------------------------------------
8 8 # Copyright (C) 2008-2010 The IPython Development Team
9 9 #
10 10 # Distributed under the terms of the BSD License. The full license is in
11 11 # the file COPYING, distributed as part of this software.
12 12 #-----------------------------------------------------------------------------
13 13
14 14 #-----------------------------------------------------------------------------
15 15 # Imports
16 16 #-----------------------------------------------------------------------------
17 17
18 18 # Standard library imports.
19 19 import atexit
20 20 import errno
21 21 from Queue import Queue, Empty
22 22 from subprocess import Popen
23 23 import signal
24 24 import sys
25 25 from threading import Thread
26 26 import time
27 27 import logging
28 28
29 29 # System library imports.
30 30 import zmq
31 31 from zmq import POLLIN, POLLOUT, POLLERR
32 32 from zmq.eventloop import ioloop
33 33
34 34 # Local imports.
35 35 from IPython.utils import io
36 36 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
37 37 from IPython.utils.traitlets import HasTraits, Any, Instance, Type, TCPAddress
38 38 from session import Session, Message
39 39
40 40 #-----------------------------------------------------------------------------
41 41 # Constants and exceptions
42 42 #-----------------------------------------------------------------------------
43 43
44 44 class InvalidPortNumber(Exception):
45 45 pass
46 46
47 47 #-----------------------------------------------------------------------------
48 48 # Utility functions
49 49 #-----------------------------------------------------------------------------
50 50
51 51 # some utilities to validate message structure, these might get moved elsewhere
52 52 # if they prove to have more generic utility
53 53
54 54 def validate_string_list(lst):
55 55 """Validate that the input is a list of strings.
56 56
57 57 Raises ValueError if not."""
58 58 if not isinstance(lst, list):
59 59 raise ValueError('input %r must be a list' % lst)
60 60 for x in lst:
61 61 if not isinstance(x, basestring):
62 62 raise ValueError('element %r in list must be a string' % x)
63 63
64 64
65 65 def validate_string_dict(dct):
66 66 """Validate that the input is a dict with string keys and values.
67 67
68 68 Raises ValueError if not."""
69 69 for k,v in dct.iteritems():
70 70 if not isinstance(k, basestring):
71 71 raise ValueError('key %r in dict must be a string' % k)
72 72 if not isinstance(v, basestring):
73 73 raise ValueError('value %r in dict must be a string' % v)
74 74
75 75
76 76 #-----------------------------------------------------------------------------
77 77 # ZMQ Socket Channel classes
78 78 #-----------------------------------------------------------------------------
79 79
80 class ZmqSocketChannel(Thread):
80 class ZMQSocketChannel(Thread):
81 81 """The base class for the channels that use ZMQ sockets.
82 82 """
83 83 context = None
84 84 session = None
85 85 socket = None
86 86 ioloop = None
87 87 iostate = None
88 88 _address = None
89 89
90 90 def __init__(self, context, session, address):
91 91 """Create a channel
92 92
93 93 Parameters
94 94 ----------
95 95 context : :class:`zmq.Context`
96 96 The ZMQ context to use.
97 97 session : :class:`session.Session`
98 98 The session to use.
99 99 address : tuple
100 100 Standard (ip, port) tuple that the kernel is listening on.
101 101 """
102 super(ZmqSocketChannel, self).__init__()
102 super(ZMQSocketChannel, self).__init__()
103 103 self.daemon = True
104 104
105 105 self.context = context
106 106 self.session = session
107 107 if address[1] == 0:
108 108 message = 'The port number for a channel cannot be 0.'
109 109 raise InvalidPortNumber(message)
110 110 self._address = address
111 111
112 112 def _run_loop(self):
113 113 """Run my loop, ignoring EINTR events in the poller"""
114 114 while True:
115 115 try:
116 116 self.ioloop.start()
117 117 except zmq.ZMQError as e:
118 118 if e.errno == errno.EINTR:
119 119 continue
120 120 else:
121 121 raise
122 122 else:
123 123 break
124 124
125 125 def stop(self):
126 126 """Stop the channel's activity.
127 127
128 128 This calls :method:`Thread.join` and returns when the thread
129 129 terminates. :class:`RuntimeError` will be raised if
130 130 :method:`self.start` is called again.
131 131 """
132 132 self.join()
133 133
134 134 @property
135 135 def address(self):
136 136 """Get the channel's address as an (ip, port) tuple.
137 137
138 138 By the default, the address is (localhost, 0), where 0 means a random
139 139 port.
140 140 """
141 141 return self._address
142 142
143 143 def add_io_state(self, state):
144 144 """Add IO state to the eventloop.
145 145
146 146 Parameters
147 147 ----------
148 148 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
149 149 The IO state flag to set.
150 150
151 151 This is thread safe as it uses the thread safe IOLoop.add_callback.
152 152 """
153 153 def add_io_state_callback():
154 154 if not self.iostate & state:
155 155 self.iostate = self.iostate | state
156 156 self.ioloop.update_handler(self.socket, self.iostate)
157 157 self.ioloop.add_callback(add_io_state_callback)
158 158
159 159 def drop_io_state(self, state):
160 160 """Drop IO state from the eventloop.
161 161
162 162 Parameters
163 163 ----------
164 164 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
165 165 The IO state flag to set.
166 166
167 167 This is thread safe as it uses the thread safe IOLoop.add_callback.
168 168 """
169 169 def drop_io_state_callback():
170 170 if self.iostate & state:
171 171 self.iostate = self.iostate & (~state)
172 172 self.ioloop.update_handler(self.socket, self.iostate)
173 173 self.ioloop.add_callback(drop_io_state_callback)
174 174
175 175
176 class XReqSocketChannel(ZmqSocketChannel):
176 class ShellSocketChannel(ZMQSocketChannel):
177 177 """The XREQ channel for issues request/replies to the kernel.
178 178 """
179 179
180 180 command_queue = None
181 181
182 182 def __init__(self, context, session, address):
183 super(XReqSocketChannel, self).__init__(context, session, address)
183 super(ShellSocketChannel, self).__init__(context, session, address)
184 184 self.command_queue = Queue()
185 185 self.ioloop = ioloop.IOLoop()
186 186
187 187 def run(self):
188 188 """The thread's main activity. Call start() instead."""
189 189 self.socket = self.context.socket(zmq.XREQ)
190 190 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
191 191 self.socket.connect('tcp://%s:%i' % self.address)
192 192 self.iostate = POLLERR|POLLIN
193 193 self.ioloop.add_handler(self.socket, self._handle_events,
194 194 self.iostate)
195 195 self._run_loop()
196 196
197 197 def stop(self):
198 198 self.ioloop.stop()
199 super(XReqSocketChannel, self).stop()
199 super(ShellSocketChannel, self).stop()
200 200
201 201 def call_handlers(self, msg):
202 202 """This method is called in the ioloop thread when a message arrives.
203 203
204 204 Subclasses should override this method to handle incoming messages.
205 205 It is important to remember that this method is called in the thread
206 206 so that some logic must be done to ensure that the application leve
207 207 handlers are called in the application thread.
208 208 """
209 209 raise NotImplementedError('call_handlers must be defined in a subclass.')
210 210
211 211 def execute(self, code, silent=False,
212 212 user_variables=None, user_expressions=None):
213 213 """Execute code in the kernel.
214 214
215 215 Parameters
216 216 ----------
217 217 code : str
218 218 A string of Python code.
219 219
220 220 silent : bool, optional (default False)
221 221 If set, the kernel will execute the code as quietly possible.
222 222
223 223 user_variables : list, optional
224 224 A list of variable names to pull from the user's namespace. They
225 225 will come back as a dict with these names as keys and their
226 226 :func:`repr` as values.
227 227
228 228 user_expressions : dict, optional
229 229 A dict with string keys and to pull from the user's
230 230 namespace. They will come back as a dict with these names as keys
231 231 and their :func:`repr` as values.
232 232
233 233 Returns
234 234 -------
235 235 The msg_id of the message sent.
236 236 """
237 237 if user_variables is None:
238 238 user_variables = []
239 239 if user_expressions is None:
240 240 user_expressions = {}
241 241
242 242 # Don't waste network traffic if inputs are invalid
243 243 if not isinstance(code, basestring):
244 244 raise ValueError('code %r must be a string' % code)
245 245 validate_string_list(user_variables)
246 246 validate_string_dict(user_expressions)
247 247
248 248 # Create class for content/msg creation. Related to, but possibly
249 249 # not in Session.
250 250 content = dict(code=code, silent=silent,
251 251 user_variables=user_variables,
252 252 user_expressions=user_expressions)
253 253 msg = self.session.msg('execute_request', content)
254 254 self._queue_request(msg)
255 255 return msg['header']['msg_id']
256 256
257 257 def complete(self, text, line, cursor_pos, block=None):
258 258 """Tab complete text in the kernel's namespace.
259 259
260 260 Parameters
261 261 ----------
262 262 text : str
263 263 The text to complete.
264 264 line : str
265 265 The full line of text that is the surrounding context for the
266 266 text to complete.
267 267 cursor_pos : int
268 268 The position of the cursor in the line where the completion was
269 269 requested.
270 270 block : str, optional
271 271 The full block of code in which the completion is being requested.
272 272
273 273 Returns
274 274 -------
275 275 The msg_id of the message sent.
276 276 """
277 277 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
278 278 msg = self.session.msg('complete_request', content)
279 279 self._queue_request(msg)
280 280 return msg['header']['msg_id']
281 281
282 282 def object_info(self, oname):
283 283 """Get metadata information about an object.
284 284
285 285 Parameters
286 286 ----------
287 287 oname : str
288 288 A string specifying the object name.
289 289
290 290 Returns
291 291 -------
292 292 The msg_id of the message sent.
293 293 """
294 294 content = dict(oname=oname)
295 295 msg = self.session.msg('object_info_request', content)
296 296 self._queue_request(msg)
297 297 return msg['header']['msg_id']
298 298
299 299 def history(self, raw=True, output=False, hist_access_type='range', **kwargs):
300 300 """Get entries from the history list.
301 301
302 302 Parameters
303 303 ----------
304 304 raw : bool
305 305 If True, return the raw input.
306 306 output : bool
307 307 If True, then return the output as well.
308 308 hist_access_type : str
309 309 'range' (fill in session, start and stop params), 'tail' (fill in n)
310 310 or 'search' (fill in pattern param).
311 311
312 312 session : int
313 313 For a range request, the session from which to get lines. Session
314 314 numbers are positive integers; negative ones count back from the
315 315 current session.
316 316 start : int
317 317 The first line number of a history range.
318 318 stop : int
319 319 The final (excluded) line number of a history range.
320 320
321 321 n : int
322 322 The number of lines of history to get for a tail request.
323 323
324 324 pattern : str
325 325 The glob-syntax pattern for a search request.
326 326
327 327 Returns
328 328 -------
329 329 The msg_id of the message sent.
330 330 """
331 331 content = dict(raw=raw, output=output, hist_access_type=hist_access_type,
332 332 **kwargs)
333 333 msg = self.session.msg('history_request', content)
334 334 self._queue_request(msg)
335 335 return msg['header']['msg_id']
336 336
337 337 def shutdown(self, restart=False):
338 338 """Request an immediate kernel shutdown.
339 339
340 340 Upon receipt of the (empty) reply, client code can safely assume that
341 341 the kernel has shut down and it's safe to forcefully terminate it if
342 342 it's still alive.
343 343
344 344 The kernel will send the reply via a function registered with Python's
345 345 atexit module, ensuring it's truly done as the kernel is done with all
346 346 normal operation.
347 347 """
348 348 # Send quit message to kernel. Once we implement kernel-side setattr,
349 349 # this should probably be done that way, but for now this will do.
350 350 msg = self.session.msg('shutdown_request', {'restart':restart})
351 351 self._queue_request(msg)
352 352 return msg['header']['msg_id']
353 353
354 354 def _handle_events(self, socket, events):
355 355 if events & POLLERR:
356 356 self._handle_err()
357 357 if events & POLLOUT:
358 358 self._handle_send()
359 359 if events & POLLIN:
360 360 self._handle_recv()
361 361
362 362 def _handle_recv(self):
363 363 ident,msg = self.session.recv(self.socket, 0)
364 364 self.call_handlers(msg)
365 365
366 366 def _handle_send(self):
367 367 try:
368 368 msg = self.command_queue.get(False)
369 369 except Empty:
370 370 pass
371 371 else:
372 372 self.session.send(self.socket,msg)
373 373 if self.command_queue.empty():
374 374 self.drop_io_state(POLLOUT)
375 375
376 376 def _handle_err(self):
377 377 # We don't want to let this go silently, so eventually we should log.
378 378 raise zmq.ZMQError()
379 379
380 380 def _queue_request(self, msg):
381 381 self.command_queue.put(msg)
382 382 self.add_io_state(POLLOUT)
383 383
384 384
385 class SubSocketChannel(ZmqSocketChannel):
385 class SubSocketChannel(ZMQSocketChannel):
386 386 """The SUB channel which listens for messages that the kernel publishes.
387 387 """
388 388
389 389 def __init__(self, context, session, address):
390 390 super(SubSocketChannel, self).__init__(context, session, address)
391 391 self.ioloop = ioloop.IOLoop()
392 392
393 393 def run(self):
394 394 """The thread's main activity. Call start() instead."""
395 395 self.socket = self.context.socket(zmq.SUB)
396 396 self.socket.setsockopt(zmq.SUBSCRIBE,'')
397 397 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
398 398 self.socket.connect('tcp://%s:%i' % self.address)
399 399 self.iostate = POLLIN|POLLERR
400 400 self.ioloop.add_handler(self.socket, self._handle_events,
401 401 self.iostate)
402 402 self._run_loop()
403 403
404 404 def stop(self):
405 405 self.ioloop.stop()
406 406 super(SubSocketChannel, self).stop()
407 407
408 408 def call_handlers(self, msg):
409 409 """This method is called in the ioloop thread when a message arrives.
410 410
411 411 Subclasses should override this method to handle incoming messages.
412 412 It is important to remember that this method is called in the thread
413 413 so that some logic must be done to ensure that the application leve
414 414 handlers are called in the application thread.
415 415 """
416 416 raise NotImplementedError('call_handlers must be defined in a subclass.')
417 417
418 418 def flush(self, timeout=1.0):
419 419 """Immediately processes all pending messages on the SUB channel.
420 420
421 421 Callers should use this method to ensure that :method:`call_handlers`
422 422 has been called for all messages that have been received on the
423 423 0MQ SUB socket of this channel.
424 424
425 425 This method is thread safe.
426 426
427 427 Parameters
428 428 ----------
429 429 timeout : float, optional
430 430 The maximum amount of time to spend flushing, in seconds. The
431 431 default is one second.
432 432 """
433 433 # We do the IOLoop callback process twice to ensure that the IOLoop
434 434 # gets to perform at least one full poll.
435 435 stop_time = time.time() + timeout
436 436 for i in xrange(2):
437 437 self._flushed = False
438 438 self.ioloop.add_callback(self._flush)
439 439 while not self._flushed and time.time() < stop_time:
440 440 time.sleep(0.01)
441 441
442 442 def _handle_events(self, socket, events):
443 443 # Turn on and off POLLOUT depending on if we have made a request
444 444 if events & POLLERR:
445 445 self._handle_err()
446 446 if events & POLLIN:
447 447 self._handle_recv()
448 448
449 449 def _handle_err(self):
450 450 # We don't want to let this go silently, so eventually we should log.
451 451 raise zmq.ZMQError()
452 452
453 453 def _handle_recv(self):
454 454 # Get all of the messages we can
455 455 while True:
456 456 try:
457 457 ident,msg = self.session.recv(self.socket)
458 458 except zmq.ZMQError:
459 459 # Check the errno?
460 460 # Will this trigger POLLERR?
461 461 break
462 462 else:
463 463 if msg is None:
464 464 break
465 465 self.call_handlers(msg)
466 466
467 467 def _flush(self):
468 468 """Callback for :method:`self.flush`."""
469 469 self._flushed = True
470 470
471 471
472 class RepSocketChannel(ZmqSocketChannel):
472 class StdInSocketChannel(ZMQSocketChannel):
473 473 """A reply channel to handle raw_input requests that the kernel makes."""
474 474
475 475 msg_queue = None
476 476
477 477 def __init__(self, context, session, address):
478 super(RepSocketChannel, self).__init__(context, session, address)
478 super(StdInSocketChannel, self).__init__(context, session, address)
479 479 self.ioloop = ioloop.IOLoop()
480 480 self.msg_queue = Queue()
481 481
482 482 def run(self):
483 483 """The thread's main activity. Call start() instead."""
484 484 self.socket = self.context.socket(zmq.XREQ)
485 485 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
486 486 self.socket.connect('tcp://%s:%i' % self.address)
487 487 self.iostate = POLLERR|POLLIN
488 488 self.ioloop.add_handler(self.socket, self._handle_events,
489 489 self.iostate)
490 490 self._run_loop()
491 491
492 492 def stop(self):
493 493 self.ioloop.stop()
494 super(RepSocketChannel, self).stop()
494 super(StdInSocketChannel, self).stop()
495 495
496 496 def call_handlers(self, msg):
497 497 """This method is called in the ioloop thread when a message arrives.
498 498
499 499 Subclasses should override this method to handle incoming messages.
500 500 It is important to remember that this method is called in the thread
501 501 so that some logic must be done to ensure that the application leve
502 502 handlers are called in the application thread.
503 503 """
504 504 raise NotImplementedError('call_handlers must be defined in a subclass.')
505 505
506 506 def input(self, string):
507 507 """Send a string of raw input to the kernel."""
508 508 content = dict(value=string)
509 509 msg = self.session.msg('input_reply', content)
510 510 self._queue_reply(msg)
511 511
512 512 def _handle_events(self, socket, events):
513 513 if events & POLLERR:
514 514 self._handle_err()
515 515 if events & POLLOUT:
516 516 self._handle_send()
517 517 if events & POLLIN:
518 518 self._handle_recv()
519 519
520 520 def _handle_recv(self):
521 521 ident,msg = self.session.recv(self.socket, 0)
522 522 self.call_handlers(msg)
523 523
524 524 def _handle_send(self):
525 525 try:
526 526 msg = self.msg_queue.get(False)
527 527 except Empty:
528 528 pass
529 529 else:
530 530 self.session.send(self.socket,msg)
531 531 if self.msg_queue.empty():
532 532 self.drop_io_state(POLLOUT)
533 533
534 534 def _handle_err(self):
535 535 # We don't want to let this go silently, so eventually we should log.
536 536 raise zmq.ZMQError()
537 537
538 538 def _queue_reply(self, msg):
539 539 self.msg_queue.put(msg)
540 540 self.add_io_state(POLLOUT)
541 541
542 542
543 class HBSocketChannel(ZmqSocketChannel):
543 class HBSocketChannel(ZMQSocketChannel):
544 544 """The heartbeat channel which monitors the kernel heartbeat.
545 545
546 546 Note that the heartbeat channel is paused by default. As long as you start
547 547 this channel, the kernel manager will ensure that it is paused and un-paused
548 548 as appropriate.
549 549 """
550 550
551 551 time_to_dead = 3.0
552 552 socket = None
553 553 poller = None
554 554 _running = None
555 555 _pause = None
556 556
557 557 def __init__(self, context, session, address):
558 558 super(HBSocketChannel, self).__init__(context, session, address)
559 559 self._running = False
560 560 self._pause = True
561 561
562 562 def _create_socket(self):
563 563 self.socket = self.context.socket(zmq.REQ)
564 564 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
565 565 self.socket.connect('tcp://%s:%i' % self.address)
566 566 self.poller = zmq.Poller()
567 567 self.poller.register(self.socket, zmq.POLLIN)
568 568
569 569 def run(self):
570 570 """The thread's main activity. Call start() instead."""
571 571 self._create_socket()
572 572 self._running = True
573 573 while self._running:
574 574 if self._pause:
575 575 time.sleep(self.time_to_dead)
576 576 else:
577 577 since_last_heartbeat = 0.0
578 578 request_time = time.time()
579 579 try:
580 580 #io.rprint('Ping from HB channel') # dbg
581 581 self.socket.send(b'ping')
582 582 except zmq.ZMQError, e:
583 583 #io.rprint('*** HB Error:', e) # dbg
584 584 if e.errno == zmq.EFSM:
585 585 #io.rprint('sleep...', self.time_to_dead) # dbg
586 586 time.sleep(self.time_to_dead)
587 587 self._create_socket()
588 588 else:
589 589 raise
590 590 else:
591 591 while True:
592 592 try:
593 593 self.socket.recv(zmq.NOBLOCK)
594 594 except zmq.ZMQError, e:
595 595 #io.rprint('*** HB Error 2:', e) # dbg
596 596 if e.errno == zmq.EAGAIN:
597 597 before_poll = time.time()
598 598 until_dead = self.time_to_dead - (before_poll -
599 599 request_time)
600 600
601 601 # When the return value of poll() is an empty
602 602 # list, that is when things have gone wrong
603 603 # (zeromq bug). As long as it is not an empty
604 604 # list, poll is working correctly even if it
605 605 # returns quickly. Note: poll timeout is in
606 606 # milliseconds.
607 607 if until_dead > 0.0:
608 608 while True:
609 609 try:
610 610 self.poller.poll(1000 * until_dead)
611 611 except zmq.ZMQError as e:
612 612 if e.errno == errno.EINTR:
613 613 continue
614 614 else:
615 615 raise
616 616 else:
617 617 break
618 618
619 619 since_last_heartbeat = time.time()-request_time
620 620 if since_last_heartbeat > self.time_to_dead:
621 621 self.call_handlers(since_last_heartbeat)
622 622 break
623 623 else:
624 624 # FIXME: We should probably log this instead.
625 625 raise
626 626 else:
627 627 until_dead = self.time_to_dead - (time.time() -
628 628 request_time)
629 629 if until_dead > 0.0:
630 630 #io.rprint('sleep...', self.time_to_dead) # dbg
631 631 time.sleep(until_dead)
632 632 break
633 633
634 634 def pause(self):
635 635 """Pause the heartbeat."""
636 636 self._pause = True
637 637
638 638 def unpause(self):
639 639 """Unpause the heartbeat."""
640 640 self._pause = False
641 641
642 642 def is_beating(self):
643 643 """Is the heartbeat running and not paused."""
644 644 if self.is_alive() and not self._pause:
645 645 return True
646 646 else:
647 647 return False
648 648
649 649 def stop(self):
650 650 self._running = False
651 651 super(HBSocketChannel, self).stop()
652 652
653 653 def call_handlers(self, since_last_heartbeat):
654 654 """This method is called in the ioloop thread when a message arrives.
655 655
656 656 Subclasses should override this method to handle incoming messages.
657 657 It is important to remember that this method is called in the thread
658 658 so that some logic must be done to ensure that the application leve
659 659 handlers are called in the application thread.
660 660 """
661 661 raise NotImplementedError('call_handlers must be defined in a subclass.')
662 662
663 663
664 664 #-----------------------------------------------------------------------------
665 665 # Main kernel manager class
666 666 #-----------------------------------------------------------------------------
667 667
668 668 class KernelManager(HasTraits):
669 669 """ Manages a kernel for a frontend.
670 670
671 671 The SUB channel is for the frontend to receive messages published by the
672 672 kernel.
673 673
674 674 The REQ channel is for the frontend to make requests of the kernel.
675 675
676 676 The REP channel is for the kernel to request stdin (raw_input) from the
677 677 frontend.
678 678 """
679 679 # The PyZMQ Context to use for communication with the kernel.
680 680 context = Instance(zmq.Context,(),{})
681 681
682 682 # The Session to use for communication with the kernel.
683 683 session = Instance(Session,(),{})
684 684
685 685 # The kernel process with which the KernelManager is communicating.
686 686 kernel = Instance(Popen)
687 687
688 688 # The addresses for the communication channels.
689 xreq_address = TCPAddress((LOCALHOST, 0))
689 shell_address = TCPAddress((LOCALHOST, 0))
690 690 sub_address = TCPAddress((LOCALHOST, 0))
691 rep_address = TCPAddress((LOCALHOST, 0))
691 stdin_address = TCPAddress((LOCALHOST, 0))
692 692 hb_address = TCPAddress((LOCALHOST, 0))
693 693
694 694 # The classes to use for the various channels.
695 xreq_channel_class = Type(XReqSocketChannel)
695 shell_channel_class = Type(ShellSocketChannel)
696 696 sub_channel_class = Type(SubSocketChannel)
697 rep_channel_class = Type(RepSocketChannel)
697 stdin_channel_class = Type(StdInSocketChannel)
698 698 hb_channel_class = Type(HBSocketChannel)
699 699
700 700 # Protected traits.
701 701 _launch_args = Any
702 _xreq_channel = Any
702 _shell_channel = Any
703 703 _sub_channel = Any
704 _rep_channel = Any
704 _stdin_channel = Any
705 705 _hb_channel = Any
706 706
707 707 def __init__(self, **kwargs):
708 708 super(KernelManager, self).__init__(**kwargs)
709 709 # Uncomment this to try closing the context.
710 710 # atexit.register(self.context.close)
711 711
712 712 #--------------------------------------------------------------------------
713 713 # Channel management methods:
714 714 #--------------------------------------------------------------------------
715 715
716 def start_channels(self, xreq=True, sub=True, rep=True, hb=True):
716 def start_channels(self, shell=True, sub=True, stdin=True, hb=True):
717 717 """Starts the channels for this kernel.
718 718
719 719 This will create the channels if they do not exist and then start
720 720 them. If port numbers of 0 are being used (random ports) then you
721 721 must first call :method:`start_kernel`. If the channels have been
722 722 stopped and you call this, :class:`RuntimeError` will be raised.
723 723 """
724 if xreq:
725 self.xreq_channel.start()
724 if shell:
725 self.shell_channel.start()
726 726 if sub:
727 727 self.sub_channel.start()
728 if rep:
729 self.rep_channel.start()
728 if stdin:
729 self.stdin_channel.start()
730 730 if hb:
731 731 self.hb_channel.start()
732 732
733 733 def stop_channels(self):
734 734 """Stops all the running channels for this kernel.
735 735 """
736 if self.xreq_channel.is_alive():
737 self.xreq_channel.stop()
736 if self.shell_channel.is_alive():
737 self.shell_channel.stop()
738 738 if self.sub_channel.is_alive():
739 739 self.sub_channel.stop()
740 if self.rep_channel.is_alive():
741 self.rep_channel.stop()
740 if self.stdin_channel.is_alive():
741 self.stdin_channel.stop()
742 742 if self.hb_channel.is_alive():
743 743 self.hb_channel.stop()
744 744
745 745 @property
746 746 def channels_running(self):
747 747 """Are any of the channels created and running?"""
748 return (self.xreq_channel.is_alive() or self.sub_channel.is_alive() or
749 self.rep_channel.is_alive() or self.hb_channel.is_alive())
748 return (self.shell_channel.is_alive() or self.sub_channel.is_alive() or
749 self.stdin_channel.is_alive() or self.hb_channel.is_alive())
750 750
751 751 #--------------------------------------------------------------------------
752 752 # Kernel process management methods:
753 753 #--------------------------------------------------------------------------
754 754
755 755 def start_kernel(self, **kw):
756 756 """Starts a kernel process and configures the manager to use it.
757 757
758 758 If random ports (port=0) are being used, this method must be called
759 759 before the channels are created.
760 760
761 761 Parameters:
762 762 -----------
763 763 ipython : bool, optional (default True)
764 764 Whether to use an IPython kernel instead of a plain Python kernel.
765 765
766 766 **kw : optional
767 767 See respective options for IPython and Python kernels.
768 768 """
769 xreq, sub, rep, hb = self.xreq_address, self.sub_address, \
770 self.rep_address, self.hb_address
771 if xreq[0] not in LOCAL_IPS or sub[0] not in LOCAL_IPS or \
772 rep[0] not in LOCAL_IPS or hb[0] not in LOCAL_IPS:
769 shell, sub, stdin, hb = self.shell_address, self.sub_address, \
770 self.stdin_address, self.hb_address
771 if shell[0] not in LOCAL_IPS or sub[0] not in LOCAL_IPS or \
772 stdin[0] not in LOCAL_IPS or hb[0] not in LOCAL_IPS:
773 773 raise RuntimeError("Can only launch a kernel on a local interface. "
774 774 "Make sure that the '*_address' attributes are "
775 775 "configured properly. "
776 776 "Currently valid addresses are: %s"%LOCAL_IPS
777 777 )
778 778
779 779 self._launch_args = kw.copy()
780 780 if kw.pop('ipython', True):
781 781 from ipkernel import launch_kernel
782 782 else:
783 783 from pykernel import launch_kernel
784 784 self.kernel, xrep, pub, req, _hb = launch_kernel(
785 shell_port=xreq[1], iopub_port=sub[1],
786 stdin_port=rep[1], hb_port=hb[1], **kw)
787 self.xreq_address = (xreq[0], xrep)
785 shell_port=shell[1], iopub_port=sub[1],
786 stdin_port=stdin[1], hb_port=hb[1], **kw)
787 self.shell_address = (shell[0], xrep)
788 788 self.sub_address = (sub[0], pub)
789 self.rep_address = (rep[0], req)
789 self.stdin_address = (stdin[0], req)
790 790 self.hb_address = (hb[0], _hb)
791 791
792 792 def shutdown_kernel(self, restart=False):
793 793 """ Attempts to the stop the kernel process cleanly. If the kernel
794 794 cannot be stopped, it is killed, if possible.
795 795 """
796 796 # FIXME: Shutdown does not work on Windows due to ZMQ errors!
797 797 if sys.platform == 'win32':
798 798 self.kill_kernel()
799 799 return
800 800
801 801 # Pause the heart beat channel if it exists.
802 802 if self._hb_channel is not None:
803 803 self._hb_channel.pause()
804 804
805 805 # Don't send any additional kernel kill messages immediately, to give
806 806 # the kernel a chance to properly execute shutdown actions. Wait for at
807 807 # most 1s, checking every 0.1s.
808 self.xreq_channel.shutdown(restart=restart)
808 self.shell_channel.shutdown(restart=restart)
809 809 for i in range(10):
810 810 if self.is_alive:
811 811 time.sleep(0.1)
812 812 else:
813 813 break
814 814 else:
815 815 # OK, we've waited long enough.
816 816 if self.has_kernel:
817 817 self.kill_kernel()
818 818
819 819 def restart_kernel(self, now=False, **kw):
820 820 """Restarts a kernel with the arguments that were used to launch it.
821 821
822 822 If the old kernel was launched with random ports, the same ports will be
823 823 used for the new kernel.
824 824
825 825 Parameters
826 826 ----------
827 827 now : bool, optional
828 828 If True, the kernel is forcefully restarted *immediately*, without
829 829 having a chance to do any cleanup action. Otherwise the kernel is
830 830 given 1s to clean up before a forceful restart is issued.
831 831
832 832 In all cases the kernel is restarted, the only difference is whether
833 833 it is given a chance to perform a clean shutdown or not.
834 834
835 835 **kw : optional
836 836 Any options specified here will replace those used to launch the
837 837 kernel.
838 838 """
839 839 if self._launch_args is None:
840 840 raise RuntimeError("Cannot restart the kernel. "
841 841 "No previous call to 'start_kernel'.")
842 842 else:
843 843 # Stop currently running kernel.
844 844 if self.has_kernel:
845 845 if now:
846 846 self.kill_kernel()
847 847 else:
848 848 self.shutdown_kernel(restart=True)
849 849
850 850 # Start new kernel.
851 851 self._launch_args.update(kw)
852 852 self.start_kernel(**self._launch_args)
853 853
854 854 # FIXME: Messages get dropped in Windows due to probable ZMQ bug
855 855 # unless there is some delay here.
856 856 if sys.platform == 'win32':
857 857 time.sleep(0.2)
858 858
859 859 @property
860 860 def has_kernel(self):
861 861 """Returns whether a kernel process has been specified for the kernel
862 862 manager.
863 863 """
864 864 return self.kernel is not None
865 865
866 866 def kill_kernel(self):
867 867 """ Kill the running kernel. """
868 868 if self.has_kernel:
869 869 # Pause the heart beat channel if it exists.
870 870 if self._hb_channel is not None:
871 871 self._hb_channel.pause()
872 872
873 873 # Attempt to kill the kernel.
874 874 try:
875 875 self.kernel.kill()
876 876 except OSError, e:
877 877 # In Windows, we will get an Access Denied error if the process
878 878 # has already terminated. Ignore it.
879 879 if sys.platform == 'win32':
880 880 if e.winerror != 5:
881 881 raise
882 882 # On Unix, we may get an ESRCH error if the process has already
883 883 # terminated. Ignore it.
884 884 else:
885 885 from errno import ESRCH
886 886 if e.errno != ESRCH:
887 887 raise
888 888 self.kernel = None
889 889 else:
890 890 raise RuntimeError("Cannot kill kernel. No kernel is running!")
891 891
892 892 def interrupt_kernel(self):
893 893 """ Interrupts the kernel. Unlike ``signal_kernel``, this operation is
894 894 well supported on all platforms.
895 895 """
896 896 if self.has_kernel:
897 897 if sys.platform == 'win32':
898 898 from parentpoller import ParentPollerWindows as Poller
899 899 Poller.send_interrupt(self.kernel.win32_interrupt_event)
900 900 else:
901 901 self.kernel.send_signal(signal.SIGINT)
902 902 else:
903 903 raise RuntimeError("Cannot interrupt kernel. No kernel is running!")
904 904
905 905 def signal_kernel(self, signum):
906 906 """ Sends a signal to the kernel. Note that since only SIGTERM is
907 907 supported on Windows, this function is only useful on Unix systems.
908 908 """
909 909 if self.has_kernel:
910 910 self.kernel.send_signal(signum)
911 911 else:
912 912 raise RuntimeError("Cannot signal kernel. No kernel is running!")
913 913
914 914 @property
915 915 def is_alive(self):
916 916 """Is the kernel process still running?"""
917 917 # FIXME: not using a heartbeat means this method is broken for any
918 918 # remote kernel, it's only capable of handling local kernels.
919 919 if self.has_kernel:
920 920 if self.kernel.poll() is None:
921 921 return True
922 922 else:
923 923 return False
924 924 else:
925 925 # We didn't start the kernel with this KernelManager so we don't
926 926 # know if it is running. We should use a heartbeat for this case.
927 927 return True
928 928
929 929 #--------------------------------------------------------------------------
930 930 # Channels used for communication with the kernel:
931 931 #--------------------------------------------------------------------------
932 932
933 933 @property
934 def xreq_channel(self):
934 def shell_channel(self):
935 935 """Get the REQ socket channel object to make requests of the kernel."""
936 if self._xreq_channel is None:
937 self._xreq_channel = self.xreq_channel_class(self.context,
936 if self._shell_channel is None:
937 self._shell_channel = self.shell_channel_class(self.context,
938 938 self.session,
939 self.xreq_address)
940 return self._xreq_channel
939 self.shell_address)
940 return self._shell_channel
941 941
942 942 @property
943 943 def sub_channel(self):
944 944 """Get the SUB socket channel object."""
945 945 if self._sub_channel is None:
946 946 self._sub_channel = self.sub_channel_class(self.context,
947 947 self.session,
948 948 self.sub_address)
949 949 return self._sub_channel
950 950
951 951 @property
952 def rep_channel(self):
952 def stdin_channel(self):
953 953 """Get the REP socket channel object to handle stdin (raw_input)."""
954 if self._rep_channel is None:
955 self._rep_channel = self.rep_channel_class(self.context,
954 if self._stdin_channel is None:
955 self._stdin_channel = self.stdin_channel_class(self.context,
956 956 self.session,
957 self.rep_address)
958 return self._rep_channel
957 self.stdin_address)
958 return self._stdin_channel
959 959
960 960 @property
961 961 def hb_channel(self):
962 962 """Get the heartbeat socket channel object to check that the
963 963 kernel is alive."""
964 964 if self._hb_channel is None:
965 965 self._hb_channel = self.hb_channel_class(self.context,
966 966 self.session,
967 967 self.hb_address)
968 968 return self._hb_channel
@@ -1,40 +1,40 b''
1 1 """Test suite for our zeromq-based messaging specification.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (C) 2010 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING.txt, distributed as part of this software.
8 8 #-----------------------------------------------------------------------------
9 9
10 10 import sys
11 11 import time
12 12
13 13 import nose.tools as nt
14 14
15 15 from ..blockingkernelmanager import BlockingKernelManager
16 16
17 17 from IPython.utils import io
18 18
19 19 def setup():
20 20 global KM
21 21 KM = BlockingKernelManager()
22 22
23 23 KM.start_kernel()
24 24 KM.start_channels()
25 25 # Give the kernel a chance to come up.
26 26 time.sleep(1)
27 27
28 28 def teardown():
29 29 io.rprint('Entering teardown...') # dbg
30 30 io.rprint('Stopping channels and kernel...') # dbg
31 31 KM.stop_channels()
32 32 KM.kill_kernel()
33 33
34 34
35 35 # Actual tests
36 36
37 37 def test_execute():
38 KM.xreq_channel.execute(code='x=1')
39 KM.xreq_channel.execute(code='print 1')
38 KM.shell_channel.execute(code='x=1')
39 KM.shell_channel.execute(code='print 1')
40 40
General Comments 0
You need to be logged in to leave comments. Login now