##// END OF EJS Templates
added shutdown notification handling to ipythonqt
MinRK -
Show More
@@ -1,554 +1,568 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 import time
6 7
7 8 # System library imports
8 9 from pygments.lexers import PythonLexer
9 10 from PyQt4 import QtCore, QtGui
10 11
11 12 # Local imports
12 13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
13 14 from IPython.core.oinspect import call_tip
14 15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
15 16 from IPython.utils.traitlets import Bool
16 17 from bracket_matcher import BracketMatcher
17 18 from call_tip_widget import CallTipWidget
18 19 from completion_lexer import CompletionLexer
19 20 from history_console_widget import HistoryConsoleWidget
20 21 from pygments_highlighter import PygmentsHighlighter
21 22
22 23
23 24 class FrontendHighlighter(PygmentsHighlighter):
24 25 """ A PygmentsHighlighter that can be turned on and off and that ignores
25 26 prompts.
26 27 """
27 28
28 29 def __init__(self, frontend):
29 30 super(FrontendHighlighter, self).__init__(frontend._control.document())
30 31 self._current_offset = 0
31 32 self._frontend = frontend
32 33 self.highlighting_on = False
33 34
34 35 def highlightBlock(self, qstring):
35 36 """ Highlight a block of text. Reimplemented to highlight selectively.
36 37 """
37 38 if not self.highlighting_on:
38 39 return
39 40
40 41 # The input to this function is unicode string that may contain
41 42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
42 43 # the string as plain text so we can compare it.
43 44 current_block = self.currentBlock()
44 45 string = self._frontend._get_block_plain_text(current_block)
45 46
46 47 # Decide whether to check for the regular or continuation prompt.
47 48 if current_block.contains(self._frontend._prompt_pos):
48 49 prompt = self._frontend._prompt
49 50 else:
50 51 prompt = self._frontend._continuation_prompt
51 52
52 53 # Don't highlight the part of the string that contains the prompt.
53 54 if string.startswith(prompt):
54 55 self._current_offset = len(prompt)
55 56 qstring.remove(0, len(prompt))
56 57 else:
57 58 self._current_offset = 0
58 59
59 60 PygmentsHighlighter.highlightBlock(self, qstring)
60 61
61 62 def rehighlightBlock(self, block):
62 63 """ Reimplemented to temporarily enable highlighting if disabled.
63 64 """
64 65 old = self.highlighting_on
65 66 self.highlighting_on = True
66 67 super(FrontendHighlighter, self).rehighlightBlock(block)
67 68 self.highlighting_on = old
68 69
69 70 def setFormat(self, start, count, format):
70 71 """ Reimplemented to highlight selectively.
71 72 """
72 73 start += self._current_offset
73 74 PygmentsHighlighter.setFormat(self, start, count, format)
74 75
75 76
76 77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 78 """ A Qt frontend for a generic Python kernel.
78 79 """
79 80
80 81 # An option and corresponding signal for overriding the default kernel
81 82 # interrupt behavior.
82 83 custom_interrupt = Bool(False)
83 84 custom_interrupt_requested = QtCore.pyqtSignal()
84 85
85 86 # An option and corresponding signals for overriding the default kernel
86 87 # restart behavior.
87 88 custom_restart = Bool(False)
88 89 custom_restart_kernel_died = QtCore.pyqtSignal(float)
89 90 custom_restart_requested = QtCore.pyqtSignal()
90 91
91 92 # Emitted when an 'execute_reply' has been received from the kernel and
92 93 # processed by the FrontendWidget.
93 94 executed = QtCore.pyqtSignal(object)
94 95
95 96 # Emitted when an exit request has been received from the kernel.
96 97 exit_requested = QtCore.pyqtSignal()
97 98
98 99 # Protected class variables.
99 100 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
100 101 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
101 102 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
102 103 _input_splitter_class = InputSplitter
103 104
104 105 #---------------------------------------------------------------------------
105 106 # 'object' interface
106 107 #---------------------------------------------------------------------------
107 108
108 109 def __init__(self, *args, **kw):
109 110 super(FrontendWidget, self).__init__(*args, **kw)
110 111
111 112 # FrontendWidget protected variables.
112 113 self._bracket_matcher = BracketMatcher(self._control)
113 114 self._call_tip_widget = CallTipWidget(self._control)
114 115 self._completion_lexer = CompletionLexer(PythonLexer())
115 116 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
116 117 self._hidden = False
117 118 self._highlighter = FrontendHighlighter(self)
118 119 self._input_splitter = self._input_splitter_class(input_mode='cell')
119 120 self._kernel_manager = None
120 121 self._request_info = {}
121 122
122 123 # Configure the ConsoleWidget.
123 124 self.tab_width = 4
124 125 self._set_continuation_prompt('... ')
125 126
126 127 # Configure the CallTipWidget.
127 128 self._call_tip_widget.setFont(self.font)
128 129 self.font_changed.connect(self._call_tip_widget.setFont)
129 130
130 131 # Configure actions.
131 132 action = self._copy_raw_action
132 133 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
133 134 action.setEnabled(False)
134 135 action.setShortcut(QtGui.QKeySequence(key))
135 136 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
136 137 action.triggered.connect(self.copy_raw)
137 138 self.copy_available.connect(action.setEnabled)
138 139 self.addAction(action)
139 140
140 141 # Connect signal handlers.
141 142 document = self._control.document()
142 143 document.contentsChange.connect(self._document_contents_change)
143 144
144 145 #---------------------------------------------------------------------------
145 146 # 'ConsoleWidget' public interface
146 147 #---------------------------------------------------------------------------
147 148
148 149 def copy(self):
149 150 """ Copy the currently selected text to the clipboard, removing prompts.
150 151 """
151 152 text = unicode(self._control.textCursor().selection().toPlainText())
152 153 if text:
153 154 lines = map(transform_classic_prompt, text.splitlines())
154 155 text = '\n'.join(lines)
155 156 QtGui.QApplication.clipboard().setText(text)
156 157
157 158 #---------------------------------------------------------------------------
158 159 # 'ConsoleWidget' abstract interface
159 160 #---------------------------------------------------------------------------
160 161
161 162 def _is_complete(self, source, interactive):
162 163 """ Returns whether 'source' can be completely processed and a new
163 164 prompt created. When triggered by an Enter/Return key press,
164 165 'interactive' is True; otherwise, it is False.
165 166 """
166 167 complete = self._input_splitter.push(source)
167 168 if interactive:
168 169 complete = not self._input_splitter.push_accepts_more()
169 170 return complete
170 171
171 172 def _execute(self, source, hidden):
172 173 """ Execute 'source'. If 'hidden', do not show any output.
173 174
174 175 See parent class :meth:`execute` docstring for full details.
175 176 """
176 177 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
177 178 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
178 179 self._hidden = hidden
179 180
180 181 def _prompt_started_hook(self):
181 182 """ Called immediately after a new prompt is displayed.
182 183 """
183 184 if not self._reading:
184 185 self._highlighter.highlighting_on = True
185 186
186 187 def _prompt_finished_hook(self):
187 188 """ Called immediately after a prompt is finished, i.e. when some input
188 189 will be processed and a new prompt displayed.
189 190 """
190 191 if not self._reading:
191 192 self._highlighter.highlighting_on = False
192 193
193 194 def _tab_pressed(self):
194 195 """ Called when the tab key is pressed. Returns whether to continue
195 196 processing the event.
196 197 """
197 198 # Perform tab completion if:
198 199 # 1) The cursor is in the input buffer.
199 200 # 2) There is a non-whitespace character before the cursor.
200 201 text = self._get_input_buffer_cursor_line()
201 202 if text is None:
202 203 return False
203 204 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
204 205 if complete:
205 206 self._complete()
206 207 return not complete
207 208
208 209 #---------------------------------------------------------------------------
209 210 # 'ConsoleWidget' protected interface
210 211 #---------------------------------------------------------------------------
211 212
212 213 def _context_menu_make(self, pos):
213 214 """ Reimplemented to add an action for raw copy.
214 215 """
215 216 menu = super(FrontendWidget, self)._context_menu_make(pos)
216 217 for before_action in menu.actions():
217 218 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
218 219 QtGui.QKeySequence.ExactMatch:
219 220 menu.insertAction(before_action, self._copy_raw_action)
220 221 break
221 222 return menu
222 223
223 224 def _event_filter_console_keypress(self, event):
224 225 """ Reimplemented for execution interruption and smart backspace.
225 226 """
226 227 key = event.key()
227 228 if self._control_key_down(event.modifiers(), include_command=False):
228 229
229 230 if key == QtCore.Qt.Key_C and self._executing:
230 231 self.interrupt_kernel()
231 232 return True
232 233
233 234 elif key == QtCore.Qt.Key_Period:
234 235 message = 'Are you sure you want to restart the kernel?'
235 236 self.restart_kernel(message, now=False)
236 237 return True
237 238
238 239 elif not event.modifiers() & QtCore.Qt.AltModifier:
239 240
240 241 # Smart backspace: remove four characters in one backspace if:
241 242 # 1) everything left of the cursor is whitespace
242 243 # 2) the four characters immediately left of the cursor are spaces
243 244 if key == QtCore.Qt.Key_Backspace:
244 245 col = self._get_input_buffer_cursor_column()
245 246 cursor = self._control.textCursor()
246 247 if col > 3 and not cursor.hasSelection():
247 248 text = self._get_input_buffer_cursor_line()[:col]
248 249 if text.endswith(' ') and not text.strip():
249 250 cursor.movePosition(QtGui.QTextCursor.Left,
250 251 QtGui.QTextCursor.KeepAnchor, 4)
251 252 cursor.removeSelectedText()
252 253 return True
253 254
254 255 return super(FrontendWidget, self)._event_filter_console_keypress(event)
255 256
256 257 def _insert_continuation_prompt(self, cursor):
257 258 """ Reimplemented for auto-indentation.
258 259 """
259 260 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
260 261 cursor.insertText(' ' * self._input_splitter.indent_spaces)
261 262
262 263 #---------------------------------------------------------------------------
263 264 # 'BaseFrontendMixin' abstract interface
264 265 #---------------------------------------------------------------------------
265 266
266 267 def _handle_complete_reply(self, rep):
267 268 """ Handle replies for tab completion.
268 269 """
269 270 cursor = self._get_cursor()
270 271 info = self._request_info.get('complete')
271 272 if info and info.id == rep['parent_header']['msg_id'] and \
272 273 info.pos == cursor.position():
273 274 text = '.'.join(self._get_context())
274 275 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
275 276 self._complete_with_items(cursor, rep['content']['matches'])
276 277
277 278 def _handle_execute_reply(self, msg):
278 279 """ Handles replies for code execution.
279 280 """
280 281 info = self._request_info.get('execute')
281 282 if info and info.id == msg['parent_header']['msg_id'] and \
282 283 info.kind == 'user' and not self._hidden:
283 284 # Make sure that all output from the SUB channel has been processed
284 285 # before writing a new prompt.
285 286 self.kernel_manager.sub_channel.flush()
286 287
287 288 # Reset the ANSI style information to prevent bad text in stdout
288 289 # from messing up our colors. We're not a true terminal so we're
289 290 # allowed to do this.
290 291 if self.ansi_codes:
291 292 self._ansi_processor.reset_sgr()
292 293
293 294 content = msg['content']
294 295 status = content['status']
295 296 if status == 'ok':
296 297 self._process_execute_ok(msg)
297 298 elif status == 'error':
298 299 self._process_execute_error(msg)
299 300 elif status == 'abort':
300 301 self._process_execute_abort(msg)
301 302
302 303 self._show_interpreter_prompt_for_reply(msg)
303 304 self.executed.emit(msg)
304 305
305 306 def _handle_input_request(self, msg):
306 307 """ Handle requests for raw_input.
307 308 """
308 309 if self._hidden:
309 310 raise RuntimeError('Request for raw input during hidden execution.')
310 311
311 312 # Make sure that all output from the SUB channel has been processed
312 313 # before entering readline mode.
313 314 self.kernel_manager.sub_channel.flush()
314 315
315 316 def callback(line):
316 317 self.kernel_manager.rep_channel.input(line)
317 318 self._readline(msg['content']['prompt'], callback=callback)
318 319
319 320 def _handle_kernel_died(self, since_last_heartbeat):
320 321 """ Handle the kernel's death by asking if the user wants to restart.
321 322 """
322 323 if self.custom_restart:
323 324 self.custom_restart_kernel_died.emit(since_last_heartbeat)
324 325 else:
325 326 message = 'The kernel heartbeat has been inactive for %.2f ' \
326 327 'seconds. Do you want to restart the kernel? You may ' \
327 328 'first want to check the network connection.' % \
328 329 since_last_heartbeat
329 330 self.restart_kernel(message, now=True)
330 331
331 332 def _handle_object_info_reply(self, rep):
332 333 """ Handle replies for call tips.
333 334 """
334 335 cursor = self._get_cursor()
335 336 info = self._request_info.get('call_tip')
336 337 if info and info.id == rep['parent_header']['msg_id'] and \
337 338 info.pos == cursor.position():
338 339 # Get the information for a call tip. For now we format the call
339 340 # line as string, later we can pass False to format_call and
340 341 # syntax-highlight it ourselves for nicer formatting in the
341 342 # calltip.
342 343 call_info, doc = call_tip(rep['content'], format_call=True)
343 344 if call_info or doc:
344 345 self._call_tip_widget.show_call_info(call_info, doc)
345 346
346 347 def _handle_pyout(self, msg):
347 348 """ Handle display hook output.
348 349 """
349 350 if not self._hidden and self._is_from_this_session(msg):
350 351 self._append_plain_text(msg['content']['data'] + '\n')
351 352
352 353 def _handle_stream(self, msg):
353 354 """ Handle stdout, stderr, and stdin.
354 355 """
355 356 if not self._hidden and self._is_from_this_session(msg):
356 357 # Most consoles treat tabs as being 8 space characters. Convert tabs
357 358 # to spaces so that output looks as expected regardless of this
358 359 # widget's tab width.
359 360 text = msg['content']['data'].expandtabs(8)
360 361
361 362 self._append_plain_text(text)
362 363 self._control.moveCursor(QtGui.QTextCursor.End)
363 364
365 def _handle_shutdown_reply(self, msg):
366 """ Handle shutdown signal, only if from other console.
367 """
368 if not self._hidden and not self._is_from_this_session(msg):
369 if not msg['content']['restart']:
370 sys.exit(0)
371 else:
372 # we just got notified of a restart!
373 time.sleep(0.25) # wait 1/4 sec to reest
374 # lest the request for a new prompt
375 # goes to the old kernel
376 self.reset()
377
364 378 def _started_channels(self):
365 379 """ Called when the KernelManager channels have started listening or
366 380 when the frontend is assigned an already listening KernelManager.
367 381 """
368 382 self.reset()
369 383
370 384 #---------------------------------------------------------------------------
371 385 # 'FrontendWidget' public interface
372 386 #---------------------------------------------------------------------------
373 387
374 388 def copy_raw(self):
375 389 """ Copy the currently selected text to the clipboard without attempting
376 390 to remove prompts or otherwise alter the text.
377 391 """
378 392 self._control.copy()
379 393
380 394 def execute_file(self, path, hidden=False):
381 395 """ Attempts to execute file with 'path'. If 'hidden', no output is
382 396 shown.
383 397 """
384 398 self.execute('execfile("%s")' % path, hidden=hidden)
385 399
386 400 def interrupt_kernel(self):
387 401 """ Attempts to interrupt the running kernel.
388 402 """
389 403 if self.custom_interrupt:
390 404 self.custom_interrupt_requested.emit()
391 405 elif self.kernel_manager.has_kernel:
392 406 self.kernel_manager.interrupt_kernel()
393 407 else:
394 408 self._append_plain_text('Kernel process is either remote or '
395 409 'unspecified. Cannot interrupt.\n')
396 410
397 411 def reset(self):
398 412 """ Resets the widget to its initial state. Similar to ``clear``, but
399 413 also re-writes the banner and aborts execution if necessary.
400 414 """
401 415 if self._executing:
402 416 self._executing = False
403 417 self._request_info['execute'] = None
404 418 self._reading = False
405 419 self._highlighter.highlighting_on = False
406 420
407 421 self._control.clear()
408 422 self._append_plain_text(self._get_banner())
409 423 self._show_interpreter_prompt()
410 424
411 425 def restart_kernel(self, message, now=False):
412 426 """ Attempts to restart the running kernel.
413 427 """
414 428 # FIXME: now should be configurable via a checkbox in the dialog. Right
415 429 # now at least the heartbeat path sets it to True and the manual restart
416 430 # to False. But those should just be the pre-selected states of a
417 431 # checkbox that the user could override if so desired. But I don't know
418 432 # enough Qt to go implementing the checkbox now.
419 433
420 434 if self.custom_restart:
421 435 self.custom_restart_requested.emit()
422 436
423 437 elif self.kernel_manager.has_kernel:
424 438 # Pause the heart beat channel to prevent further warnings.
425 439 self.kernel_manager.hb_channel.pause()
426 440
427 441 # Prompt the user to restart the kernel. Un-pause the heartbeat if
428 442 # they decline. (If they accept, the heartbeat will be un-paused
429 443 # automatically when the kernel is restarted.)
430 444 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
431 445 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
432 446 message, buttons)
433 447 if result == QtGui.QMessageBox.Yes:
434 448 try:
435 449 self.kernel_manager.restart_kernel(now=now)
436 450 except RuntimeError:
437 451 self._append_plain_text('Kernel started externally. '
438 452 'Cannot restart.\n')
439 453 else:
440 454 self.reset()
441 455 else:
442 456 self.kernel_manager.hb_channel.unpause()
443 457
444 458 else:
445 459 self._append_plain_text('Kernel process is either remote or '
446 460 'unspecified. Cannot restart.\n')
447 461
448 462 #---------------------------------------------------------------------------
449 463 # 'FrontendWidget' protected interface
450 464 #---------------------------------------------------------------------------
451 465
452 466 def _call_tip(self):
453 467 """ Shows a call tip, if appropriate, at the current cursor location.
454 468 """
455 469 # Decide if it makes sense to show a call tip
456 470 cursor = self._get_cursor()
457 471 cursor.movePosition(QtGui.QTextCursor.Left)
458 472 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
459 473 return False
460 474 context = self._get_context(cursor)
461 475 if not context:
462 476 return False
463 477
464 478 # Send the metadata request to the kernel
465 479 name = '.'.join(context)
466 480 msg_id = self.kernel_manager.xreq_channel.object_info(name)
467 481 pos = self._get_cursor().position()
468 482 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
469 483 return True
470 484
471 485 def _complete(self):
472 486 """ Performs completion at the current cursor location.
473 487 """
474 488 context = self._get_context()
475 489 if context:
476 490 # Send the completion request to the kernel
477 491 msg_id = self.kernel_manager.xreq_channel.complete(
478 492 '.'.join(context), # text
479 493 self._get_input_buffer_cursor_line(), # line
480 494 self._get_input_buffer_cursor_column(), # cursor_pos
481 495 self.input_buffer) # block
482 496 pos = self._get_cursor().position()
483 497 info = self._CompletionRequest(msg_id, pos)
484 498 self._request_info['complete'] = info
485 499
486 500 def _get_banner(self):
487 501 """ Gets a banner to display at the beginning of a session.
488 502 """
489 503 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
490 504 '"license" for more information.'
491 505 return banner % (sys.version, sys.platform)
492 506
493 507 def _get_context(self, cursor=None):
494 508 """ Gets the context for the specified cursor (or the current cursor
495 509 if none is specified).
496 510 """
497 511 if cursor is None:
498 512 cursor = self._get_cursor()
499 513 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
500 514 QtGui.QTextCursor.KeepAnchor)
501 515 text = unicode(cursor.selection().toPlainText())
502 516 return self._completion_lexer.get_context(text)
503 517
504 518 def _process_execute_abort(self, msg):
505 519 """ Process a reply for an aborted execution request.
506 520 """
507 521 self._append_plain_text("ERROR: execution aborted\n")
508 522
509 523 def _process_execute_error(self, msg):
510 524 """ Process a reply for an execution request that resulted in an error.
511 525 """
512 526 content = msg['content']
513 527 traceback = ''.join(content['traceback'])
514 528 self._append_plain_text(traceback)
515 529
516 530 def _process_execute_ok(self, msg):
517 531 """ Process a reply for a successful execution equest.
518 532 """
519 533 payload = msg['content']['payload']
520 534 for item in payload:
521 535 if not self._process_execute_payload(item):
522 536 warning = 'Warning: received unknown payload of type %s'
523 537 print(warning % repr(item['source']))
524 538
525 539 def _process_execute_payload(self, item):
526 540 """ Process a single payload item from the list of payload items in an
527 541 execution reply. Returns whether the payload was handled.
528 542 """
529 543 # The basic FrontendWidget doesn't handle payloads, as they are a
530 544 # mechanism for going beyond the standard Python interpreter model.
531 545 return False
532 546
533 547 def _show_interpreter_prompt(self):
534 548 """ Shows a prompt for the interpreter.
535 549 """
536 550 self._show_prompt('>>> ')
537 551
538 552 def _show_interpreter_prompt_for_reply(self, msg):
539 553 """ Shows a prompt for the interpreter given an 'execute_reply' message.
540 554 """
541 555 self._show_interpreter_prompt()
542 556
543 557 #------ Signal handlers ----------------------------------------------------
544 558
545 559 def _document_contents_change(self, position, removed, added):
546 560 """ Called whenever the document's content changes. Display a call tip
547 561 if appropriate.
548 562 """
549 563 # Calculate where the cursor should be *after* the change:
550 564 position += added
551 565
552 566 document = self._control.document()
553 567 if position == self._get_cursor().position():
554 568 self._call_tip()
@@ -1,148 +1,153 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 # Systemm library imports
9 9 from PyQt4 import QtGui
10 10
11 11 # Local imports
12 12 from IPython.external.argparse import ArgumentParser
13 13 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
14 14 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
15 15 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
16 16 from IPython.frontend.qt.kernelmanager import QtKernelManager
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Constants
20 20 #-----------------------------------------------------------------------------
21 21
22 22 LOCALHOST = '127.0.0.1'
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Classes
26 26 #-----------------------------------------------------------------------------
27 27
28 28 class MainWindow(QtGui.QMainWindow):
29 29
30 30 #---------------------------------------------------------------------------
31 31 # 'object' interface
32 32 #---------------------------------------------------------------------------
33 33
34 def __init__(self, frontend):
34 def __init__(self, frontend, existing=False):
35 35 """ Create a MainWindow for the specified FrontendWidget.
36 36 """
37 37 super(MainWindow, self).__init__()
38 38 self._frontend = frontend
39 self._existing = existing
39 40 self._frontend.exit_requested.connect(self.close)
40 41 self.setCentralWidget(frontend)
41 42
42 43 #---------------------------------------------------------------------------
43 44 # QWidget interface
44 45 #---------------------------------------------------------------------------
45 46
46 47 def closeEvent(self, event):
47 48 """ Reimplemented to prompt the user and close the kernel cleanly.
48 49 """
49 50 kernel_manager = self._frontend.kernel_manager
50 51 if kernel_manager and kernel_manager.channels_running:
51 52 title = self.window().windowTitle()
52 53 reply = QtGui.QMessageBox.question(self, title,
53 'Closing console. Leave Kernel alive?',
54 "Closing console. Shutdown kernel as well?\n"+
55 "'Yes' will close the kernel and all connected consoles.",
54 56 QtGui.QMessageBox.Yes, QtGui.QMessageBox.No, QtGui.QMessageBox.Cancel)
55 57 if reply == QtGui.QMessageBox.Yes:
56 self.destroy()
57 event.ignore()
58 elif reply == QtGui.QMessageBox.No:
59 58 kernel_manager.shutdown_kernel()
60 59 #kernel_manager.stop_channels()
61 60 event.accept()
61 elif reply == QtGui.QMessageBox.No:
62 if self._existing:
63 event.accept()
64 else:
65 self.destroy()
66 event.ignore()
62 67 else:
63 68 event.ignore()
64 69
65 70 #-----------------------------------------------------------------------------
66 71 # Main entry point
67 72 #-----------------------------------------------------------------------------
68 73
69 74 def main():
70 75 """ Entry point for application.
71 76 """
72 77 # Parse command line arguments.
73 78 parser = ArgumentParser()
74 79 kgroup = parser.add_argument_group('kernel options')
75 80 kgroup.add_argument('-e', '--existing', action='store_true',
76 81 help='connect to an existing kernel')
77 82 kgroup.add_argument('--ip', type=str, default=LOCALHOST,
78 83 help='set the kernel\'s IP address [default localhost]')
79 84 kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
80 85 help='set the XREQ channel port [default random]')
81 86 kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
82 87 help='set the SUB channel port [default random]')
83 88 kgroup.add_argument('--rep', type=int, metavar='PORT', default=0,
84 89 help='set the REP channel port [default random]')
85 90 kgroup.add_argument('--hb', type=int, metavar='PORT', default=0,
86 91 help='set the heartbeat port [default: random]')
87 92
88 93 egroup = kgroup.add_mutually_exclusive_group()
89 94 egroup.add_argument('--pure', action='store_true', help = \
90 95 'use a pure Python kernel instead of an IPython kernel')
91 96 egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
92 97 const='auto', help = \
93 98 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
94 99 given, the GUI backend is matplotlib's, otherwise use one of: \
95 100 ['tk', 'gtk', 'qt', 'wx', 'inline'].")
96 101
97 102 wgroup = parser.add_argument_group('widget options')
98 103 wgroup.add_argument('--paging', type=str, default='inside',
99 104 choices = ['inside', 'hsplit', 'vsplit', 'none'],
100 105 help='set the paging style [default inside]')
101 106 wgroup.add_argument('--rich', action='store_true',
102 107 help='enable rich text support')
103 108 wgroup.add_argument('--gui-completion', action='store_true',
104 109 help='use a GUI widget for tab completion')
105 110
106 111 args = parser.parse_args()
107 112
108 113 # Don't let Qt or ZMQ swallow KeyboardInterupts.
109 114 import signal
110 115 signal.signal(signal.SIGINT, signal.SIG_DFL)
111 116
112 117 # Create a KernelManager and start a kernel.
113 118 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
114 119 sub_address=(args.ip, args.sub),
115 120 rep_address=(args.ip, args.rep),
116 121 hb_address=(args.ip, args.hb))
117 122 if args.ip == LOCALHOST and not args.existing:
118 123 if args.pure:
119 124 kernel_manager.start_kernel(ipython=False)
120 125 elif args.pylab:
121 126 kernel_manager.start_kernel(pylab=args.pylab)
122 127 else:
123 128 kernel_manager.start_kernel()
124 129 kernel_manager.start_channels()
125 130
126 131 # Create the widget.
127 132 app = QtGui.QApplication([])
128 133 if args.pure:
129 134 kind = 'rich' if args.rich else 'plain'
130 135 widget = FrontendWidget(kind=kind, paging=args.paging)
131 136 elif args.rich or args.pylab:
132 137 widget = RichIPythonWidget(paging=args.paging)
133 138 else:
134 139 widget = IPythonWidget(paging=args.paging)
135 140 widget.gui_completion = args.gui_completion
136 141 widget.kernel_manager = kernel_manager
137 142
138 143 # Create the main window.
139 window = MainWindow(widget)
144 window = MainWindow(widget, args.existing)
140 145 window.setWindowTitle('Python' if args.pure else 'IPython')
141 146 window.show()
142 147
143 148 # Start the application main loop.
144 149 app.exec_()
145 150
146 151
147 152 if __name__ == '__main__':
148 153 main()
@@ -1,237 +1,240 b''
1 1 """ Defines a KernelManager that provides signals and slots.
2 2 """
3 3
4 4 # System library imports.
5 5 from PyQt4 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 10 XReqSocketChannel, RepSocketChannel, 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.pyqtSignal()
18 18
19 19 # Emitted when the channel is stopped.
20 20 stopped = QtCore.pyqtSignal()
21 21
22 22 #---------------------------------------------------------------------------
23 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 39 class QtXReqSocketChannel(SocketChannelQObject, XReqSocketChannel):
40 40
41 41 # Emitted when any message is received.
42 42 message_received = QtCore.pyqtSignal(object)
43 43
44 44 # Emitted when a reply has been received for the corresponding request
45 45 # type.
46 46 execute_reply = QtCore.pyqtSignal(object)
47 47 complete_reply = QtCore.pyqtSignal(object)
48 48 object_info_reply = QtCore.pyqtSignal(object)
49 49
50 50 # Emitted when the first reply comes back.
51 51 first_reply = QtCore.pyqtSignal()
52 52
53 53 # Used by the first_reply signal logic to determine if a reply is the
54 54 # first.
55 55 _handlers_called = False
56 56
57 57 #---------------------------------------------------------------------------
58 58 # 'XReqSocketChannel' interface
59 59 #---------------------------------------------------------------------------
60 60
61 61 def call_handlers(self, msg):
62 62 """ Reimplemented to emit signals instead of making callbacks.
63 63 """
64 64 # Emit the generic signal.
65 65 self.message_received.emit(msg)
66 66
67 67 # Emit signals for specialized message types.
68 68 msg_type = msg['msg_type']
69 69 signal = getattr(self, msg_type, None)
70 70 if signal:
71 71 signal.emit(msg)
72 72
73 73 if not self._handlers_called:
74 74 self.first_reply.emit()
75 75 self._handlers_called = True
76 76
77 77 #---------------------------------------------------------------------------
78 78 # 'QtXReqSocketChannel' interface
79 79 #---------------------------------------------------------------------------
80 80
81 81 def reset_first_reply(self):
82 82 """ Reset the first_reply signal to fire again on the next reply.
83 83 """
84 84 self._handlers_called = False
85 85
86 86
87 87 class QtSubSocketChannel(SocketChannelQObject, SubSocketChannel):
88 88
89 89 # Emitted when any message is received.
90 90 message_received = QtCore.pyqtSignal(object)
91 91
92 92 # Emitted when a message of type 'stream' is received.
93 93 stream_received = QtCore.pyqtSignal(object)
94 94
95 95 # Emitted when a message of type 'pyin' is received.
96 96 pyin_received = QtCore.pyqtSignal(object)
97 97
98 98 # Emitted when a message of type 'pyout' is received.
99 99 pyout_received = QtCore.pyqtSignal(object)
100 100
101 101 # Emitted when a message of type 'pyerr' is received.
102 102 pyerr_received = QtCore.pyqtSignal(object)
103 103
104 104 # Emitted when a crash report message is received from the kernel's
105 105 # last-resort sys.excepthook.
106 106 crash_received = QtCore.pyqtSignal(object)
107 107
108 # Emitted when a shutdown is noticed.
109 shutdown_reply_received = QtCore.pyqtSignal(object)
110
108 111 #---------------------------------------------------------------------------
109 112 # 'SubSocketChannel' interface
110 113 #---------------------------------------------------------------------------
111 114
112 115 def call_handlers(self, msg):
113 116 """ Reimplemented to emit signals instead of making callbacks.
114 117 """
115 118 # Emit the generic signal.
116 119 self.message_received.emit(msg)
117 120
118 121 # Emit signals for specialized message types.
119 122 msg_type = msg['msg_type']
120 123 signal = getattr(self, msg_type + '_received', None)
121 124 if signal:
122 125 signal.emit(msg)
123 126 elif msg_type in ('stdout', 'stderr'):
124 127 self.stream_received.emit(msg)
125 128
126 129 def flush(self):
127 130 """ Reimplemented to ensure that signals are dispatched immediately.
128 131 """
129 132 super(QtSubSocketChannel, self).flush()
130 133 QtCore.QCoreApplication.instance().processEvents()
131 134
132 135
133 136 class QtRepSocketChannel(SocketChannelQObject, RepSocketChannel):
134 137
135 138 # Emitted when any message is received.
136 139 message_received = QtCore.pyqtSignal(object)
137 140
138 141 # Emitted when an input request is received.
139 142 input_requested = QtCore.pyqtSignal(object)
140 143
141 144 #---------------------------------------------------------------------------
142 145 # 'RepSocketChannel' interface
143 146 #---------------------------------------------------------------------------
144 147
145 148 def call_handlers(self, msg):
146 149 """ Reimplemented to emit signals instead of making callbacks.
147 150 """
148 151 # Emit the generic signal.
149 152 self.message_received.emit(msg)
150 153
151 154 # Emit signals for specialized message types.
152 155 msg_type = msg['msg_type']
153 156 if msg_type == 'input_request':
154 157 self.input_requested.emit(msg)
155 158
156 159
157 160 class QtHBSocketChannel(SocketChannelQObject, HBSocketChannel):
158 161
159 162 # Emitted when the kernel has died.
160 163 kernel_died = QtCore.pyqtSignal(object)
161 164
162 165 #---------------------------------------------------------------------------
163 166 # 'HBSocketChannel' interface
164 167 #---------------------------------------------------------------------------
165 168
166 169 def call_handlers(self, since_last_heartbeat):
167 170 """ Reimplemented to emit signals instead of making callbacks.
168 171 """
169 172 # Emit the generic signal.
170 173 self.kernel_died.emit(since_last_heartbeat)
171 174
172 175
173 176 class QtKernelManager(KernelManager, SuperQObject):
174 177 """ A KernelManager that provides signals and slots.
175 178 """
176 179
177 180 __metaclass__ = MetaQObjectHasTraits
178 181
179 182 # Emitted when the kernel manager has started listening.
180 183 started_channels = QtCore.pyqtSignal()
181 184
182 185 # Emitted when the kernel manager has stopped listening.
183 186 stopped_channels = QtCore.pyqtSignal()
184 187
185 188 # Use Qt-specific channel classes that emit signals.
186 189 sub_channel_class = Type(QtSubSocketChannel)
187 190 xreq_channel_class = Type(QtXReqSocketChannel)
188 191 rep_channel_class = Type(QtRepSocketChannel)
189 192 hb_channel_class = Type(QtHBSocketChannel)
190 193
191 194 #---------------------------------------------------------------------------
192 195 # 'KernelManager' interface
193 196 #---------------------------------------------------------------------------
194 197
195 198 #------ Kernel process management ------------------------------------------
196 199
197 200 def start_kernel(self, *args, **kw):
198 201 """ Reimplemented for proper heartbeat management.
199 202 """
200 203 if self._xreq_channel is not None:
201 204 self._xreq_channel.reset_first_reply()
202 205 super(QtKernelManager, self).start_kernel(*args, **kw)
203 206
204 207 #------ Channel management -------------------------------------------------
205 208
206 209 def start_channels(self, *args, **kw):
207 210 """ Reimplemented to emit signal.
208 211 """
209 212 super(QtKernelManager, self).start_channels(*args, **kw)
210 213 self.started_channels.emit()
211 214
212 215 def stop_channels(self):
213 216 """ Reimplemented to emit signal.
214 217 """
215 218 super(QtKernelManager, self).stop_channels()
216 219 self.stopped_channels.emit()
217 220
218 221 @property
219 222 def xreq_channel(self):
220 223 """ Reimplemented for proper heartbeat management.
221 224 """
222 225 if self._xreq_channel is None:
223 226 self._xreq_channel = super(QtKernelManager, self).xreq_channel
224 227 self._xreq_channel.first_reply.connect(self._first_reply)
225 228 return self._xreq_channel
226 229
227 230 #---------------------------------------------------------------------------
228 231 # Protected interface
229 232 #---------------------------------------------------------------------------
230 233
231 234 def _first_reply(self):
232 235 """ Unpauses the heartbeat channel when the first reply is received on
233 236 the execute channel. Note that this will *not* start the heartbeat
234 237 channel if it is not already running!
235 238 """
236 239 if self._hb_channel is not None:
237 240 self._hb_channel.unpause()
General Comments 0
You need to be logged in to leave comments. Login now