##// END OF EJS Templates
tab management new/existing kernel....
Matthias BUSSONNIER -
Show More
@@ -1,638 +1,638 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, Unicode
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 understands and ignores prompts.
26 26 """
27 27
28 28 def __init__(self, frontend):
29 29 super(FrontendHighlighter, self).__init__(frontend._control.document())
30 30 self._current_offset = 0
31 31 self._frontend = frontend
32 32 self.highlighting_on = False
33 33
34 34 def highlightBlock(self, string):
35 35 """ Highlight a block of text. Reimplemented to highlight selectively.
36 36 """
37 37 if not self.highlighting_on:
38 38 return
39 39
40 40 # The input to this function is a unicode string that may contain
41 41 # paragraph break characters, non-breaking spaces, etc. Here we acquire
42 42 # the string as plain text so we can compare it.
43 43 current_block = self.currentBlock()
44 44 string = self._frontend._get_block_plain_text(current_block)
45 45
46 46 # Decide whether to check for the regular or continuation prompt.
47 47 if current_block.contains(self._frontend._prompt_pos):
48 48 prompt = self._frontend._prompt
49 49 else:
50 50 prompt = self._frontend._continuation_prompt
51 51
52 52 # Only highlight if we can identify a prompt, but make sure not to
53 53 # highlight the prompt.
54 54 if string.startswith(prompt):
55 55 self._current_offset = len(prompt)
56 56 string = string[len(prompt):]
57 57 super(FrontendHighlighter, self).highlightBlock(string)
58 58
59 59 def rehighlightBlock(self, block):
60 60 """ Reimplemented to temporarily enable highlighting if disabled.
61 61 """
62 62 old = self.highlighting_on
63 63 self.highlighting_on = True
64 64 super(FrontendHighlighter, self).rehighlightBlock(block)
65 65 self.highlighting_on = old
66 66
67 67 def setFormat(self, start, count, format):
68 68 """ Reimplemented to highlight selectively.
69 69 """
70 70 start += self._current_offset
71 71 super(FrontendHighlighter, self).setFormat(start, count, format)
72 72
73 73
74 74 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
75 75 """ A Qt frontend for a generic Python kernel.
76 76 """
77 77
78 78 # The text to show when the kernel is (re)started.
79 79 banner = Unicode()
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 # Whether to automatically show calltips on open-parentheses.
93 93 enable_calltips = Bool(True, config=True,
94 94 help="Whether to draw information calltips on open-parentheses.")
95 95
96 96 # Emitted when a user visible 'execute_request' has been submitted to the
97 97 # kernel from the FrontendWidget. Contains the code to be executed.
98 98 executing = QtCore.Signal(object)
99 99
100 100 # Emitted when a user-visible 'execute_reply' has been received from the
101 101 # kernel and processed by the FrontendWidget. Contains the response message.
102 102 executed = QtCore.Signal(object)
103 103
104 104 # Emitted when an exit request has been received from the kernel.
105 exit_requested = QtCore.Signal()
105 exit_requested = QtCore.Signal(object)
106 106
107 107 # Protected class variables.
108 108 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
109 109 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
110 110 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
111 111 _input_splitter_class = InputSplitter
112 112 _local_kernel = False
113 113 _highlighter = Instance(FrontendHighlighter)
114 114
115 115 #---------------------------------------------------------------------------
116 116 # 'object' interface
117 117 #---------------------------------------------------------------------------
118 118
119 119 def __init__(self, *args, **kw):
120 120 super(FrontendWidget, self).__init__(*args, **kw)
121 121
122 122 # FrontendWidget protected variables.
123 123 self._bracket_matcher = BracketMatcher(self._control)
124 124 self._call_tip_widget = CallTipWidget(self._control)
125 125 self._completion_lexer = CompletionLexer(PythonLexer())
126 126 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
127 127 self._hidden = False
128 128 self._highlighter = FrontendHighlighter(self)
129 129 self._input_splitter = self._input_splitter_class(input_mode='cell')
130 130 self._kernel_manager = None
131 131 self._request_info = {}
132 132
133 133 # Configure the ConsoleWidget.
134 134 self.tab_width = 4
135 135 self._set_continuation_prompt('... ')
136 136
137 137 # Configure the CallTipWidget.
138 138 self._call_tip_widget.setFont(self.font)
139 139 self.font_changed.connect(self._call_tip_widget.setFont)
140 140
141 141 # Configure actions.
142 142 action = self._copy_raw_action
143 143 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
144 144 action.setEnabled(False)
145 145 action.setShortcut(QtGui.QKeySequence(key))
146 146 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
147 147 action.triggered.connect(self.copy_raw)
148 148 self.copy_available.connect(action.setEnabled)
149 149 self.addAction(action)
150 150
151 151 # Connect signal handlers.
152 152 document = self._control.document()
153 153 document.contentsChange.connect(self._document_contents_change)
154 154
155 155 # Set flag for whether we are connected via localhost.
156 156 self._local_kernel = kw.get('local_kernel',
157 157 FrontendWidget._local_kernel)
158 158
159 159 #---------------------------------------------------------------------------
160 160 # 'ConsoleWidget' public interface
161 161 #---------------------------------------------------------------------------
162 162
163 163 def copy(self):
164 164 """ Copy the currently selected text to the clipboard, removing prompts.
165 165 """
166 166 text = self._control.textCursor().selection().toPlainText()
167 167 if text:
168 168 lines = map(transform_classic_prompt, text.splitlines())
169 169 text = '\n'.join(lines)
170 170 QtGui.QApplication.clipboard().setText(text)
171 171
172 172 #---------------------------------------------------------------------------
173 173 # 'ConsoleWidget' abstract interface
174 174 #---------------------------------------------------------------------------
175 175
176 176 def _is_complete(self, source, interactive):
177 177 """ Returns whether 'source' can be completely processed and a new
178 178 prompt created. When triggered by an Enter/Return key press,
179 179 'interactive' is True; otherwise, it is False.
180 180 """
181 181 complete = self._input_splitter.push(source)
182 182 if interactive:
183 183 complete = not self._input_splitter.push_accepts_more()
184 184 return complete
185 185
186 186 def _execute(self, source, hidden):
187 187 """ Execute 'source'. If 'hidden', do not show any output.
188 188
189 189 See parent class :meth:`execute` docstring for full details.
190 190 """
191 191 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
192 192 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
193 193 self._hidden = hidden
194 194 if not hidden:
195 195 self.executing.emit(source)
196 196
197 197 def _prompt_started_hook(self):
198 198 """ Called immediately after a new prompt is displayed.
199 199 """
200 200 if not self._reading:
201 201 self._highlighter.highlighting_on = True
202 202
203 203 def _prompt_finished_hook(self):
204 204 """ Called immediately after a prompt is finished, i.e. when some input
205 205 will be processed and a new prompt displayed.
206 206 """
207 207 # Flush all state from the input splitter so the next round of
208 208 # reading input starts with a clean buffer.
209 209 self._input_splitter.reset()
210 210
211 211 if not self._reading:
212 212 self._highlighter.highlighting_on = False
213 213
214 214 def _tab_pressed(self):
215 215 """ Called when the tab key is pressed. Returns whether to continue
216 216 processing the event.
217 217 """
218 218 # Perform tab completion if:
219 219 # 1) The cursor is in the input buffer.
220 220 # 2) There is a non-whitespace character before the cursor.
221 221 text = self._get_input_buffer_cursor_line()
222 222 if text is None:
223 223 return False
224 224 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
225 225 if complete:
226 226 self._complete()
227 227 return not complete
228 228
229 229 #---------------------------------------------------------------------------
230 230 # 'ConsoleWidget' protected interface
231 231 #---------------------------------------------------------------------------
232 232
233 233 def _context_menu_make(self, pos):
234 234 """ Reimplemented to add an action for raw copy.
235 235 """
236 236 menu = super(FrontendWidget, self)._context_menu_make(pos)
237 237 for before_action in menu.actions():
238 238 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
239 239 QtGui.QKeySequence.ExactMatch:
240 240 menu.insertAction(before_action, self._copy_raw_action)
241 241 break
242 242 return menu
243 243
244 244 def _event_filter_console_keypress(self, event):
245 245 """ Reimplemented for execution interruption and smart backspace.
246 246 """
247 247 key = event.key()
248 248 if self._control_key_down(event.modifiers(), include_command=False):
249 249
250 250 if key == QtCore.Qt.Key_C and self._executing:
251 251 self.interrupt_kernel()
252 252 return True
253 253
254 254 elif key == QtCore.Qt.Key_Period:
255 255 message = 'Are you sure you want to restart the kernel?'
256 256 self.restart_kernel(message, now=False)
257 257 return True
258 258
259 259 elif not event.modifiers() & QtCore.Qt.AltModifier:
260 260
261 261 # Smart backspace: remove four characters in one backspace if:
262 262 # 1) everything left of the cursor is whitespace
263 263 # 2) the four characters immediately left of the cursor are spaces
264 264 if key == QtCore.Qt.Key_Backspace:
265 265 col = self._get_input_buffer_cursor_column()
266 266 cursor = self._control.textCursor()
267 267 if col > 3 and not cursor.hasSelection():
268 268 text = self._get_input_buffer_cursor_line()[:col]
269 269 if text.endswith(' ') and not text.strip():
270 270 cursor.movePosition(QtGui.QTextCursor.Left,
271 271 QtGui.QTextCursor.KeepAnchor, 4)
272 272 cursor.removeSelectedText()
273 273 return True
274 274
275 275 return super(FrontendWidget, self)._event_filter_console_keypress(event)
276 276
277 277 def _insert_continuation_prompt(self, cursor):
278 278 """ Reimplemented for auto-indentation.
279 279 """
280 280 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
281 281 cursor.insertText(' ' * self._input_splitter.indent_spaces)
282 282
283 283 #---------------------------------------------------------------------------
284 284 # 'BaseFrontendMixin' abstract interface
285 285 #---------------------------------------------------------------------------
286 286
287 287 def _handle_complete_reply(self, rep):
288 288 """ Handle replies for tab completion.
289 289 """
290 290 self.log.debug("complete: %s", rep.get('content', ''))
291 291 cursor = self._get_cursor()
292 292 info = self._request_info.get('complete')
293 293 if info and info.id == rep['parent_header']['msg_id'] and \
294 294 info.pos == cursor.position():
295 295 text = '.'.join(self._get_context())
296 296 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
297 297 self._complete_with_items(cursor, rep['content']['matches'])
298 298
299 299 def _handle_execute_reply(self, msg):
300 300 """ Handles replies for code execution.
301 301 """
302 302 self.log.debug("execute: %s", msg.get('content', ''))
303 303 info = self._request_info.get('execute')
304 304 if info and info.id == msg['parent_header']['msg_id'] and \
305 305 info.kind == 'user' and not self._hidden:
306 306 # Make sure that all output from the SUB channel has been processed
307 307 # before writing a new prompt.
308 308 self.kernel_manager.sub_channel.flush()
309 309
310 310 # Reset the ANSI style information to prevent bad text in stdout
311 311 # from messing up our colors. We're not a true terminal so we're
312 312 # allowed to do this.
313 313 if self.ansi_codes:
314 314 self._ansi_processor.reset_sgr()
315 315
316 316 content = msg['content']
317 317 status = content['status']
318 318 if status == 'ok':
319 319 self._process_execute_ok(msg)
320 320 elif status == 'error':
321 321 self._process_execute_error(msg)
322 322 elif status == 'abort':
323 323 self._process_execute_abort(msg)
324 324
325 325 self._show_interpreter_prompt_for_reply(msg)
326 326 self.executed.emit(msg)
327 327 else:
328 328 super(FrontendWidget, self)._handle_execute_reply(msg)
329 329
330 330 def _handle_input_request(self, msg):
331 331 """ Handle requests for raw_input.
332 332 """
333 333 self.log.debug("input: %s", msg.get('content', ''))
334 334 if self._hidden:
335 335 raise RuntimeError('Request for raw input during hidden execution.')
336 336
337 337 # Make sure that all output from the SUB channel has been processed
338 338 # before entering readline mode.
339 339 self.kernel_manager.sub_channel.flush()
340 340
341 341 def callback(line):
342 342 self.kernel_manager.stdin_channel.input(line)
343 343 self._readline(msg['content']['prompt'], callback=callback)
344 344
345 345 def _handle_kernel_died(self, since_last_heartbeat):
346 346 """ Handle the kernel's death by asking if the user wants to restart.
347 347 """
348 348 self.log.debug("kernel died: %s", since_last_heartbeat)
349 349 if self.custom_restart:
350 350 self.custom_restart_kernel_died.emit(since_last_heartbeat)
351 351 else:
352 352 message = 'The kernel heartbeat has been inactive for %.2f ' \
353 353 'seconds. Do you want to restart the kernel? You may ' \
354 354 'first want to check the network connection.' % \
355 355 since_last_heartbeat
356 356 self.restart_kernel(message, now=True)
357 357
358 358 def _handle_object_info_reply(self, rep):
359 359 """ Handle replies for call tips.
360 360 """
361 361 self.log.debug("oinfo: %s", rep.get('content', ''))
362 362 cursor = self._get_cursor()
363 363 info = self._request_info.get('call_tip')
364 364 if info and info.id == rep['parent_header']['msg_id'] and \
365 365 info.pos == cursor.position():
366 366 # Get the information for a call tip. For now we format the call
367 367 # line as string, later we can pass False to format_call and
368 368 # syntax-highlight it ourselves for nicer formatting in the
369 369 # calltip.
370 370 content = rep['content']
371 371 # if this is from pykernel, 'docstring' will be the only key
372 372 if content.get('ismagic', False):
373 373 # Don't generate a call-tip for magics. Ideally, we should
374 374 # generate a tooltip, but not on ( like we do for actual
375 375 # callables.
376 376 call_info, doc = None, None
377 377 else:
378 378 call_info, doc = call_tip(content, format_call=True)
379 379 if call_info or doc:
380 380 self._call_tip_widget.show_call_info(call_info, doc)
381 381
382 382 def _handle_pyout(self, msg):
383 383 """ Handle display hook output.
384 384 """
385 385 self.log.debug("pyout: %s", msg.get('content', ''))
386 386 if not self._hidden and self._is_from_this_session(msg):
387 387 text = msg['content']['data']
388 388 self._append_plain_text(text + '\n', before_prompt=True)
389 389
390 390 def _handle_stream(self, msg):
391 391 """ Handle stdout, stderr, and stdin.
392 392 """
393 393 self.log.debug("stream: %s", msg.get('content', ''))
394 394 if not self._hidden and self._is_from_this_session(msg):
395 395 # Most consoles treat tabs as being 8 space characters. Convert tabs
396 396 # to spaces so that output looks as expected regardless of this
397 397 # widget's tab width.
398 398 text = msg['content']['data'].expandtabs(8)
399 399
400 400 self._append_plain_text(text, before_prompt=True)
401 401 self._control.moveCursor(QtGui.QTextCursor.End)
402 402
403 403 def _handle_shutdown_reply(self, msg):
404 404 """ Handle shutdown signal, only if from other console.
405 405 """
406 406 self.log.debug("shutdown: %s", msg.get('content', ''))
407 407 if not self._hidden and not self._is_from_this_session(msg):
408 408 if self._local_kernel:
409 409 if not msg['content']['restart']:
410 410 sys.exit(0)
411 411 else:
412 412 # we just got notified of a restart!
413 413 time.sleep(0.25) # wait 1/4 sec to reset
414 414 # lest the request for a new prompt
415 415 # goes to the old kernel
416 416 self.reset()
417 417 else: # remote kernel, prompt on Kernel shutdown/reset
418 418 title = self.window().windowTitle()
419 419 if not msg['content']['restart']:
420 420 reply = QtGui.QMessageBox.question(self, title,
421 421 "Kernel has been shutdown permanently. "
422 422 "Close the Console?",
423 423 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
424 424 if reply == QtGui.QMessageBox.Yes:
425 sys.exit(0)
425 self.exit_requested.emit(self)
426 426 else:
427 427 reply = QtGui.QMessageBox.question(self, title,
428 428 "Kernel has been reset. Clear the Console?",
429 429 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
430 430 if reply == QtGui.QMessageBox.Yes:
431 431 time.sleep(0.25) # wait 1/4 sec to reset
432 432 # lest the request for a new prompt
433 433 # goes to the old kernel
434 434 self.reset()
435 435
436 436 def _started_channels(self):
437 437 """ Called when the KernelManager channels have started listening or
438 438 when the frontend is assigned an already listening KernelManager.
439 439 """
440 440 self.reset()
441 441
442 442 #---------------------------------------------------------------------------
443 443 # 'FrontendWidget' public interface
444 444 #---------------------------------------------------------------------------
445 445
446 446 def copy_raw(self):
447 447 """ Copy the currently selected text to the clipboard without attempting
448 448 to remove prompts or otherwise alter the text.
449 449 """
450 450 self._control.copy()
451 451
452 452 def execute_file(self, path, hidden=False):
453 453 """ Attempts to execute file with 'path'. If 'hidden', no output is
454 454 shown.
455 455 """
456 456 self.execute('execfile(%r)' % path, hidden=hidden)
457 457
458 458 def interrupt_kernel(self):
459 459 """ Attempts to interrupt the running kernel.
460 460 """
461 461 if self.custom_interrupt:
462 462 self.custom_interrupt_requested.emit()
463 463 elif self.kernel_manager.has_kernel:
464 464 self.kernel_manager.interrupt_kernel()
465 465 else:
466 466 self._append_plain_text('Kernel process is either remote or '
467 467 'unspecified. Cannot interrupt.\n')
468 468
469 469 def reset(self):
470 470 """ Resets the widget to its initial state. Similar to ``clear``, but
471 471 also re-writes the banner and aborts execution if necessary.
472 472 """
473 473 if self._executing:
474 474 self._executing = False
475 475 self._request_info['execute'] = None
476 476 self._reading = False
477 477 self._highlighter.highlighting_on = False
478 478
479 479 self._control.clear()
480 480 self._append_plain_text(self.banner)
481 481 self._show_interpreter_prompt()
482 482
483 483 def restart_kernel(self, message, now=False):
484 484 """ Attempts to restart the running kernel.
485 485 """
486 486 # FIXME: now should be configurable via a checkbox in the dialog. Right
487 487 # now at least the heartbeat path sets it to True and the manual restart
488 488 # to False. But those should just be the pre-selected states of a
489 489 # checkbox that the user could override if so desired. But I don't know
490 490 # enough Qt to go implementing the checkbox now.
491 491
492 492 if self.custom_restart:
493 493 self.custom_restart_requested.emit()
494 494
495 495 elif self.kernel_manager.has_kernel:
496 496 # Pause the heart beat channel to prevent further warnings.
497 497 self.kernel_manager.hb_channel.pause()
498 498
499 499 # Prompt the user to restart the kernel. Un-pause the heartbeat if
500 500 # they decline. (If they accept, the heartbeat will be un-paused
501 501 # automatically when the kernel is restarted.)
502 502 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
503 503 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
504 504 message, buttons)
505 505 if result == QtGui.QMessageBox.Yes:
506 506 try:
507 507 self.kernel_manager.restart_kernel(now=now)
508 508 except RuntimeError:
509 509 self._append_plain_text('Kernel started externally. '
510 510 'Cannot restart.\n')
511 511 else:
512 512 self.reset()
513 513 else:
514 514 self.kernel_manager.hb_channel.unpause()
515 515
516 516 else:
517 517 self._append_plain_text('Kernel process is either remote or '
518 518 'unspecified. Cannot restart.\n')
519 519
520 520 #---------------------------------------------------------------------------
521 521 # 'FrontendWidget' protected interface
522 522 #---------------------------------------------------------------------------
523 523
524 524 def _call_tip(self):
525 525 """ Shows a call tip, if appropriate, at the current cursor location.
526 526 """
527 527 # Decide if it makes sense to show a call tip
528 528 if not self.enable_calltips:
529 529 return False
530 530 cursor = self._get_cursor()
531 531 cursor.movePosition(QtGui.QTextCursor.Left)
532 532 if cursor.document().characterAt(cursor.position()) != '(':
533 533 return False
534 534 context = self._get_context(cursor)
535 535 if not context:
536 536 return False
537 537
538 538 # Send the metadata request to the kernel
539 539 name = '.'.join(context)
540 540 msg_id = self.kernel_manager.shell_channel.object_info(name)
541 541 pos = self._get_cursor().position()
542 542 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
543 543 return True
544 544
545 545 def _complete(self):
546 546 """ Performs completion at the current cursor location.
547 547 """
548 548 context = self._get_context()
549 549 if context:
550 550 # Send the completion request to the kernel
551 551 msg_id = self.kernel_manager.shell_channel.complete(
552 552 '.'.join(context), # text
553 553 self._get_input_buffer_cursor_line(), # line
554 554 self._get_input_buffer_cursor_column(), # cursor_pos
555 555 self.input_buffer) # block
556 556 pos = self._get_cursor().position()
557 557 info = self._CompletionRequest(msg_id, pos)
558 558 self._request_info['complete'] = info
559 559
560 560 def _get_context(self, cursor=None):
561 561 """ Gets the context for the specified cursor (or the current cursor
562 562 if none is specified).
563 563 """
564 564 if cursor is None:
565 565 cursor = self._get_cursor()
566 566 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
567 567 QtGui.QTextCursor.KeepAnchor)
568 568 text = cursor.selection().toPlainText()
569 569 return self._completion_lexer.get_context(text)
570 570
571 571 def _process_execute_abort(self, msg):
572 572 """ Process a reply for an aborted execution request.
573 573 """
574 574 self._append_plain_text("ERROR: execution aborted\n")
575 575
576 576 def _process_execute_error(self, msg):
577 577 """ Process a reply for an execution request that resulted in an error.
578 578 """
579 579 content = msg['content']
580 580 # If a SystemExit is passed along, this means exit() was called - also
581 581 # all the ipython %exit magic syntax of '-k' to be used to keep
582 582 # the kernel running
583 583 if content['ename']=='SystemExit':
584 584 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
585 585 self._keep_kernel_on_exit = keepkernel
586 self.exit_requested.emit()
586 self.exit_requested.emit(self)
587 587 else:
588 588 traceback = ''.join(content['traceback'])
589 589 self._append_plain_text(traceback)
590 590
591 591 def _process_execute_ok(self, msg):
592 592 """ Process a reply for a successful execution equest.
593 593 """
594 594 payload = msg['content']['payload']
595 595 for item in payload:
596 596 if not self._process_execute_payload(item):
597 597 warning = 'Warning: received unknown payload of type %s'
598 598 print(warning % repr(item['source']))
599 599
600 600 def _process_execute_payload(self, item):
601 601 """ Process a single payload item from the list of payload items in an
602 602 execution reply. Returns whether the payload was handled.
603 603 """
604 604 # The basic FrontendWidget doesn't handle payloads, as they are a
605 605 # mechanism for going beyond the standard Python interpreter model.
606 606 return False
607 607
608 608 def _show_interpreter_prompt(self):
609 609 """ Shows a prompt for the interpreter.
610 610 """
611 611 self._show_prompt('>>> ')
612 612
613 613 def _show_interpreter_prompt_for_reply(self, msg):
614 614 """ Shows a prompt for the interpreter given an 'execute_reply' message.
615 615 """
616 616 self._show_interpreter_prompt()
617 617
618 618 #------ Signal handlers ----------------------------------------------------
619 619
620 620 def _document_contents_change(self, position, removed, added):
621 621 """ Called whenever the document's content changes. Display a call tip
622 622 if appropriate.
623 623 """
624 624 # Calculate where the cursor should be *after* the change:
625 625 position += added
626 626
627 627 document = self._control.document()
628 628 if position == self._get_cursor().position():
629 629 self._call_tip()
630 630
631 631 #------ Trait default initializers -----------------------------------------
632 632
633 633 def _banner_default(self):
634 634 """ Returns the standard Python banner.
635 635 """
636 636 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
637 637 '"license" for more information.'
638 638 return banner % (sys.version, sys.platform)
@@ -1,549 +1,549 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 import time
16 16 from textwrap import dedent
17 17
18 18 # System library imports
19 19 from IPython.external.qt import QtCore, QtGui
20 20
21 21 # Local imports
22 22 from IPython.core.inputsplitter import IPythonInputSplitter, \
23 23 transform_ipy_prompt
24 24 from IPython.utils.traitlets import Bool, 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 if sys.platform.startswith('win'):
44 44 default_editor = 'notepad'
45 45 else:
46 46 default_editor = ''
47 47
48 48 #-----------------------------------------------------------------------------
49 49 # IPythonWidget class
50 50 #-----------------------------------------------------------------------------
51 51
52 52 class IPythonWidget(FrontendWidget):
53 53 """ A FrontendWidget for an IPython kernel.
54 54 """
55 55
56 56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 58 # settings.
59 59 custom_edit = Bool(False)
60 60 custom_edit_requested = QtCore.Signal(object, object)
61 61
62 62 editor = Unicode(default_editor, config=True,
63 63 help="""
64 64 A command for invoking a system text editor. If the string contains a
65 65 {filename} format specifier, it will be used. Otherwise, the filename
66 66 will be appended to the end the command.
67 67 """)
68 68
69 69 editor_line = Unicode(config=True,
70 70 help="""
71 71 The editor command to use when a specific line number is requested. The
72 72 string should contain two format specifiers: {line} and {filename}. If
73 73 this parameter is not specified, the line number option to the %edit
74 74 magic will be ignored.
75 75 """)
76 76
77 77 style_sheet = Unicode(config=True,
78 78 help="""
79 79 A CSS stylesheet. The stylesheet can contain classes for:
80 80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 83 """)
84 84
85 85 syntax_style = Unicode(config=True,
86 86 help="""
87 87 If not empty, use this Pygments style for syntax highlighting.
88 88 Otherwise, the style sheet is queried for Pygments style
89 89 information.
90 90 """)
91 91
92 92 # Prompts.
93 93 in_prompt = Unicode(default_in_prompt, config=True)
94 94 out_prompt = Unicode(default_out_prompt, config=True)
95 95 input_sep = Unicode(default_input_sep, config=True)
96 96 output_sep = Unicode(default_output_sep, config=True)
97 97 output_sep2 = Unicode(default_output_sep2, config=True)
98 98
99 99 # FrontendWidget protected class variables.
100 100 _input_splitter_class = IPythonInputSplitter
101 101
102 102 # IPythonWidget protected class variables.
103 103 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
104 104 _payload_source_edit = zmq_shell_source + '.edit_magic'
105 105 _payload_source_exit = zmq_shell_source + '.ask_exit'
106 106 _payload_source_next_input = zmq_shell_source + '.set_next_input'
107 107 _payload_source_page = 'IPython.zmq.page.page'
108 108 _retrying_history_request = False
109 109
110 110 #---------------------------------------------------------------------------
111 111 # 'object' interface
112 112 #---------------------------------------------------------------------------
113 113
114 114 def __init__(self, *args, **kw):
115 115 super(IPythonWidget, self).__init__(*args, **kw)
116 116
117 117 # IPythonWidget protected variables.
118 118 self._payload_handlers = {
119 119 self._payload_source_edit : self._handle_payload_edit,
120 120 self._payload_source_exit : self._handle_payload_exit,
121 121 self._payload_source_page : self._handle_payload_page,
122 122 self._payload_source_next_input : self._handle_payload_next_input }
123 123 self._previous_prompt_obj = None
124 124 self._keep_kernel_on_exit = None
125 125
126 126 # Initialize widget styling.
127 127 if self.style_sheet:
128 128 self._style_sheet_changed()
129 129 self._syntax_style_changed()
130 130 else:
131 131 self.set_default_style()
132 132
133 133 #---------------------------------------------------------------------------
134 134 # 'BaseFrontendMixin' abstract interface
135 135 #---------------------------------------------------------------------------
136 136
137 137 def _handle_complete_reply(self, rep):
138 138 """ Reimplemented to support IPython's improved completion machinery.
139 139 """
140 140 self.log.debug("complete: %s", rep.get('content', ''))
141 141 cursor = self._get_cursor()
142 142 info = self._request_info.get('complete')
143 143 if info and info.id == rep['parent_header']['msg_id'] and \
144 144 info.pos == cursor.position():
145 145 matches = rep['content']['matches']
146 146 text = rep['content']['matched_text']
147 147 offset = len(text)
148 148
149 149 # Clean up matches with period and path separators if the matched
150 150 # text has not been transformed. This is done by truncating all
151 151 # but the last component and then suitably decreasing the offset
152 152 # between the current cursor position and the start of completion.
153 153 if len(matches) > 1 and matches[0][:offset] == text:
154 154 parts = re.split(r'[./\\]', text)
155 155 sep_count = len(parts) - 1
156 156 if sep_count:
157 157 chop_length = sum(map(len, parts[:sep_count])) + sep_count
158 158 matches = [ match[chop_length:] for match in matches ]
159 159 offset -= chop_length
160 160
161 161 # Move the cursor to the start of the match and complete.
162 162 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
163 163 self._complete_with_items(cursor, matches)
164 164
165 165 def _handle_execute_reply(self, msg):
166 166 """ Reimplemented to support prompt requests.
167 167 """
168 168 info = self._request_info.get('execute')
169 169 if info and info.id == msg['parent_header']['msg_id']:
170 170 if info.kind == 'prompt':
171 171 number = msg['content']['execution_count'] + 1
172 172 self._show_interpreter_prompt(number)
173 173 else:
174 174 super(IPythonWidget, self)._handle_execute_reply(msg)
175 175
176 176 def _handle_history_reply(self, msg):
177 177 """ Implemented to handle history tail replies, which are only supported
178 178 by the IPython kernel.
179 179 """
180 180 self.log.debug("history: %s", msg.get('content', ''))
181 181 content = msg['content']
182 182 if 'history' not in content:
183 183 self.log.error("History request failed: %r"%content)
184 184 if content.get('status', '') == 'aborted' and \
185 185 not self._retrying_history_request:
186 186 # a *different* action caused this request to be aborted, so
187 187 # we should try again.
188 188 self.log.error("Retrying aborted history request")
189 189 # prevent multiple retries of aborted requests:
190 190 self._retrying_history_request = True
191 191 # wait out the kernel's queue flush, which is currently timed at 0.1s
192 192 time.sleep(0.25)
193 193 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
194 194 else:
195 195 self._retrying_history_request = False
196 196 return
197 197 # reset retry flag
198 198 self._retrying_history_request = False
199 199 history_items = content['history']
200 200 items = [ line.rstrip() for _, _, line in history_items ]
201 201 self._set_history(items)
202 202
203 203 def _handle_pyout(self, msg):
204 204 """ Reimplemented for IPython-style "display hook".
205 205 """
206 206 self.log.debug("pyout: %s", msg.get('content', ''))
207 207 if not self._hidden and self._is_from_this_session(msg):
208 208 content = msg['content']
209 209 prompt_number = content['execution_count']
210 210 data = content['data']
211 211 if data.has_key('text/html'):
212 212 self._append_plain_text(self.output_sep, True)
213 213 self._append_html(self._make_out_prompt(prompt_number), True)
214 214 html = data['text/html']
215 215 self._append_plain_text('\n', True)
216 216 self._append_html(html + self.output_sep2, True)
217 217 elif data.has_key('text/plain'):
218 218 self._append_plain_text(self.output_sep, True)
219 219 self._append_html(self._make_out_prompt(prompt_number), True)
220 220 text = data['text/plain']
221 221 # If the repr is multiline, make sure we start on a new line,
222 222 # so that its lines are aligned.
223 223 if "\n" in text and not self.output_sep.endswith("\n"):
224 224 self._append_plain_text('\n', True)
225 225 self._append_plain_text(text + self.output_sep2, True)
226 226
227 227 def _handle_display_data(self, msg):
228 228 """ The base handler for the ``display_data`` message.
229 229 """
230 230 self.log.debug("display: %s", msg.get('content', ''))
231 231 # For now, we don't display data from other frontends, but we
232 232 # eventually will as this allows all frontends to monitor the display
233 233 # data. But we need to figure out how to handle this in the GUI.
234 234 if not self._hidden and self._is_from_this_session(msg):
235 235 source = msg['content']['source']
236 236 data = msg['content']['data']
237 237 metadata = msg['content']['metadata']
238 238 # In the regular IPythonWidget, we simply print the plain text
239 239 # representation.
240 240 if data.has_key('text/html'):
241 241 html = data['text/html']
242 242 self._append_html(html, True)
243 243 elif data.has_key('text/plain'):
244 244 text = data['text/plain']
245 245 self._append_plain_text(text, True)
246 246 # This newline seems to be needed for text and html output.
247 247 self._append_plain_text(u'\n', True)
248 248
249 249 def _started_channels(self):
250 250 """ Reimplemented to make a history request.
251 251 """
252 252 super(IPythonWidget, self)._started_channels()
253 253 self.kernel_manager.shell_channel.history(hist_access_type='tail',
254 254 n=1000)
255 255 #---------------------------------------------------------------------------
256 256 # 'ConsoleWidget' public interface
257 257 #---------------------------------------------------------------------------
258 258
259 259 def copy(self):
260 260 """ Copy the currently selected text to the clipboard, removing prompts
261 261 if possible.
262 262 """
263 263 text = self._control.textCursor().selection().toPlainText()
264 264 if text:
265 265 lines = map(transform_ipy_prompt, text.splitlines())
266 266 text = '\n'.join(lines)
267 267 QtGui.QApplication.clipboard().setText(text)
268 268
269 269 #---------------------------------------------------------------------------
270 270 # 'FrontendWidget' public interface
271 271 #---------------------------------------------------------------------------
272 272
273 273 def execute_file(self, path, hidden=False):
274 274 """ Reimplemented to use the 'run' magic.
275 275 """
276 276 # Use forward slashes on Windows to avoid escaping each separator.
277 277 if sys.platform == 'win32':
278 278 path = os.path.normpath(path).replace('\\', '/')
279 279
280 280 # Perhaps we should not be using %run directly, but while we
281 281 # are, it is necessary to quote filenames containing spaces or quotes.
282 282 # Escaping quotes in filename in %run seems tricky and inconsistent,
283 283 # so not trying it at present.
284 284 if '"' in path:
285 285 if "'" in path:
286 286 raise ValueError("Can't run filename containing both single "
287 287 "and double quotes: %s" % path)
288 288 path = "'%s'" % path
289 289 elif ' ' in path or "'" in path:
290 290 path = '"%s"' % path
291 291
292 292 self.execute('%%run %s' % path, hidden=hidden)
293 293
294 294 #---------------------------------------------------------------------------
295 295 # 'FrontendWidget' protected interface
296 296 #---------------------------------------------------------------------------
297 297
298 298 def _complete(self):
299 299 """ Reimplemented to support IPython's improved completion machinery.
300 300 """
301 301 # We let the kernel split the input line, so we *always* send an empty
302 302 # text field. Readline-based frontends do get a real text field which
303 303 # they can use.
304 304 text = ''
305 305
306 306 # Send the completion request to the kernel
307 307 msg_id = self.kernel_manager.shell_channel.complete(
308 308 text, # text
309 309 self._get_input_buffer_cursor_line(), # line
310 310 self._get_input_buffer_cursor_column(), # cursor_pos
311 311 self.input_buffer) # block
312 312 pos = self._get_cursor().position()
313 313 info = self._CompletionRequest(msg_id, pos)
314 314 self._request_info['complete'] = info
315 315
316 316 def _process_execute_error(self, msg):
317 317 """ Reimplemented for IPython-style traceback formatting.
318 318 """
319 319 content = msg['content']
320 320 traceback = '\n'.join(content['traceback']) + '\n'
321 321 if False:
322 322 # FIXME: For now, tracebacks come as plain text, so we can't use
323 323 # the html renderer yet. Once we refactor ultratb to produce
324 324 # properly styled tracebacks, this branch should be the default
325 325 traceback = traceback.replace(' ', '&nbsp;')
326 326 traceback = traceback.replace('\n', '<br/>')
327 327
328 328 ename = content['ename']
329 329 ename_styled = '<span class="error">%s</span>' % ename
330 330 traceback = traceback.replace(ename, ename_styled)
331 331
332 332 self._append_html(traceback)
333 333 else:
334 334 # This is the fallback for now, using plain text with ansi escapes
335 335 self._append_plain_text(traceback)
336 336
337 337 def _process_execute_payload(self, item):
338 338 """ Reimplemented to dispatch payloads to handler methods.
339 339 """
340 340 handler = self._payload_handlers.get(item['source'])
341 341 if handler is None:
342 342 # We have no handler for this type of payload, simply ignore it
343 343 return False
344 344 else:
345 345 handler(item)
346 346 return True
347 347
348 348 def _show_interpreter_prompt(self, number=None):
349 349 """ Reimplemented for IPython-style prompts.
350 350 """
351 351 # If a number was not specified, make a prompt number request.
352 352 if number is None:
353 353 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
354 354 info = self._ExecutionRequest(msg_id, 'prompt')
355 355 self._request_info['execute'] = info
356 356 return
357 357
358 358 # Show a new prompt and save information about it so that it can be
359 359 # updated later if the prompt number turns out to be wrong.
360 360 self._prompt_sep = self.input_sep
361 361 self._show_prompt(self._make_in_prompt(number), html=True)
362 362 block = self._control.document().lastBlock()
363 363 length = len(self._prompt)
364 364 self._previous_prompt_obj = self._PromptBlock(block, length, number)
365 365
366 366 # Update continuation prompt to reflect (possibly) new prompt length.
367 367 self._set_continuation_prompt(
368 368 self._make_continuation_prompt(self._prompt), html=True)
369 369
370 370 def _show_interpreter_prompt_for_reply(self, msg):
371 371 """ Reimplemented for IPython-style prompts.
372 372 """
373 373 # Update the old prompt number if necessary.
374 374 content = msg['content']
375 375 previous_prompt_number = content['execution_count']
376 376 if self._previous_prompt_obj and \
377 377 self._previous_prompt_obj.number != previous_prompt_number:
378 378 block = self._previous_prompt_obj.block
379 379
380 380 # Make sure the prompt block has not been erased.
381 381 if block.isValid() and block.text():
382 382
383 383 # Remove the old prompt and insert a new prompt.
384 384 cursor = QtGui.QTextCursor(block)
385 385 cursor.movePosition(QtGui.QTextCursor.Right,
386 386 QtGui.QTextCursor.KeepAnchor,
387 387 self._previous_prompt_obj.length)
388 388 prompt = self._make_in_prompt(previous_prompt_number)
389 389 self._prompt = self._insert_html_fetching_plain_text(
390 390 cursor, prompt)
391 391
392 392 # When the HTML is inserted, Qt blows away the syntax
393 393 # highlighting for the line, so we need to rehighlight it.
394 394 self._highlighter.rehighlightBlock(cursor.block())
395 395
396 396 self._previous_prompt_obj = None
397 397
398 398 # Show a new prompt with the kernel's estimated prompt number.
399 399 self._show_interpreter_prompt(previous_prompt_number + 1)
400 400
401 401 #---------------------------------------------------------------------------
402 402 # 'IPythonWidget' interface
403 403 #---------------------------------------------------------------------------
404 404
405 405 def set_default_style(self, colors='lightbg'):
406 406 """ Sets the widget style to the class defaults.
407 407
408 408 Parameters:
409 409 -----------
410 410 colors : str, optional (default lightbg)
411 411 Whether to use the default IPython light background or dark
412 412 background or B&W style.
413 413 """
414 414 colors = colors.lower()
415 415 if colors=='lightbg':
416 416 self.style_sheet = styles.default_light_style_sheet
417 417 self.syntax_style = styles.default_light_syntax_style
418 418 elif colors=='linux':
419 419 self.style_sheet = styles.default_dark_style_sheet
420 420 self.syntax_style = styles.default_dark_syntax_style
421 421 elif colors=='nocolor':
422 422 self.style_sheet = styles.default_bw_style_sheet
423 423 self.syntax_style = styles.default_bw_syntax_style
424 424 else:
425 425 raise KeyError("No such color scheme: %s"%colors)
426 426
427 427 #---------------------------------------------------------------------------
428 428 # 'IPythonWidget' protected interface
429 429 #---------------------------------------------------------------------------
430 430
431 431 def _edit(self, filename, line=None):
432 432 """ Opens a Python script for editing.
433 433
434 434 Parameters:
435 435 -----------
436 436 filename : str
437 437 A path to a local system file.
438 438
439 439 line : int, optional
440 440 A line of interest in the file.
441 441 """
442 442 if self.custom_edit:
443 443 self.custom_edit_requested.emit(filename, line)
444 444 elif not self.editor:
445 445 self._append_plain_text('No default editor available.\n'
446 446 'Specify a GUI text editor in the `IPythonWidget.editor` '
447 447 'configurable to enable the %edit magic')
448 448 else:
449 449 try:
450 450 filename = '"%s"' % filename
451 451 if line and self.editor_line:
452 452 command = self.editor_line.format(filename=filename,
453 453 line=line)
454 454 else:
455 455 try:
456 456 command = self.editor.format()
457 457 except KeyError:
458 458 command = self.editor.format(filename=filename)
459 459 else:
460 460 command += ' ' + filename
461 461 except KeyError:
462 462 self._append_plain_text('Invalid editor command.\n')
463 463 else:
464 464 try:
465 465 Popen(command, shell=True)
466 466 except OSError:
467 467 msg = 'Opening editor with command "%s" failed.\n'
468 468 self._append_plain_text(msg % command)
469 469
470 470 def _make_in_prompt(self, number):
471 471 """ Given a prompt number, returns an HTML In prompt.
472 472 """
473 473 try:
474 474 body = self.in_prompt % number
475 475 except TypeError:
476 476 # allow in_prompt to leave out number, e.g. '>>> '
477 477 body = self.in_prompt
478 478 return '<span class="in-prompt">%s</span>' % body
479 479
480 480 def _make_continuation_prompt(self, prompt):
481 481 """ Given a plain text version of an In prompt, returns an HTML
482 482 continuation prompt.
483 483 """
484 484 end_chars = '...: '
485 485 space_count = len(prompt.lstrip('\n')) - len(end_chars)
486 486 body = '&nbsp;' * space_count + end_chars
487 487 return '<span class="in-prompt">%s</span>' % body
488 488
489 489 def _make_out_prompt(self, number):
490 490 """ Given a prompt number, returns an HTML Out prompt.
491 491 """
492 492 body = self.out_prompt % number
493 493 return '<span class="out-prompt">%s</span>' % body
494 494
495 495 #------ Payload handlers --------------------------------------------------
496 496
497 497 # Payload handlers with a generic interface: each takes the opaque payload
498 498 # dict, unpacks it and calls the underlying functions with the necessary
499 499 # arguments.
500 500
501 501 def _handle_payload_edit(self, item):
502 502 self._edit(item['filename'], item['line_number'])
503 503
504 504 def _handle_payload_exit(self, item):
505 505 self._keep_kernel_on_exit = item['keepkernel']
506 self.exit_requested.emit()
506 self.exit_requested.emit(self)
507 507
508 508 def _handle_payload_next_input(self, item):
509 509 self.input_buffer = dedent(item['text'].rstrip())
510 510
511 511 def _handle_payload_page(self, item):
512 512 # Since the plain text widget supports only a very small subset of HTML
513 513 # and we have no control over the HTML source, we only page HTML
514 514 # payloads in the rich text widget.
515 515 if item['html'] and self.kind == 'rich':
516 516 self._page(item['html'], html=True)
517 517 else:
518 518 self._page(item['text'], html=False)
519 519
520 520 #------ Trait change handlers --------------------------------------------
521 521
522 522 def _style_sheet_changed(self):
523 523 """ Set the style sheets of the underlying widgets.
524 524 """
525 525 self.setStyleSheet(self.style_sheet)
526 526 self._control.document().setDefaultStyleSheet(self.style_sheet)
527 527 if self._page_control:
528 528 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
529 529
530 530 bg_color = self._control.palette().window().color()
531 531 self._ansi_processor.set_background_color(bg_color)
532 532
533 533
534 534 def _syntax_style_changed(self):
535 535 """ Set the style for the syntax highlighter.
536 536 """
537 537 if self._highlighter is None:
538 538 # ignore premature calls
539 539 return
540 540 if self.syntax_style:
541 541 self._highlighter.set_style(self.syntax_style)
542 542 else:
543 543 self._highlighter.set_style_sheet(self.style_sheet)
544 544
545 545 #------ Trait default initializers -----------------------------------------
546 546
547 547 def _banner_default(self):
548 548 from IPython.core.usage import default_gui_banner
549 549 return default_gui_banner
@@ -1,802 +1,953 b''
1 1 """ A minimal application using the Qt console-style IPython frontend.
2 2
3 3 This is not a complete console app, as subprocess will not be able to receive
4 4 input, there is no real readline support, among other limitations.
5 5
6 6 Authors:
7 7
8 8 * Evan Patterson
9 9 * Min RK
10 10 * Erik Tollerud
11 11 * Fernando Perez
12 12
13 13 """
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 # stdlib imports
20 20 import json
21 21 import os
22 22 import signal
23 23 import sys
24 24
25 25 # System library imports
26 26 from IPython.external.qt import QtGui,QtCore
27 27 from pygments.styles import get_all_styles
28 28
29 29 # Local imports
30 30 from IPython.config.application import boolean_flag
31 31 from IPython.core.application import BaseIPythonApplication
32 32 from IPython.core.profiledir import ProfileDir
33 33 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
34 34 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
35 35 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
36 36 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
37 37 from IPython.frontend.qt.console import styles
38 38 from IPython.frontend.qt.kernelmanager import QtKernelManager
39 39 from IPython.utils.path import filefind
40 40 from IPython.utils.py3compat import str_to_bytes
41 41 from IPython.utils.traitlets import (
42 42 Dict, List, Unicode, Int, CaselessStrEnum, CBool, Any
43 43 )
44 44 from IPython.zmq.ipkernel import (
45 45 flags as ipkernel_flags,
46 46 aliases as ipkernel_aliases,
47 47 IPKernelApp
48 48 )
49 49 from IPython.zmq.session import Session, default_secure
50 50 from IPython.zmq.zmqshell import ZMQInteractiveShell
51 51
52 52 import application_rc
53 53
54 54 #-----------------------------------------------------------------------------
55 55 # Network Constants
56 56 #-----------------------------------------------------------------------------
57 57
58 58 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
59 59
60 60 #-----------------------------------------------------------------------------
61 61 # Globals
62 62 #-----------------------------------------------------------------------------
63 63
64 64 _examples = """
65 65 ipython qtconsole # start the qtconsole
66 66 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
67 67 """
68 68
69 69 #-----------------------------------------------------------------------------
70 70 # Classes
71 71 #-----------------------------------------------------------------------------
72 72
73 73 class MainWindow(QtGui.QMainWindow):
74 74
75 75 #---------------------------------------------------------------------------
76 76 # 'object' interface
77 77 #---------------------------------------------------------------------------
78 78
79 79 def __init__(self, app, frontend, existing=False, may_close=True,
80 80 confirm_exit=True):
81 81 """ Create a MainWindow for the specified FrontendWidget.
82 82
83 83 The app is passed as an argument to allow for different
84 84 closing behavior depending on whether we are the Kernel's parent.
85 85
86 86 If existing is True, then this Console does not own the Kernel.
87 87
88 88 If may_close is True, then this Console is permitted to close the kernel
89 89 """
90 90 super(MainWindow, self).__init__()
91 91 self._app = app
92 92 self._frontend = frontend
93 93 self._existing = existing
94 94 if existing:
95 95 self._may_close = may_close
96 96 else:
97 97 self._may_close = True
98 98 self._frontend.exit_requested.connect(self.close)
99 99 self._confirm_exit = confirm_exit
100 self.setCentralWidget(frontend)
101 100
101 self.tabWidget = QtGui.QTabWidget(self)
102 self.tabWidget.setDocumentMode(True)
103 self.tabWidget.setTabsClosable(True)
104 self.tabWidget.addTab(frontend,"QtConsole1")
105 self.tabWidget.tabCloseRequested[int].connect(self.closetab)
106
107 self.setCentralWidget(self.tabWidget)
108 self.updateTabBarVisibility()
109
110 def updateTabBarVisibility(self):
111 """ update visibility of the tabBar depending of the number of tab
112
113 0 or 1 tab, tabBar hiddent
114 2+ tabs, tabbarHidden
115
116 need to be called explicitely, or be connected to tabInserted/tabRemoved
117 """
118 if self.tabWidget.count() <= 1:
119 self.tabWidget.tabBar().setVisible(False)
120 else:
121 self.tabWidget.tabBar().setVisible(True)
122
123 def activeFrontend(self):
124 return self.tabWidget.currentWidget()
125
126 def closetab(self,tab):
127 """ Called when a user try to close a tab
128
129 It takes the number of the tab to be closed as argument, (does not for
130 now, but should) take care of whether or not shuting down the kernel
131 attached to the frontend
132 """
133 print "closing tab",tab
134 try:
135 if self.tabWidget.widget(tab)._local_kernel:
136 kernel_manager = self.tabWidget.widget(tab).kernel_manager.shutdown_kernel()
137 else:
138 print "not owning the kernel"
139 except:
140 print "can't ask the kernel to shutdown"
141 #if self.tabWidget.count() == 1:
142 #self.close()
143 self.tabWidget.removeTab(tab)
144 self.updateTabBarVisibility()
145
146 def addTabWithFrontend(self,frontend,name=None):
147 """ insert a tab with a given frontend in the tab bar, and give it a name
148
149 """
150 if not name:
151 name=str('no Name '+str(self.tabWidget.count()))
152 self.tabWidget.addTab(frontend,name)
153 self.updateTabBarVisibility()
154 frontend.exit_requested.connect(self.irequest)
155
156 def irequest(self,obj):
157 print "I request to exit",obj
158 print "which is tab:",self.tabWidget.indexOf(obj)
159 self.closetab(self.tabWidget.indexOf(obj))
102 160 # MenuBar is always present on Mac Os, so let's populate it with possible
103 161 # action, don't do it on other platform as some user might not want the
104 162 # menu bar, or give them an option to remove it
105 163
106 164 def initMenuBar(self):
107 165 #create menu in the order they should appear in the menu bar
108 166 self.fileMenu = self.menuBar().addMenu("File")
109 167 self.editMenu = self.menuBar().addMenu("Edit")
110 168 self.fontMenu = self.menuBar().addMenu("Font")
111 169 self.windowMenu = self.menuBar().addMenu("Window")
112 170 self.magicMenu = self.menuBar().addMenu("Magic")
113 171
114 172 # please keep the Help menu in Mac Os even if empty. It will
115 173 # automatically contain a search field to search inside menus and
116 174 # please keep it spelled in English, as long as Qt Doesn't support
117 175 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
118 176 # this search field fonctionnality
119 177
120 178 self.helpMenu = self.menuBar().addMenu("Help")
121 179
122 180 # sould wrap every line of the following block into a try/except,
123 181 # as we are not sure of instanciating a _frontend which support all
124 182 # theses actions, but there might be a better way
125 183 try:
126 self.fileMenu.addAction(self._frontend.print_action)
184 pass
185 self.print_action = QtGui.QAction("Print",
186 self,
187 shortcut="Ctrl+P",
188 triggered=self.undo_active_frontend)
189 self.fileMenu.addAction(self.print_action)
127 190 except AttributeError:
128 print "trying to add unexisting action, skipping"
191 print "trying to add unexisting action (print), skipping"
129 192
130 193 try:
131 self.fileMenu.addAction(self._frontend.export_action)
194 self.export_action=QtGui.QAction("Export",
195 self,
196 shortcut="Ctrl+S",
197 triggered=self.export_action_active_frontend
198 )
199 self.fileMenu.addAction(self.export_action)
132 200 except AttributeError:
133 print "trying to add unexisting action, skipping"
201 print "trying to add unexisting action (Export), skipping"
134 202
135 203 try:
136 204 self.fileMenu.addAction(self._frontend.select_all_action)
137 205 except AttributeError:
138 206 print "trying to add unexisting action, skipping"
139 207
140 208 try:
141 209 self.undo_action = QtGui.QAction("Undo",
142 210 self,
143 211 shortcut="Ctrl+Z",
144 212 statusTip="Undo last action if possible",
145 triggered=self._frontend.undo)
213 triggered=self.undo_active_frontend)
146 214
147 215 self.editMenu.addAction(self.undo_action)
148 216 except AttributeError:
149 print "trying to add unexisting action, skipping"
217 print "trying to add unexisting action (undo), skipping"
150 218
151 219 try:
152 220 self.redo_action = QtGui.QAction("Redo",
153 221 self,
154 222 shortcut="Ctrl+Shift+Z",
155 223 statusTip="Redo last action if possible",
156 triggered=self._frontend.redo)
224 triggered=self.redo_active_frontend)
157 225 self.editMenu.addAction(self.redo_action)
158 226 except AttributeError:
159 print "trying to add unexisting action, skipping"
227 print "trying to add unexisting action(redo), skipping"
160 228
161 229 try:
162 self.fontMenu.addAction(self._frontend.increase_font_size)
230 pass#self.fontMenu.addAction(self.increase_font_size_active_frontend)
163 231 except AttributeError:
164 print "trying to add unexisting action, skipping"
232 print "trying to add unexisting action (increase font size), skipping"
165 233
166 234 try:
167 self.fontMenu.addAction(self._frontend.decrease_font_size)
235 pass#self.fontMenu.addAction(self.decrease_font_size_active_frontend)
168 236 except AttributeError:
169 print "trying to add unexisting action, skipping"
237 print "trying to add unexisting action (decrease font size), skipping"
170 238
171 239 try:
172 self.fontMenu.addAction(self._frontend.reset_font_size)
240 pass#self.fontMenu.addAction(self.reset_font_size_active_frontend)
173 241 except AttributeError:
174 print "trying to add unexisting action, skipping"
242 print "trying to add unexisting action (reset font size), skipping"
175 243
176 244 try:
177 245 self.reset_action = QtGui.QAction("Reset",
178 246 self,
179 247 statusTip="Clear all varible from workspace",
180 triggered=self._frontend.reset_magic)
248 triggered=self.reset_magic_active_frontend)
181 249 self.magicMenu.addAction(self.reset_action)
182 250 except AttributeError:
183 251 print "trying to add unexisting action (reset), skipping"
184 252
185 253 try:
186 254 self.history_action = QtGui.QAction("History",
187 255 self,
188 256 statusTip="show command history",
189 triggered=self._frontend.history_magic)
257 triggered=self.history_magic_active_frontend)
190 258 self.magicMenu.addAction(self.history_action)
191 259 except AttributeError:
192 260 print "trying to add unexisting action (history), skipping"
193 261
194 262 try:
195 263 self.save_action = QtGui.QAction("Export History ",
196 264 self,
197 265 statusTip="Export History as Python File",
198 triggered=self._frontend.save_magic)
266 triggered=self.save_magic_active_frontend)
199 267 self.magicMenu.addAction(self.save_action)
200 268 except AttributeError:
201 269 print "trying to add unexisting action (save), skipping"
202 270
203 271 try:
204 272 self.clear_action = QtGui.QAction("Clear",
205 273 self,
206 274 statusTip="Clear the console",
207 triggered=self._frontend.clear_magic)
275 triggered=self.clear_magic_active_frontend)
208 276 self.magicMenu.addAction(self.clear_action)
209 277 except AttributeError:
210 278 print "trying to add unexisting action, skipping"
211 279
212 280 try:
213 281 self.who_action = QtGui.QAction("Who",
214 282 self,
215 283 statusTip="List interactive variable",
216 triggered=self._frontend.who_magic)
284 triggered=self.who_magic_active_frontend)
217 285 self.magicMenu.addAction(self.who_action)
218 286 except AttributeError:
219 287 print "trying to add unexisting action (who), skipping"
220 288
221 289 try:
222 290 self.who_ls_action = QtGui.QAction("Who ls",
223 291 self,
224 292 statusTip="Return a list of interactive variable",
225 triggered=self._frontend.who_ls_magic)
293 triggered=self.who_ls_magic_active_frontend)
226 294 self.magicMenu.addAction(self.who_ls_action)
227 295 except AttributeError:
228 296 print "trying to add unexisting action (who_ls), skipping"
229 297
230 298 try:
231 299 self.whos_action = QtGui.QAction("Whos",
232 300 self,
233 301 statusTip="List interactive variable with detail",
234 triggered=self._frontend.whos_magic)
302 triggered=self.whos_magic_active_frontend)
235 303 self.magicMenu.addAction(self.whos_action)
236 304 except AttributeError:
237 305 print "trying to add unexisting action (whos), skipping"
238 306
239
307 def undo_active_frontend(self):
308 self.activeFrontend().undo()
309
310 def redo_active_frontend(self):
311 self.activeFrontend().redo()
312 def reset_magic_active_frontend(self):
313 self.activeFrontend().reset_magic()
314 def history_magic_active_frontend(self):
315 self.activeFrontend().history_magic()
316 def save_magic_active_frontend(self):
317 self.activeFrontend().save_magic()
318 def clear_magic_active_frontend(self):
319 self.activeFrontend().clear_magic()
320 def who_magic_active_frontend(self):
321 self.activeFrontend().who_magic()
322 def who_ls_magic_active_frontend(self):
323 self.activeFrontend().who_ls_magic()
324 def whos_magic_active_frontend(self):
325 self.activeFrontend().whos_magic()
326
327 def print_action_active_frontend(self):
328 self.activeFrontend().print_action()
329
330 def export_action_active_frontend(self):
331 self.activeFrontend().export_action()
332
333 def select_all_action_frontend(self):
334 self.activeFrontend().select_all_action()
335
336 def increase_font_size_active_frontend(self):
337 self.activeFrontend().increase_font_size()
338 def decrease_font_size_active_frontend(self):
339 self.activeFrontend().decrease_font_size()
340 def reset_font_size_active_frontend(self):
341 self.activeFrontend().reset_font_size()
240 342 #---------------------------------------------------------------------------
241 343 # QWidget interface
242 344 #---------------------------------------------------------------------------
243 345
244 346 def closeEvent(self, event):
245 347 """ Close the window and the kernel (if necessary).
246 348
247 349 This will prompt the user if they are finished with the kernel, and if
248 350 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
249 351 it closes without prompt.
250 352 """
251 353 keepkernel = None #Use the prompt by default
252 354 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
253 355 keepkernel = self._frontend._keep_kernel_on_exit
254 356
255 357 kernel_manager = self._frontend.kernel_manager
256 358
257 359 if keepkernel is None and not self._confirm_exit:
258 360 # don't prompt, just terminate the kernel if we own it
259 361 # or leave it alone if we don't
260 362 keepkernel = not self._existing
261 363
262 364 if keepkernel is None: #show prompt
263 365 if kernel_manager and kernel_manager.channels_running:
264 366 title = self.window().windowTitle()
265 367 cancel = QtGui.QMessageBox.Cancel
266 368 okay = QtGui.QMessageBox.Ok
267 369 if self._may_close:
268 370 msg = "You are closing this Console window."
269 371 info = "Would you like to quit the Kernel and all attached Consoles as well?"
270 372 justthis = QtGui.QPushButton("&No, just this Console", self)
271 373 justthis.setShortcut('N')
272 374 closeall = QtGui.QPushButton("&Yes, quit everything", self)
273 375 closeall.setShortcut('Y')
274 376 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
275 377 title, msg)
276 378 box.setInformativeText(info)
277 379 box.addButton(cancel)
278 380 box.addButton(justthis, QtGui.QMessageBox.NoRole)
279 381 box.addButton(closeall, QtGui.QMessageBox.YesRole)
280 382 box.setDefaultButton(closeall)
281 383 box.setEscapeButton(cancel)
282 384 pixmap = QtGui.QPixmap(':/icon/IPythonConsole.png')
283 385 scaledpixmap = pixmap.scaledToWidth(64,mode=QtCore.Qt.SmoothTransformation)
284 386 box.setIconPixmap(scaledpixmap)
285 387 reply = box.exec_()
286 388 if reply == 1: # close All
287 389 kernel_manager.shutdown_kernel()
288 390 #kernel_manager.stop_channels()
289 391 event.accept()
290 392 elif reply == 0: # close Console
291 393 if not self._existing:
292 394 # Have kernel: don't quit, just close the window
293 395 self._app.setQuitOnLastWindowClosed(False)
294 396 self.deleteLater()
295 397 event.accept()
296 398 else:
297 399 event.ignore()
298 400 else:
299 401 reply = QtGui.QMessageBox.question(self, title,
300 402 "Are you sure you want to close this Console?"+
301 403 "\nThe Kernel and other Consoles will remain active.",
302 404 okay|cancel,
303 405 defaultButton=okay
304 406 )
305 407 if reply == okay:
306 408 event.accept()
307 409 else:
308 410 event.ignore()
309 411 elif keepkernel: #close console but leave kernel running (no prompt)
310 412 if kernel_manager and kernel_manager.channels_running:
311 413 if not self._existing:
312 414 # I have the kernel: don't quit, just close the window
313 415 self._app.setQuitOnLastWindowClosed(False)
314 416 event.accept()
315 417 else: #close console and kernel (no prompt)
316 418 if kernel_manager and kernel_manager.channels_running:
317 419 kernel_manager.shutdown_kernel()
318 420 event.accept()
319 421
320 422 #-----------------------------------------------------------------------------
321 423 # Aliases and Flags
322 424 #-----------------------------------------------------------------------------
323 425
324 426 flags = dict(ipkernel_flags)
325 427 qt_flags = {
326 428 'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
327 429 "Connect to an existing kernel. If no argument specified, guess most recent"),
328 430 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
329 431 "Use a pure Python kernel instead of an IPython kernel."),
330 432 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
331 433 "Disable rich text support."),
332 434 }
333 435 qt_flags.update(boolean_flag(
334 436 'gui-completion', 'ConsoleWidget.gui_completion',
335 437 "use a GUI widget for tab completion",
336 438 "use plaintext output for completion"
337 439 ))
338 440 qt_flags.update(boolean_flag(
339 441 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
340 442 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
341 443 to force a direct exit without any confirmation.
342 444 """,
343 445 """Don't prompt the user when exiting. This will terminate the kernel
344 446 if it is owned by the frontend, and leave it alive if it is external.
345 447 """
346 448 ))
347 449 flags.update(qt_flags)
348 450
349 451 aliases = dict(ipkernel_aliases)
350 452
351 453 qt_aliases = dict(
352 454 hb = 'IPythonQtConsoleApp.hb_port',
353 455 shell = 'IPythonQtConsoleApp.shell_port',
354 456 iopub = 'IPythonQtConsoleApp.iopub_port',
355 457 stdin = 'IPythonQtConsoleApp.stdin_port',
356 458 ip = 'IPythonQtConsoleApp.ip',
357 459 existing = 'IPythonQtConsoleApp.existing',
358 460 f = 'IPythonQtConsoleApp.connection_file',
359 461
360 462 style = 'IPythonWidget.syntax_style',
361 463 stylesheet = 'IPythonQtConsoleApp.stylesheet',
362 464 colors = 'ZMQInteractiveShell.colors',
363 465
364 466 editor = 'IPythonWidget.editor',
365 467 paging = 'ConsoleWidget.paging',
366 468 ssh = 'IPythonQtConsoleApp.sshserver',
367 469 )
368 470 aliases.update(qt_aliases)
369 471
370 472
371 473 #-----------------------------------------------------------------------------
372 474 # IPythonQtConsole
373 475 #-----------------------------------------------------------------------------
374 476
375 477
376 478 class IPythonQtConsoleApp(BaseIPythonApplication):
377 479 name = 'ipython-qtconsole'
378 480 default_config_file_name='ipython_config.py'
379 481
380 482 description = """
381 483 The IPython QtConsole.
382 484
383 485 This launches a Console-style application using Qt. It is not a full
384 486 console, in that launched terminal subprocesses will not be able to accept
385 487 input.
386 488
387 489 The QtConsole supports various extra features beyond the Terminal IPython
388 490 shell, such as inline plotting with matplotlib, via:
389 491
390 492 ipython qtconsole --pylab=inline
391 493
392 494 as well as saving your session as HTML, and printing the output.
393 495
394 496 """
395 497 examples = _examples
396 498
397 499 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
398 500 flags = Dict(flags)
399 501 aliases = Dict(aliases)
400 502
401 503 kernel_argv = List(Unicode)
402 504
403 505 # create requested profiles by default, if they don't exist:
404 506 auto_create = CBool(True)
405 507 # connection info:
406 508 ip = Unicode(LOCALHOST, config=True,
407 509 help="""Set the kernel\'s IP address [default localhost].
408 510 If the IP address is something other than localhost, then
409 511 Consoles on other machines will be able to connect
410 512 to the Kernel, so be careful!"""
411 513 )
412 514
413 515 sshserver = Unicode('', config=True,
414 516 help="""The SSH server to use to connect to the kernel.""")
415 517 sshkey = Unicode('', config=True,
416 518 help="""Path to the ssh key to use for logging in to the ssh server.""")
417 519
418 520 hb_port = Int(0, config=True,
419 521 help="set the heartbeat port [default: random]")
420 522 shell_port = Int(0, config=True,
421 523 help="set the shell (XREP) port [default: random]")
422 524 iopub_port = Int(0, config=True,
423 525 help="set the iopub (PUB) port [default: random]")
424 526 stdin_port = Int(0, config=True,
425 527 help="set the stdin (XREQ) port [default: random]")
426 528 connection_file = Unicode('', config=True,
427 529 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
428 530
429 531 This file will contain the IP, ports, and authentication key needed to connect
430 532 clients to this kernel. By default, this file will be created in the security-dir
431 533 of the current profile, but can be specified by absolute path.
432 534 """)
433 535 def _connection_file_default(self):
434 536 return 'kernel-%i.json' % os.getpid()
435 537
436 538 existing = Unicode('', config=True,
437 539 help="""Connect to an already running kernel""")
438 540
439 541 stylesheet = Unicode('', config=True,
440 542 help="path to a custom CSS stylesheet")
441 543
442 544 pure = CBool(False, config=True,
443 545 help="Use a pure Python kernel instead of an IPython kernel.")
444 546 plain = CBool(False, config=True,
445 547 help="Use a plaintext widget instead of rich text (plain can't print/save).")
446 548
447 549 def _pure_changed(self, name, old, new):
448 550 kind = 'plain' if self.plain else 'rich'
449 551 self.config.ConsoleWidget.kind = kind
450 552 if self.pure:
451 553 self.widget_factory = FrontendWidget
452 554 elif self.plain:
453 555 self.widget_factory = IPythonWidget
454 556 else:
455 557 self.widget_factory = RichIPythonWidget
456 558
457 559 _plain_changed = _pure_changed
458 560
459 561 confirm_exit = CBool(True, config=True,
460 562 help="""
461 563 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
462 564 to force a direct exit without any confirmation.""",
463 565 )
464 566
465 567 # the factory for creating a widget
466 568 widget_factory = Any(RichIPythonWidget)
467 569
468 570 def parse_command_line(self, argv=None):
469 571 super(IPythonQtConsoleApp, self).parse_command_line(argv)
470 572 if argv is None:
471 573 argv = sys.argv[1:]
472 574
473 575 self.kernel_argv = list(argv) # copy
474 576 # kernel should inherit default config file from frontend
475 577 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
476 578 # Scrub frontend-specific flags
477 579 for a in argv:
478 580 if a.startswith('-') and a.lstrip('-') in qt_flags:
479 581 self.kernel_argv.remove(a)
480 582 swallow_next = False
481 583 for a in argv:
482 584 if swallow_next:
483 585 self.kernel_argv.remove(a)
484 586 swallow_next = False
485 587 continue
486 588 if a.startswith('-'):
487 589 split = a.lstrip('-').split('=')
488 590 alias = split[0]
489 591 if alias in qt_aliases:
490 592 self.kernel_argv.remove(a)
491 593 if len(split) == 1:
492 594 # alias passed with arg via space
493 595 swallow_next = True
494 596
495 597 def init_connection_file(self):
496 598 """find the connection file, and load the info if found.
497 599
498 600 The current working directory and the current profile's security
499 601 directory will be searched for the file if it is not given by
500 602 absolute path.
501 603
502 604 When attempting to connect to an existing kernel and the `--existing`
503 605 argument does not match an existing file, it will be interpreted as a
504 606 fileglob, and the matching file in the current profile's security dir
505 607 with the latest access time will be used.
506 608 """
507 609 if self.existing:
508 610 try:
509 611 cf = find_connection_file(self.existing)
510 612 except Exception:
511 613 self.log.critical("Could not find existing kernel connection file %s", self.existing)
512 614 self.exit(1)
513 615 self.log.info("Connecting to existing kernel: %s" % cf)
514 616 self.connection_file = cf
515 617 # should load_connection_file only be used for existing?
516 618 # as it is now, this allows reusing ports if an existing
517 619 # file is requested
518 620 try:
519 621 self.load_connection_file()
520 622 except Exception:
521 623 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
522 624 self.exit(1)
523 625
524 626 def load_connection_file(self):
525 627 """load ip/port/hmac config from JSON connection file"""
526 628 # this is identical to KernelApp.load_connection_file
527 629 # perhaps it can be centralized somewhere?
528 630 try:
529 631 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
530 632 except IOError:
531 633 self.log.debug("Connection File not found: %s", self.connection_file)
532 634 return
533 635 self.log.debug(u"Loading connection file %s", fname)
534 636 with open(fname) as f:
535 637 s = f.read()
536 638 cfg = json.loads(s)
537 639 if self.ip == LOCALHOST and 'ip' in cfg:
538 640 # not overridden by config or cl_args
539 641 self.ip = cfg['ip']
540 642 for channel in ('hb', 'shell', 'iopub', 'stdin'):
541 643 name = channel + '_port'
542 644 if getattr(self, name) == 0 and name in cfg:
543 645 # not overridden by config or cl_args
544 646 setattr(self, name, cfg[name])
545 647 if 'key' in cfg:
546 648 self.config.Session.key = str_to_bytes(cfg['key'])
547 649
548 650 def init_ssh(self):
549 651 """set up ssh tunnels, if needed."""
550 652 if not self.sshserver and not self.sshkey:
551 653 return
552 654
553 655 if self.sshkey and not self.sshserver:
554 656 # specifying just the key implies that we are connecting directly
555 657 self.sshserver = self.ip
556 658 self.ip = LOCALHOST
557 659
558 660 # build connection dict for tunnels:
559 661 info = dict(ip=self.ip,
560 662 shell_port=self.shell_port,
561 663 iopub_port=self.iopub_port,
562 664 stdin_port=self.stdin_port,
563 665 hb_port=self.hb_port
564 666 )
565 667
566 668 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
567 669
568 670 # tunnels return a new set of ports, which will be on localhost:
569 671 self.ip = LOCALHOST
570 672 try:
571 673 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
572 674 except:
573 675 # even catch KeyboardInterrupt
574 676 self.log.error("Could not setup tunnels", exc_info=True)
575 677 self.exit(1)
576 678
577 679 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
578 680
579 681 cf = self.connection_file
580 682 base,ext = os.path.splitext(cf)
581 683 base = os.path.basename(base)
582 684 self.connection_file = os.path.basename(base)+'-ssh'+ext
583 685 self.log.critical("To connect another client via this tunnel, use:")
584 686 self.log.critical("--existing %s" % self.connection_file)
585 687
586 688 def init_kernel_manager(self):
587 689 # Don't let Qt or ZMQ swallow KeyboardInterupts.
588 690 signal.signal(signal.SIGINT, signal.SIG_DFL)
589 691 sec = self.profile_dir.security_dir
590 692 try:
591 693 cf = filefind(self.connection_file, ['.', sec])
592 694 except IOError:
593 695 # file might not exist
594 696 if self.connection_file == os.path.basename(self.connection_file):
595 697 # just shortname, put it in security dir
596 698 cf = os.path.join(sec, self.connection_file)
597 699 else:
598 700 cf = self.connection_file
599 701
600 702 # Create a KernelManager and start a kernel.
601 703 self.kernel_manager = QtKernelManager(
602 704 ip=self.ip,
603 705 shell_port=self.shell_port,
604 706 iopub_port=self.iopub_port,
605 707 stdin_port=self.stdin_port,
606 708 hb_port=self.hb_port,
607 709 connection_file=cf,
608 710 config=self.config,
609 711 )
610 712 # start the kernel
611 713 if not self.existing:
612 714 kwargs = dict(ipython=not self.pure)
613 715 kwargs['extra_arguments'] = self.kernel_argv
614 716 self.kernel_manager.start_kernel(**kwargs)
615 717 elif self.sshserver:
616 718 # ssh, write new connection file
617 719 self.kernel_manager.write_connection_file()
618 720 self.kernel_manager.start_channels()
619 721
722 def createTabWithNewFrontend(self):
723 kernel_manager = QtKernelManager(
724 shell_address=(self.ip, self.shell_port),
725 sub_address=(self.ip, self.iopub_port),
726 stdin_address=(self.ip, self.stdin_port),
727 hb_address=(self.ip, self.hb_port),
728 config=self.config
729 )
730 # start the kernel
731 if not self.existing:
732 kwargs = dict(ip=self.ip, ipython=not self.pure)
733 kwargs['extra_arguments'] = self.kernel_argv
734 kernel_manager.start_kernel(**kwargs)
735 kernel_manager.start_channels()
736 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
737 widget = self.widget_factory(config=self.config,
738 local_kernel=local_kernel)
739 widget.kernel_manager = kernel_manager
740 self.window.addTabWithFrontend(widget)
741
742 def createTabAttachedToCurrentTabKernel(self):
743 currentWidget=self.window.tabWidget.currentWidget()
744 currentWidgetIndex=self.window.tabWidget.indexOf(currentWidget)
745 ckm=currentWidget.kernel_manager;
746 cwname=self.window.tabWidget.tabText(currentWidgetIndex);
747 kernel_manager = QtKernelManager(
748 shell_address=ckm.shell_address,
749 sub_address=ckm.sub_address,
750 stdin_address=ckm.stdin_address,
751 hb_address=ckm.hb_address,
752 config=self.config
753 )
754 kernel_manager.start_channels()
755 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
756 widget = self.widget_factory(config=self.config,
757 local_kernel=False)
758 widget.kernel_manager = kernel_manager
759 self.window.addTabWithFrontend(widget,name=str('('+cwname+') slave'))
620 760
621 761 def init_qt_elements(self):
622 762 # Create the widget.
623 763 self.app = QtGui.QApplication([])
624 764 pixmap=QtGui.QPixmap(':/icon/IPythonConsole.png')
625 765 icon=QtGui.QIcon(pixmap)
626 766 QtGui.QApplication.setWindowIcon(icon)
627 767
628 768 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
629 769 self.widget = self.widget_factory(config=self.config,
630 770 local_kernel=local_kernel)
631 771 self.widget.kernel_manager = self.kernel_manager
632 772 self.window = MainWindow(self.app, self.widget, self.existing,
633 773 may_close=local_kernel,
634 774 confirm_exit=self.confirm_exit)
635 775 self.window.initMenuBar()
636 776 self.window.setWindowTitle('Python' if self.pure else 'IPython')
637 777
638 778 def init_colors(self):
639 779 """Configure the coloring of the widget"""
640 780 # Note: This will be dramatically simplified when colors
641 781 # are removed from the backend.
642 782
643 783 if self.pure:
644 784 # only IPythonWidget supports styling
645 785 return
646 786
647 787 # parse the colors arg down to current known labels
648 788 try:
649 789 colors = self.config.ZMQInteractiveShell.colors
650 790 except AttributeError:
651 791 colors = None
652 792 try:
653 793 style = self.config.IPythonWidget.colors
654 794 except AttributeError:
655 795 style = None
656 796
657 797 # find the value for colors:
658 798 if colors:
659 799 colors=colors.lower()
660 800 if colors in ('lightbg', 'light'):
661 801 colors='lightbg'
662 802 elif colors in ('dark', 'linux'):
663 803 colors='linux'
664 804 else:
665 805 colors='nocolor'
666 806 elif style:
667 807 if style=='bw':
668 808 colors='nocolor'
669 809 elif styles.dark_style(style):
670 810 colors='linux'
671 811 else:
672 812 colors='lightbg'
673 813 else:
674 814 colors=None
675 815
676 816 # Configure the style.
677 817 widget = self.widget
678 818 if style:
679 819 widget.style_sheet = styles.sheet_from_template(style, colors)
680 820 widget.syntax_style = style
681 821 widget._syntax_style_changed()
682 822 widget._style_sheet_changed()
683 823 elif colors:
684 824 # use a default style
685 825 widget.set_default_style(colors=colors)
686 826 else:
687 827 # this is redundant for now, but allows the widget's
688 828 # defaults to change
689 829 widget.set_default_style()
690 830
691 831 if self.stylesheet:
692 832 # we got an expicit stylesheet
693 833 if os.path.isfile(self.stylesheet):
694 834 with open(self.stylesheet) as f:
695 835 sheet = f.read()
696 836 widget.style_sheet = sheet
697 837 widget._style_sheet_changed()
698 838 else:
699 839 raise IOError("Stylesheet %r not found."%self.stylesheet)
700 840
701 841 def initialize(self, argv=None):
702 842 super(IPythonQtConsoleApp, self).initialize(argv)
703 843 self.init_connection_file()
704 844 default_secure(self.config)
705 845 self.init_ssh()
706 846 self.init_kernel_manager()
707 847 self.init_qt_elements()
708 848 self.init_colors()
709 849 self.init_window_shortcut()
710 850
711 851 def init_window_shortcut(self):
712 852
713 853 self.fullScreenAct = QtGui.QAction("Full Screen",
714 854 self.window,
715 855 shortcut="Ctrl+Meta+Space",
716 856 statusTip="Toggle between Fullscreen and Normal Size",
717 857 triggered=self.toggleFullScreen)
718 858
859 self.tabAndNewKernelAct =QtGui.QAction("Tab with New kernel",
860 self.window,
861 shortcut="Ctrl+T",
862 triggered=self.createTabWithNewFrontend)
863 self.window.windowMenu.addAction(self.tabAndNewKernelAct)
864 self.tabSameKernalAct =QtGui.QAction("Tab with Same kernel",
865 self.window,
866 shortcut="Ctrl+Shift+T",
867 triggered=self.createTabAttachedToCurrentTabKernel)
868 self.window.windowMenu.addAction(self.tabSameKernalAct)
869 self.window.windowMenu.addSeparator()
719 870
720 871 # creating shortcut in menubar only for Mac OS as I don't
721 872 # know the shortcut or if the windows manager assign it in
722 873 # other platform.
723 874 if sys.platform == 'darwin':
724 875 self.minimizeAct = QtGui.QAction("Minimize",
725 876 self.window,
726 877 shortcut="Ctrl+m",
727 878 statusTip="Minimize the window/Restore Normal Size",
728 879 triggered=self.toggleMinimized)
729 880 self.maximizeAct = QtGui.QAction("Maximize",
730 881 self.window,
731 882 shortcut="Ctrl+Shift+M",
732 883 statusTip="Maximize the window/Restore Normal Size",
733 884 triggered=self.toggleMaximized)
734 885
735 886 self.onlineHelpAct = QtGui.QAction("Open Online Help",
736 887 self.window,
737 888 triggered=self._open_online_help)
738 889
739 890 self.windowMenu = self.window.windowMenu
740 891 self.windowMenu.addAction(self.minimizeAct)
741 892 self.windowMenu.addAction(self.maximizeAct)
742 893 self.windowMenu.addSeparator()
743 894 self.windowMenu.addAction(self.fullScreenAct)
744 895
745 896 self.window.helpMenu.addAction(self.onlineHelpAct)
746 897 else:
747 898 # if we don't put it in a menu, we add it to the window so
748 899 # that it can still be triggerd by shortcut
749 900 self.window.addAction(self.fullScreenAct)
750 901
751 902 def toggleMinimized(self):
752 903 if not self.window.isMinimized():
753 904 self.window.showMinimized()
754 905 else:
755 906 self.window.showNormal()
756 907
757 908 def _open_online_help(self):
758 909 QtGui.QDesktopServices.openUrl(
759 910 QtCore.QUrl("http://ipython.org/documentation.html",
760 911 QtCore.QUrl.TolerantMode)
761 912 )
762 913
763 914 def toggleMaximized(self):
764 915 if not self.window.isMaximized():
765 916 self.window.showMaximized()
766 917 else:
767 918 self.window.showNormal()
768 919
769 920 # Min/Max imizing while in full screen give a bug
770 921 # when going out of full screen, at least on OSX
771 922 def toggleFullScreen(self):
772 923 if not self.window.isFullScreen():
773 924 self.window.showFullScreen()
774 925 if sys.platform == 'darwin':
775 926 self.maximizeAct.setEnabled(False)
776 927 self.minimizeAct.setEnabled(False)
777 928 else:
778 929 self.window.showNormal()
779 930 if sys.platform == 'darwin':
780 931 self.maximizeAct.setEnabled(True)
781 932 self.minimizeAct.setEnabled(True)
782 933
783 934 def start(self):
784 935
785 936 # draw the window
786 937 self.window.show()
787 938
788 939 # Start the application main loop.
789 940 self.app.exec_()
790 941
791 942 #-----------------------------------------------------------------------------
792 943 # Main entry point
793 944 #-----------------------------------------------------------------------------
794 945
795 946 def main():
796 947 app = IPythonQtConsoleApp()
797 948 app.initialize()
798 949 app.start()
799 950
800 951
801 952 if __name__ == '__main__':
802 953 main()
General Comments 0
You need to be logged in to leave comments. Login now