##// END OF EJS Templates
remove old comment no longer usefull
Matthias BUSSONNIER -
Show More
@@ -1,1165 +1,1162 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 import webbrowser
25 25 from getpass import getpass
26 26
27 27 # System library imports
28 28 from IPython.external.qt import QtGui,QtCore
29 29 from pygments.styles import get_all_styles
30 30
31 31 # Local imports
32 32 from IPython.config.application import boolean_flag
33 33 from IPython.core.application import BaseIPythonApplication
34 34 from IPython.core.profiledir import ProfileDir
35 35 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
36 36 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
37 37 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
38 38 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
39 39 from IPython.frontend.qt.console import styles
40 40 from IPython.frontend.qt.kernelmanager import QtKernelManager
41 41 from IPython.utils.path import filefind
42 42 from IPython.utils.py3compat import str_to_bytes
43 43 from IPython.utils.traitlets import (
44 44 Dict, List, Unicode, Int, CaselessStrEnum, CBool, Any
45 45 )
46 46 from IPython.zmq.ipkernel import (
47 47 flags as ipkernel_flags,
48 48 aliases as ipkernel_aliases,
49 49 IPKernelApp
50 50 )
51 51 from IPython.zmq.session import Session, default_secure
52 52 from IPython.zmq.zmqshell import ZMQInteractiveShell
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
91 91 super(MainWindow, self).__init__()
92 92 self._app = app
93 93
94 94 self.tab_widget = QtGui.QTabWidget(self)
95 95 self.tab_widget.setDocumentMode(True)
96 96 self.tab_widget.setTabsClosable(True)
97 97 self.tab_widget.tabCloseRequested[int].connect(self.close_tab)
98 98
99 99 self.setCentralWidget(self.tab_widget)
100 100 self.update_tab_bar_visibility()
101 101
102 102 def update_tab_bar_visibility(self):
103 103 """ update visibility of the tabBar depending of the number of tab
104 104
105 105 0 or 1 tab, tabBar hidden
106 106 2+ tabs, tabBar visible
107 107
108 108 send a self.close if number of tab ==0
109 109
110 110 need to be called explicitely, or be connected to tabInserted/tabRemoved
111 111 """
112 112 if self.tab_widget.count() <= 1:
113 113 self.tab_widget.tabBar().setVisible(False)
114 114 else:
115 115 self.tab_widget.tabBar().setVisible(True)
116 116 if self.tab_widget.count()==0 :
117 117 self.close()
118 118
119 119 @property
120 120 def active_frontend(self):
121 121 return self.tab_widget.currentWidget()
122 122
123 123 def close_tab(self,current_tab):
124 124 """ Called when you need to try to close a tab.
125 125
126 126 It takes the number of the tab to be closed as argument, or a referece
127 127 to the wiget insite this tab
128 128 """
129 129
130 130 # let's be sure "tab" and "closing widget are respectivey the index of the tab to close
131 131 # and a reference to the trontend to close
132 132 if type(current_tab) is not int :
133 133 current_tab = self.tab_widget.indexOf(current_tab)
134 134 closing_widget=self.tab_widget.widget(current_tab)
135 135
136 136
137 137 # when trying to be closed, widget might re-send a request to be closed again, but will
138 138 # be deleted when event will be processed. So need to check that widget still exist and
139 139 # skip if not. One example of this is when 'exit' is send in a slave tab. 'exit' will be
140 140 # re-send by this fonction on the master widget, which ask all slaves widget to exit
141 141 if closing_widget==None:
142 142 return
143 143
144 144 #get a list of all wwidget not owning the kernel.
145 145 slave_tabs=self.find_slaves_tabs(closing_widget)
146 146
147 147 keepkernel = None #Use the prompt by default
148 148 if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic
149 149 keepkernel = closing_widget._keep_kernel_on_exit
150 150 # If signal sent by exist magic (_keep_kernel_on_exit, exist and not None)
151 151 # we set local slave tabs._hidden to True to avoit prompting for kernel
152 152 # restart when they litt get the signal. and the "forward" the 'exit'
153 153 # to the main win
154 154 if keepkernel is not None:
155 155 for tab in slave_tabs:
156 156 tab._hidden = True
157 157 if closing_widget in slave_tabs :
158 158 try :
159 159 self.find_master_tab(closing_widget).execute('exit')
160 160 except AttributeError:
161 161 self.log.info("Master already closed or not local, closing only current tab")
162 162 self.tab_widget.removeTab(current_tab)
163 163 return
164 164
165 165 kernel_manager = closing_widget.kernel_manager
166 166
167 167 if keepkernel is None and not closing_widget._confirm_exit:
168 168 # don't prompt, just terminate the kernel if we own it
169 169 # or leave it alone if we don't
170 170 keepkernel = not closing_widget._existing
171 171
172 172 if keepkernel is None: #show prompt
173 173 if kernel_manager and kernel_manager.channels_running:
174 174 title = self.window().windowTitle()
175 175 cancel = QtGui.QMessageBox.Cancel
176 176 okay = QtGui.QMessageBox.Ok
177 177 if closing_widget._may_close:
178 178 msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"'
179 179 info = "Would you like to quit the Kernel and all attached Consoles as well?"
180 180 justthis = QtGui.QPushButton("&No, just this Console", self)
181 181 justthis.setShortcut('N')
182 182 closeall = QtGui.QPushButton("&Yes, quit everything", self)
183 183 closeall.setShortcut('Y')
184 184 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
185 185 title, msg)
186 186 box.setInformativeText(info)
187 187 box.addButton(cancel)
188 188 box.addButton(justthis, QtGui.QMessageBox.NoRole)
189 189 box.addButton(closeall, QtGui.QMessageBox.YesRole)
190 190 box.setDefaultButton(closeall)
191 191 box.setEscapeButton(cancel)
192 192 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
193 193 box.setIconPixmap(pixmap)
194 194 reply = box.exec_()
195 195 if reply == 1: # close All
196 196 for slave in slave_tabs:
197 197 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
198 198 closing_widget.execute("exit")
199 199 self.tab_widget.removeTab(current_tab)
200 200 elif reply == 0: # close Console
201 201 if not closing_widget._existing:
202 202 # Have kernel: don't quit, just close the window
203 203 self._app.setQuitOnLastWindowClosed(False)
204 204 closing_widget.execute("exit True")
205 205 else:
206 206 reply = QtGui.QMessageBox.question(self, title,
207 207 "Are you sure you want to close this Console?"+
208 208 "\nThe Kernel and other Consoles will remain active.",
209 209 okay|cancel,
210 210 defaultButton=okay
211 211 )
212 212 if reply == okay:
213 213 self.tab_widget.removeTab(current_tab)
214 214 elif keepkernel: #close console but leave kernel running (no prompt)
215 215 if kernel_manager and kernel_manager.channels_running:
216 216 if not closing_widget._existing:
217 217 # I have the kernel: don't quit, just close the window
218 218 self.tab_widget.removeTab(current_tab)
219 219 else: #close console and kernel (no prompt)
220 220 if kernel_manager and kernel_manager.channels_running:
221 221 for slave in slave_tabs:
222 222 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
223 223 self.tab_widget.removeTab(current_tab)
224 224 kernel_manager.shutdown_kernel()
225 225 self.update_tab_bar_visibility()
226 226
227 227 def add_tab_with_frontend(self,frontend,name=None):
228 228 """ insert a tab with a given frontend in the tab bar, and give it a name
229 229
230 230 """
231 231 if not name:
232 232 name=str('kernel '+str(self.tab_widget.count()))
233 233 self.tab_widget.addTab(frontend,name)
234 234 self.update_tab_bar_visibility()
235 235 self.make_frontend_visible(frontend)
236 236 frontend.exit_requested.connect(self.close_tab)
237 237
238 238 def next_tab(self):
239 239 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1))
240 240
241 241 def prev_tab(self):
242 242 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1))
243 243
244 244 def make_frontend_visible(self,frontend):
245 245 widget_index=self.tab_widget.indexOf(frontend)
246 246 if widget_index > 0 :
247 247 self.tab_widget.setCurrentIndex(widget_index)
248 248
249 249 def find_master_tab(self,tab,as_list=False):
250 250 """
251 251 Try to return the frontend that own the kernel attached to the given widget/tab.
252 252
253 253 Only find frontend owed by the current application. Selection
254 254 based on port of the kernel, might be inacurate if several kernel
255 255 on different ip use same port number.
256 256
257 257 This fonction does the conversion tabNumber/widget if needed.
258 258 Might return None if no master widget (non local kernel)
259 259 Will crash IPython if more than 1 masterWidget
260 260
261 261 When asList set to True, always return a list of widget(s) owning
262 262 the kernel. The list might be empty or containing several Widget.
263 263 """
264 264
265 265 #convert from/to int/richIpythonWidget if needed
266 266 if type(tab) == int:
267 267 tab = self.tab_widget.widget(tab)
268 268 km=tab.kernel_manager;
269 269
270 270 #build list of all widgets
271 271 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
272 272
273 273 # widget that are candidate to be the owner of the kernel does have all the same port of the curent widget
274 274 # And should have a _may_close attribute
275 275 filtred_widget_list = [ widget for widget in widget_list if
276 276 widget.kernel_manager.shell_address == km.shell_address and
277 277 widget.kernel_manager.sub_address == km.sub_address and
278 278 widget.kernel_manager.stdin_address == km.stdin_address and
279 279 widget.kernel_manager.hb_address == km.hb_address and
280 280 hasattr(widget,'_may_close') ]
281 281 # the master widget is the one that may close the kernel
282 282 master_widget= [ widget for widget in filtred_widget_list if widget._may_close]
283 283 if as_list:
284 284 return master_widget
285 285 assert(len(master_widget)<=1 )
286 286 if len(master_widget)==0:
287 287 return None
288 288
289 289 return master_widget[0]
290 290
291 291 def find_slaves_tabs(self,tab):
292 292 """
293 293 Try to return all the frontend that do not own the kernel attached to the given widget/tab.
294 294
295 295 Only find frontend owed by the current application. Selection
296 296 based on port of the kernel, might be innacurate if several kernel
297 297 on different ip use same port number.
298 298
299 299 This fonction does the conversion tabNumber/widget if needed.
300 300 """
301 301 #convert from/to int/richIpythonWidget if needed
302 302 if type(tab) == int:
303 303 tab = self.tab_widget.widget(tab)
304 304 km=tab.kernel_manager;
305 305
306 306 #build list of all widgets
307 307 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
308 308
309 309 # widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget
310 310 filtered_widget_list = ( widget for widget in widget_list if
311 311 widget.kernel_manager.shell_address == km.shell_address and
312 312 widget.kernel_manager.sub_address == km.sub_address and
313 313 widget.kernel_manager.stdin_address == km.stdin_address and
314 314 widget.kernel_manager.hb_address == km.hb_address)
315 315 # Get a list of all widget owning the same kernel and removed it from
316 316 # the previous cadidate. (better using sets ?)
317 317 master_widget_list = self.find_master_tab(tab,as_list=True)
318 318 slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list]
319 319
320 320 return slave_list
321 321
322 322 # MenuBar is always present on Mac Os, so let's populate it with possible
323 323 # action, don't do it on other platform as some user might not want the
324 324 # menu bar, or give them an option to remove it
325 325 def init_menu_bar(self):
326 326 #create menu in the order they should appear in the menu bar
327 327 self.file_menu = self.menuBar().addMenu("&File")
328 328 self.edit_menu = self.menuBar().addMenu("&Edit")
329 329 self.window_menu = self.menuBar().addMenu("&Window")
330 330 self.magic_menu = self.menuBar().addMenu("&Magic")
331 331 self.all_magic_menu = self.magic_menu.addMenu("&All Magic")
332 332
333 333 # please keep the Help menu in Mac Os even if empty. It will
334 334 # automatically contain a search field to search inside menus and
335 335 # please keep it spelled in English, as long as Qt Doesn't support
336 336 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
337 337 # this search field fonctionnality
338 338
339 339 self.help_menu = self.menuBar().addMenu("&Help")
340 340
341 # sould wrap every line of the following block into a try/except,
342 # as we are not sure of instanciating a _frontend which support all
343 # theses actions, but there might be a better way
344 341 self.print_action = QtGui.QAction("&Print",
345 342 self,
346 343 shortcut="Ctrl+P",
347 344 triggered=self.print_action_active_frontend)
348 345 self.file_menu.addAction(self.print_action)
349 346
350 347 self.export_action=QtGui.QAction("E&xport",
351 348 self,
352 349 shortcut="Ctrl+S",
353 350 triggered=self.export_action_active_frontend
354 351 )
355 352 self.file_menu.addAction(self.export_action)
356 353
357 354 self.select_all_action = QtGui.QAction("Select &All",
358 355 self,
359 356 shortcut="Ctrl+A",
360 357 triggered=self.select_all_active_frontend
361 358 )
362 359 self.file_menu.addAction(self.select_all_action)
363 360
364 361 self.paste_action = QtGui.QAction("&Paste",
365 362 self,
366 363 shortcut=QtGui.QKeySequence.Paste,
367 364 triggered=self.paste_active_frontend
368 365 )
369 366 self.edit_menu.addAction(self.paste_action)
370 367
371 368 self.copy_action = QtGui.QAction("&Copy",
372 369 self,
373 370 shortcut=QtGui.QKeySequence.Copy,
374 371 triggered=self.copy_active_frontend
375 372 )
376 373 self.edit_menu.addAction(self.copy_action)
377 374
378 375 self.cut_action = QtGui.QAction("&Cut",
379 376 self,
380 377 shortcut=QtGui.QKeySequence.Cut,
381 378 triggered=self.cut_active_frontend
382 379 )
383 380 self.edit_menu.addAction(self.cut_action)
384 381
385 382 self.edit_menu.addSeparator()
386 383
387 384 self.undo_action = QtGui.QAction("&Undo",
388 385 self,
389 386 shortcut="Ctrl+Z",
390 387 statusTip="Undo last action if possible",
391 388 triggered=self.undo_active_frontend
392 389 )
393 390 self.edit_menu.addAction(self.undo_action)
394 391
395 392 self.redo_action = QtGui.QAction("&Redo",
396 393 self,
397 394 shortcut="Ctrl+Shift+Z",
398 395 statusTip="Redo last action if possible",
399 396 triggered=self.redo_active_frontend)
400 397 self.edit_menu.addAction(self.redo_action)
401 398
402 399 self.window_menu.addSeparator()
403 400
404 401 self.increase_font_size = QtGui.QAction("&Increase Font Size",
405 402 self,
406 403 shortcut="Ctrl++",
407 404 triggered=self.increase_font_size_active_frontend
408 405 )
409 406 self.window_menu.addAction(self.increase_font_size)
410 407
411 408 self.decrease_font_size = QtGui.QAction("&Decrease Font Size",
412 409 self,
413 410 shortcut="Ctrl+-",
414 411 triggered=self.decrease_font_size_active_frontend
415 412 )
416 413 self.window_menu.addAction(self.decrease_font_size)
417 414
418 415 self.reset_font_size = QtGui.QAction("&Reset Font Size",
419 416 self,
420 417 shortcut="Ctrl+0",
421 418 triggered=self.reset_font_size_active_frontend
422 419 )
423 420 self.window_menu.addAction(self.reset_font_size)
424 421
425 422 self.window_menu.addSeparator()
426 423
427 424 self.reset_action = QtGui.QAction("&Reset",
428 425 self,
429 426 statusTip="Clear all varible from workspace",
430 427 triggered=self.reset_magic_active_frontend)
431 428 self.magic_menu.addAction(self.reset_action)
432 429
433 430 self.history_action = QtGui.QAction("&History",
434 431 self,
435 432 statusTip="show command history",
436 433 triggered=self.history_magic_active_frontend)
437 434 self.magic_menu.addAction(self.history_action)
438 435
439 436 self.save_action = QtGui.QAction("E&xport History ",
440 437 self,
441 438 statusTip="Export History as Python File",
442 439 triggered=self.save_magic_active_frontend)
443 440 self.magic_menu.addAction(self.save_action)
444 441
445 442 self.clear_action = QtGui.QAction("&Clear Screen",
446 443 self,
447 444 shortcut='Ctrl+L',
448 445 statusTip="Clear the console",
449 446 triggered=self.clear_magic_active_frontend)
450 447 self.window_menu.addAction(self.clear_action)
451 448
452 449 self.who_action = QtGui.QAction("&Who",
453 450 self,
454 451 statusTip="List interactive variable",
455 452 triggered=self.who_magic_active_frontend)
456 453 self.magic_menu.addAction(self.who_action)
457 454
458 455 self.who_ls_action = QtGui.QAction("Wh&o ls",
459 456 self,
460 457 statusTip="Return a list of interactive variable",
461 458 triggered=self.who_ls_magic_active_frontend)
462 459 self.magic_menu.addAction(self.who_ls_action)
463 460
464 461 self.whos_action = QtGui.QAction("Who&s",
465 462 self,
466 463 statusTip="List interactive variable with detail",
467 464 triggered=self.whos_magic_active_frontend)
468 465 self.magic_menu.addAction(self.whos_action)
469 466
470 467 self.intro_active_frontend_action = QtGui.QAction("Intro",
471 468 self,
472 469 triggered=self.intro_active_frontend
473 470 )
474 471 self.help_menu.addAction(self.intro_active_frontend_action)
475 472
476 473 self.guiref_active_frontend_action = QtGui.QAction("Gui references",
477 474 self,
478 475 triggered=self.guiref_active_frontend
479 476 )
480 477 self.help_menu.addAction(self.guiref_active_frontend_action)
481 478
482 479 self.quickref_active_frontend_action = QtGui.QAction("Quick references",
483 480 self,
484 481 triggered=self.quickref_active_frontend
485 482 )
486 483 self.help_menu.addAction(self.quickref_active_frontend_action)
487 484
488 485 magiclist=["%alias", "%autocall", "%automagic", "%bookmark", "%cd", "%clear",
489 486 "%colors", "%debug", "%dhist", "%dirs", "%doctest_mode", "%ed", "%edit", "%env", "%gui",
490 487 "%guiref", "%hist", "%history", "%install_default_config", "%install_profiles",
491 488 "%less", "%load_ext", "%loadpy", "%logoff", "%logon", "%logstart", "%logstate",
492 489 "%logstop", "%lsmagic", "%macro", "%magic", "%man", "%more", "%notebook", "%page",
493 490 "%pastebin", "%pdb", "%pdef", "%pdoc", "%pfile", "%pinfo", "%pinfo2", "%popd", "%pprint",
494 491 "%precision", "%profile", "%prun", "%psearch", "%psource", "%pushd", "%pwd", "%pycat",
495 492 "%pylab", "%quickref", "%recall", "%rehashx", "%reload_ext", "%rep", "%rerun",
496 493 "%reset", "%reset_selective", "%run", "%save", "%sc", "%sx", "%tb", "%time", "%timeit",
497 494 "%unalias", "%unload_ext", "%who", "%who_ls", "%whos", "%xdel", "%xmode"]
498 495
499 496 def make_dynamic_magic(i):
500 497 def inner_dynamic_magic():
501 498 self.active_frontend.execute(i)
502 499 inner_dynamic_magic.__name__ = "dynamics_magic_%s" % i
503 500 return inner_dynamic_magic
504 501
505 502 for magic in magiclist:
506 503 xaction = QtGui.QAction(magic,
507 504 self,
508 505 triggered=make_dynamic_magic(magic)
509 506 )
510 507 self.all_magic_menu.addAction(xaction)
511 508
512 509 def cut_active_frontend(self):
513 510 self.active_frontend.cut_action.trigger()
514 511
515 512 def copy_active_frontend(self):
516 513 self.active_frontend.copy_action.trigger()
517 514
518 515 def paste_active_frontend(self):
519 516 self.active_frontend.paste_action.trigger()
520 517
521 518 def undo_active_frontend(self):
522 519 self.active_frontend.undo()
523 520
524 521 def redo_active_frontend(self):
525 522 self.active_frontend.redo()
526 523
527 524 def reset_magic_active_frontend(self):
528 525 self.active_frontend.execute("%reset")
529 526
530 527 def history_magic_active_frontend(self):
531 528 self.active_frontend.execute("%history")
532 529
533 530 def save_magic_active_frontend(self):
534 531 self.active_frontend.save_magic()
535 532
536 533 def clear_magic_active_frontend(self):
537 534 self.active_frontend.execute("%clear")
538 535
539 536 def who_magic_active_frontend(self):
540 537 self.active_frontend.execute("%who")
541 538
542 539 def who_ls_magic_active_frontend(self):
543 540 self.active_frontend.execute("%who_ls")
544 541
545 542 def whos_magic_active_frontend(self):
546 543 self.active_frontend.execute("%whos")
547 544
548 545 def print_action_active_frontend(self):
549 546 self.active_frontend.print_action.trigger()
550 547
551 548 def export_action_active_frontend(self):
552 549 self.active_frontend.export_action.trigger()
553 550
554 551 def select_all_active_frontend(self):
555 552 self.active_frontend.select_all_action.trigger()
556 553
557 554 def increase_font_size_active_frontend(self):
558 555 self.active_frontend.increase_font_size.trigger()
559 556
560 557 def decrease_font_size_active_frontend(self):
561 558 self.active_frontend.decrease_font_size.trigger()
562 559
563 560 def reset_font_size_active_frontend(self):
564 561 self.active_frontend.reset_font_size.trigger()
565 562
566 563 def guiref_active_frontend(self):
567 564 self.active_frontend.execute("%guiref")
568 565
569 566 def intro_active_frontend(self):
570 567 self.active_frontend.execute("?")
571 568
572 569 def quickref_active_frontend(self):
573 570 self.active_frontend.execute("%quickref")
574 571 #---------------------------------------------------------------------------
575 572 # QWidget interface
576 573 #---------------------------------------------------------------------------
577 574
578 575 def closeEvent(self, event):
579 576 """ Forward the close event to every tabs contained by the windows
580 577 """
581 578 # Do Not loop on the widget count as it change while closing
582 579 widget_list=[ self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
583 580 for widget in widget_list:
584 581 self.close_tab(widget)
585 582 event.accept()
586 583
587 584 #-----------------------------------------------------------------------------
588 585 # Aliases and Flags
589 586 #-----------------------------------------------------------------------------
590 587
591 588 flags = dict(ipkernel_flags)
592 589 qt_flags = {
593 590 'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
594 591 "Connect to an existing kernel. If no argument specified, guess most recent"),
595 592 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
596 593 "Use a pure Python kernel instead of an IPython kernel."),
597 594 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
598 595 "Disable rich text support."),
599 596 }
600 597 qt_flags.update(boolean_flag(
601 598 'gui-completion', 'ConsoleWidget.gui_completion',
602 599 "use a GUI widget for tab completion",
603 600 "use plaintext output for completion"
604 601 ))
605 602 qt_flags.update(boolean_flag(
606 603 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
607 604 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
608 605 to force a direct exit without any confirmation.
609 606 """,
610 607 """Don't prompt the user when exiting. This will terminate the kernel
611 608 if it is owned by the frontend, and leave it alive if it is external.
612 609 """
613 610 ))
614 611 flags.update(qt_flags)
615 612
616 613 aliases = dict(ipkernel_aliases)
617 614
618 615 qt_aliases = dict(
619 616 hb = 'IPythonQtConsoleApp.hb_port',
620 617 shell = 'IPythonQtConsoleApp.shell_port',
621 618 iopub = 'IPythonQtConsoleApp.iopub_port',
622 619 stdin = 'IPythonQtConsoleApp.stdin_port',
623 620 ip = 'IPythonQtConsoleApp.ip',
624 621 existing = 'IPythonQtConsoleApp.existing',
625 622 f = 'IPythonQtConsoleApp.connection_file',
626 623
627 624 style = 'IPythonWidget.syntax_style',
628 625 stylesheet = 'IPythonQtConsoleApp.stylesheet',
629 626 colors = 'ZMQInteractiveShell.colors',
630 627
631 628 editor = 'IPythonWidget.editor',
632 629 paging = 'ConsoleWidget.paging',
633 630 ssh = 'IPythonQtConsoleApp.sshserver',
634 631 )
635 632 aliases.update(qt_aliases)
636 633
637 634
638 635 #-----------------------------------------------------------------------------
639 636 # IPythonQtConsole
640 637 #-----------------------------------------------------------------------------
641 638
642 639
643 640 class IPythonQtConsoleApp(BaseIPythonApplication):
644 641 name = 'ipython-qtconsole'
645 642 default_config_file_name='ipython_config.py'
646 643
647 644 description = """
648 645 The IPython QtConsole.
649 646
650 647 This launches a Console-style application using Qt. It is not a full
651 648 console, in that launched terminal subprocesses will not be able to accept
652 649 input.
653 650
654 651 The QtConsole supports various extra features beyond the Terminal IPython
655 652 shell, such as inline plotting with matplotlib, via:
656 653
657 654 ipython qtconsole --pylab=inline
658 655
659 656 as well as saving your session as HTML, and printing the output.
660 657
661 658 """
662 659 examples = _examples
663 660
664 661 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
665 662 flags = Dict(flags)
666 663 aliases = Dict(aliases)
667 664
668 665 kernel_argv = List(Unicode)
669 666
670 667 # create requested profiles by default, if they don't exist:
671 668 auto_create = CBool(True)
672 669 # connection info:
673 670 ip = Unicode(LOCALHOST, config=True,
674 671 help="""Set the kernel\'s IP address [default localhost].
675 672 If the IP address is something other than localhost, then
676 673 Consoles on other machines will be able to connect
677 674 to the Kernel, so be careful!"""
678 675 )
679 676
680 677 sshserver = Unicode('', config=True,
681 678 help="""The SSH server to use to connect to the kernel.""")
682 679 sshkey = Unicode('', config=True,
683 680 help="""Path to the ssh key to use for logging in to the ssh server.""")
684 681
685 682 hb_port = Int(0, config=True,
686 683 help="set the heartbeat port [default: random]")
687 684 shell_port = Int(0, config=True,
688 685 help="set the shell (XREP) port [default: random]")
689 686 iopub_port = Int(0, config=True,
690 687 help="set the iopub (PUB) port [default: random]")
691 688 stdin_port = Int(0, config=True,
692 689 help="set the stdin (XREQ) port [default: random]")
693 690 connection_file = Unicode('', config=True,
694 691 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
695 692
696 693 This file will contain the IP, ports, and authentication key needed to connect
697 694 clients to this kernel. By default, this file will be created in the security-dir
698 695 of the current profile, but can be specified by absolute path.
699 696 """)
700 697 def _connection_file_default(self):
701 698 return 'kernel-%i.json' % os.getpid()
702 699
703 700 existing = Unicode('', config=True,
704 701 help="""Connect to an already running kernel""")
705 702
706 703 stylesheet = Unicode('', config=True,
707 704 help="path to a custom CSS stylesheet")
708 705
709 706 pure = CBool(False, config=True,
710 707 help="Use a pure Python kernel instead of an IPython kernel.")
711 708 plain = CBool(False, config=True,
712 709 help="Use a plaintext widget instead of rich text (plain can't print/save).")
713 710
714 711 def _pure_changed(self, name, old, new):
715 712 kind = 'plain' if self.plain else 'rich'
716 713 self.config.ConsoleWidget.kind = kind
717 714 if self.pure:
718 715 self.widget_factory = FrontendWidget
719 716 elif self.plain:
720 717 self.widget_factory = IPythonWidget
721 718 else:
722 719 self.widget_factory = RichIPythonWidget
723 720
724 721 _plain_changed = _pure_changed
725 722
726 723 confirm_exit = CBool(True, config=True,
727 724 help="""
728 725 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
729 726 to force a direct exit without any confirmation.""",
730 727 )
731 728
732 729 # the factory for creating a widget
733 730 widget_factory = Any(RichIPythonWidget)
734 731
735 732 def parse_command_line(self, argv=None):
736 733 super(IPythonQtConsoleApp, self).parse_command_line(argv)
737 734 if argv is None:
738 735 argv = sys.argv[1:]
739 736
740 737 self.kernel_argv = list(argv) # copy
741 738 # kernel should inherit default config file from frontend
742 739 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
743 740 # Scrub frontend-specific flags
744 741 for a in argv:
745 742 if a.startswith('-') and a.lstrip('-') in qt_flags:
746 743 self.kernel_argv.remove(a)
747 744 swallow_next = False
748 745 for a in argv:
749 746 if swallow_next:
750 747 self.kernel_argv.remove(a)
751 748 swallow_next = False
752 749 continue
753 750 if a.startswith('-'):
754 751 split = a.lstrip('-').split('=')
755 752 alias = split[0]
756 753 if alias in qt_aliases:
757 754 self.kernel_argv.remove(a)
758 755 if len(split) == 1:
759 756 # alias passed with arg via space
760 757 swallow_next = True
761 758
762 759 def init_connection_file(self):
763 760 """find the connection file, and load the info if found.
764 761
765 762 The current working directory and the current profile's security
766 763 directory will be searched for the file if it is not given by
767 764 absolute path.
768 765
769 766 When attempting to connect to an existing kernel and the `--existing`
770 767 argument does not match an existing file, it will be interpreted as a
771 768 fileglob, and the matching file in the current profile's security dir
772 769 with the latest access time will be used.
773 770 """
774 771 if self.existing:
775 772 try:
776 773 cf = find_connection_file(self.existing)
777 774 except Exception:
778 775 self.log.critical("Could not find existing kernel connection file %s", self.existing)
779 776 self.exit(1)
780 777 self.log.info("Connecting to existing kernel: %s" % cf)
781 778 self.connection_file = cf
782 779 # should load_connection_file only be used for existing?
783 780 # as it is now, this allows reusing ports if an existing
784 781 # file is requested
785 782 try:
786 783 self.load_connection_file()
787 784 except Exception:
788 785 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
789 786 self.exit(1)
790 787
791 788 def load_connection_file(self):
792 789 """load ip/port/hmac config from JSON connection file"""
793 790 # this is identical to KernelApp.load_connection_file
794 791 # perhaps it can be centralized somewhere?
795 792 try:
796 793 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
797 794 except IOError:
798 795 self.log.debug("Connection File not found: %s", self.connection_file)
799 796 return
800 797 self.log.debug(u"Loading connection file %s", fname)
801 798 with open(fname) as f:
802 799 s = f.read()
803 800 cfg = json.loads(s)
804 801 if self.ip == LOCALHOST and 'ip' in cfg:
805 802 # not overridden by config or cl_args
806 803 self.ip = cfg['ip']
807 804 for channel in ('hb', 'shell', 'iopub', 'stdin'):
808 805 name = channel + '_port'
809 806 if getattr(self, name) == 0 and name in cfg:
810 807 # not overridden by config or cl_args
811 808 setattr(self, name, cfg[name])
812 809 if 'key' in cfg:
813 810 self.config.Session.key = str_to_bytes(cfg['key'])
814 811
815 812 def init_ssh(self):
816 813 """set up ssh tunnels, if needed."""
817 814 if not self.sshserver and not self.sshkey:
818 815 return
819 816
820 817 if self.sshkey and not self.sshserver:
821 818 # specifying just the key implies that we are connecting directly
822 819 self.sshserver = self.ip
823 820 self.ip = LOCALHOST
824 821
825 822 # build connection dict for tunnels:
826 823 info = dict(ip=self.ip,
827 824 shell_port=self.shell_port,
828 825 iopub_port=self.iopub_port,
829 826 stdin_port=self.stdin_port,
830 827 hb_port=self.hb_port
831 828 )
832 829
833 830 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
834 831
835 832 # tunnels return a new set of ports, which will be on localhost:
836 833 self.ip = LOCALHOST
837 834 try:
838 835 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
839 836 except:
840 837 # even catch KeyboardInterrupt
841 838 self.log.error("Could not setup tunnels", exc_info=True)
842 839 self.exit(1)
843 840
844 841 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
845 842
846 843 cf = self.connection_file
847 844 base,ext = os.path.splitext(cf)
848 845 base = os.path.basename(base)
849 846 self.connection_file = os.path.basename(base)+'-ssh'+ext
850 847 self.log.critical("To connect another client via this tunnel, use:")
851 848 self.log.critical("--existing %s" % self.connection_file)
852 849
853 850 def init_kernel_manager(self):
854 851 # Don't let Qt or ZMQ swallow KeyboardInterupts.
855 852 signal.signal(signal.SIGINT, signal.SIG_DFL)
856 853 sec = self.profile_dir.security_dir
857 854 try:
858 855 cf = filefind(self.connection_file, ['.', sec])
859 856 except IOError:
860 857 # file might not exist
861 858 if self.connection_file == os.path.basename(self.connection_file):
862 859 # just shortname, put it in security dir
863 860 cf = os.path.join(sec, self.connection_file)
864 861 else:
865 862 cf = self.connection_file
866 863
867 864 # Create a KernelManager and start a kernel.
868 865 self.kernel_manager = QtKernelManager(
869 866 ip=self.ip,
870 867 shell_port=self.shell_port,
871 868 iopub_port=self.iopub_port,
872 869 stdin_port=self.stdin_port,
873 870 hb_port=self.hb_port,
874 871 connection_file=cf,
875 872 config=self.config,
876 873 )
877 874 # start the kernel
878 875 if not self.existing:
879 876 kwargs = dict(ipython=not self.pure)
880 877 kwargs['extra_arguments'] = self.kernel_argv
881 878 self.kernel_manager.start_kernel(**kwargs)
882 879 elif self.sshserver:
883 880 # ssh, write new connection file
884 881 self.kernel_manager.write_connection_file()
885 882 self.kernel_manager.start_channels()
886 883
887 884 def create_tab_with_new_frontend(self):
888 885 """ Create new tab attached to new kernel, launched on localhost.
889 886 """
890 887 kernel_manager = QtKernelManager(
891 888 shell_address=(LOCALHOST,0 ),
892 889 sub_address=(LOCALHOST, 0),
893 890 stdin_address=(LOCALHOST, 0),
894 891 hb_address=(LOCALHOST, 0),
895 892 config=self.config
896 893 )
897 894 # start the kernel
898 895 kwargs = dict(ip=LOCALHOST, ipython=not self.pure)
899 896 kwargs['extra_arguments'] = self.kernel_argv
900 897 kernel_manager.start_kernel(**kwargs)
901 898 kernel_manager.start_channels()
902 899 local_kernel = (not False) or self.ip in LOCAL_IPS
903 900 widget = self.widget_factory(config=self.config,
904 901 local_kernel=local_kernel)
905 902 widget.kernel_manager = kernel_manager
906 903 widget._existing=False;
907 904 widget._confirm_exit=True;
908 905 widget._may_close=True;
909 906 self.window.add_tab_with_frontend(widget)
910 907
911 908 def create_tab_attached_to_current_tab_kernel(self):
912 909 current_widget = self.window.tab_widget.currentWidget()
913 910 current_widget_index = self.window.tab_widget.indexOf(current_widget)
914 911 current_widget.kernel_manager = current_widget.kernel_manager;
915 912 current_widget_name = self.window.tab_widget.tabText(current_widget_index);
916 913 kernel_manager = QtKernelManager(
917 914 shell_address = current_widget.kernel_manager.shell_address,
918 915 sub_address = current_widget.kernel_manager.sub_address,
919 916 stdin_address = current_widget.kernel_manager.stdin_address,
920 917 hb_address = current_widget.kernel_manager.hb_address,
921 918 config = self.config
922 919 )
923 920 kernel_manager.start_channels()
924 921 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
925 922 widget = self.widget_factory(config=self.config,
926 923 local_kernel=False)
927 924 widget._confirm_exit=True;
928 925 widget._may_close=False;
929 926 widget.kernel_manager = kernel_manager
930 927 self.window.add_tab_with_frontend(widget,name=str('('+current_widget_name+') slave'))
931 928
932 929 def init_qt_elements(self):
933 930 # Create the widget.
934 931 self.app = QtGui.QApplication([])
935 932
936 933 base_path = os.path.abspath(os.path.dirname(__file__))
937 934 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
938 935 self.app.icon = QtGui.QIcon(icon_path)
939 936 QtGui.QApplication.setWindowIcon(self.app.icon)
940 937
941 938 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
942 939 self.widget = self.widget_factory(config=self.config,
943 940 local_kernel=local_kernel)
944 941 self.widget._existing = self.existing;
945 942 self.widget._may_close = not self.existing;
946 943 self.widget._confirm_exit = not self.existing;
947 944
948 945 self.widget.kernel_manager = self.kernel_manager
949 946 self.window = MainWindow(self.app, self.widget, self.existing,
950 947 may_close=local_kernel,
951 948 confirm_exit=self.confirm_exit)
952 949 self.window.log = self.log
953 950 self.window.add_tab_with_frontend(self.widget)
954 951 self.window.init_menu_bar()
955 952 self.window.setWindowTitle('Python' if self.pure else 'IPython')
956 953
957 954 def init_colors(self):
958 955 """Configure the coloring of the widget"""
959 956 # Note: This will be dramatically simplified when colors
960 957 # are removed from the backend.
961 958
962 959 if self.pure:
963 960 # only IPythonWidget supports styling
964 961 return
965 962
966 963 # parse the colors arg down to current known labels
967 964 try:
968 965 colors = self.config.ZMQInteractiveShell.colors
969 966 except AttributeError:
970 967 colors = None
971 968 try:
972 969 style = self.config.IPythonWidget.colors
973 970 except AttributeError:
974 971 style = None
975 972
976 973 # find the value for colors:
977 974 if colors:
978 975 colors=colors.lower()
979 976 if colors in ('lightbg', 'light'):
980 977 colors='lightbg'
981 978 elif colors in ('dark', 'linux'):
982 979 colors='linux'
983 980 else:
984 981 colors='nocolor'
985 982 elif style:
986 983 if style=='bw':
987 984 colors='nocolor'
988 985 elif styles.dark_style(style):
989 986 colors='linux'
990 987 else:
991 988 colors='lightbg'
992 989 else:
993 990 colors=None
994 991
995 992 # Configure the style.
996 993 widget = self.widget
997 994 if style:
998 995 widget.style_sheet = styles.sheet_from_template(style, colors)
999 996 widget.syntax_style = style
1000 997 widget._syntax_style_changed()
1001 998 widget._style_sheet_changed()
1002 999 elif colors:
1003 1000 # use a default style
1004 1001 widget.set_default_style(colors=colors)
1005 1002 else:
1006 1003 # this is redundant for now, but allows the widget's
1007 1004 # defaults to change
1008 1005 widget.set_default_style()
1009 1006
1010 1007 if self.stylesheet:
1011 1008 # we got an expicit stylesheet
1012 1009 if os.path.isfile(self.stylesheet):
1013 1010 with open(self.stylesheet) as f:
1014 1011 sheet = f.read()
1015 1012 widget.style_sheet = sheet
1016 1013 widget._style_sheet_changed()
1017 1014 else:
1018 1015 raise IOError("Stylesheet %r not found."%self.stylesheet)
1019 1016
1020 1017 def initialize(self, argv=None):
1021 1018 super(IPythonQtConsoleApp, self).initialize(argv)
1022 1019 self.init_connection_file()
1023 1020 default_secure(self.config)
1024 1021 self.init_ssh()
1025 1022 self.init_kernel_manager()
1026 1023 self.init_qt_elements()
1027 1024 self.init_colors()
1028 1025 self.init_window_shortcut()
1029 1026
1030 1027 def init_window_shortcut(self):
1031 1028
1032 1029 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
1033 1030 self.window,
1034 1031 shortcut="Ctrl+PgDown",
1035 1032 statusTip="Cahange to next tab",
1036 1033 triggered=self.window.prev_tab)
1037 1034
1038 1035 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
1039 1036 self.window,
1040 1037 shortcut="Ctrl+PgUp",
1041 1038 statusTip="Cahange to next tab",
1042 1039 triggered=self.window.next_tab)
1043 1040
1044 1041 self.fullScreenAct = QtGui.QAction("&Full Screen",
1045 1042 self.window,
1046 1043 shortcut="Ctrl+Meta+Space",
1047 1044 statusTip="Toggle between Fullscreen and Normal Size",
1048 1045 triggered=self.toggleFullScreen)
1049 1046
1050 1047
1051 1048
1052 1049 self.tabAndNewKernelAct =QtGui.QAction("Tab with &New kernel",
1053 1050 self.window,
1054 1051 shortcut="Ctrl+T",
1055 1052 triggered=self.create_tab_with_new_frontend)
1056 1053 self.window.window_menu.addAction(self.tabAndNewKernelAct)
1057 1054
1058 1055 self.tabSameKernalAct =QtGui.QAction("Tab with Sa&me kernel",
1059 1056 self.window,
1060 1057 shortcut="Ctrl+Shift+T",
1061 1058 triggered=self.create_tab_attached_to_current_tab_kernel)
1062 1059 self.window.window_menu.addAction(self.tabSameKernalAct)
1063 1060 self.window.window_menu.addSeparator()
1064 1061
1065 1062 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
1066 1063 self.window,
1067 1064 triggered=self._open_online_help)
1068 1065 self.window.help_menu.addAction(self.onlineHelpAct)
1069 1066 # creating shortcut in menubar only for Mac OS as I don't
1070 1067 # know the shortcut or if the windows manager assign it in
1071 1068 # other platform.
1072 1069 if sys.platform == 'darwin':
1073 1070 self.minimizeAct = QtGui.QAction("Mini&mize",
1074 1071 self.window,
1075 1072 shortcut="Ctrl+m",
1076 1073 statusTip="Minimize the window/Restore Normal Size",
1077 1074 triggered=self.toggleMinimized)
1078 1075 self.maximizeAct = QtGui.QAction("Ma&ximize",
1079 1076 self.window,
1080 1077 shortcut="Ctrl+Shift+M",
1081 1078 statusTip="Maximize the window/Restore Normal Size",
1082 1079 triggered=self.toggleMaximized)
1083 1080
1084 1081
1085 1082 self.window_menu = self.window.window_menu
1086 1083
1087 1084 self.window_menu.addAction(self.next_tab_act)
1088 1085 self.window_menu.addAction(self.prev_tab_act)
1089 1086 self.window_menu.addSeparator()
1090 1087 self.window_menu.addAction(self.minimizeAct)
1091 1088 self.window_menu.addAction(self.maximizeAct)
1092 1089 self.window_menu.addSeparator()
1093 1090 self.window_menu.addAction(self.fullScreenAct)
1094 1091
1095 1092 else:
1096 1093 # if we don't put it in a menu, we add it to the window so
1097 1094 # that it can still be triggerd by shortcut
1098 1095 self.window.addAction(self.fullScreenAct)
1099 1096
1100 1097 # Don't activate toggleMenubar on mac, doen't work,
1101 1098 # as toolbar always here
1102 1099 self.toggle_menu_bar_act = QtGui.QAction("&Toggle Menu Bar",
1103 1100 self.window,
1104 1101 shortcut="Ctrl+Meta+H",
1105 1102 statusTip="Toggle menubar betwin visible and not",
1106 1103 triggered=self.toggle_menu_bar)
1107 1104 self.window_menu.addAction(self.toggle_menu_bar_act)
1108 1105
1109 1106 def toggle_menu_bar(self):
1110 1107 menu_bar = self.window.menuBar();
1111 1108 if not menu_bar.isVisible():
1112 1109 menu_bar.setVisible(False)
1113 1110 else:
1114 1111 menu_bar.setVisible(True)
1115 1112
1116 1113 def toggleMinimized(self):
1117 1114 if not self.window.isMinimized():
1118 1115 self.window.showMinimized()
1119 1116 else:
1120 1117 self.window.showNormal()
1121 1118
1122 1119 def _open_online_help(self):
1123 1120 filename="http://ipython.org/ipython-doc/stable/index.html"
1124 1121 webbrowser.open(filename, new=1, autoraise=True)
1125 1122
1126 1123 def toggleMaximized(self):
1127 1124 if not self.window.isMaximized():
1128 1125 self.window.showMaximized()
1129 1126 else:
1130 1127 self.window.showNormal()
1131 1128
1132 1129 # Min/Max imizing while in full screen give a bug
1133 1130 # when going out of full screen, at least on OSX
1134 1131 def toggleFullScreen(self):
1135 1132 if not self.window.isFullScreen():
1136 1133 self.window.showFullScreen()
1137 1134 if sys.platform == 'darwin':
1138 1135 self.maximizeAct.setEnabled(False)
1139 1136 self.minimizeAct.setEnabled(False)
1140 1137 else:
1141 1138 self.window.showNormal()
1142 1139 if sys.platform == 'darwin':
1143 1140 self.maximizeAct.setEnabled(True)
1144 1141 self.minimizeAct.setEnabled(True)
1145 1142
1146 1143 def start(self):
1147 1144
1148 1145 # draw the window
1149 1146 self.window.show()
1150 1147
1151 1148 # Start the application main loop.
1152 1149 self.app.exec_()
1153 1150
1154 1151 #-----------------------------------------------------------------------------
1155 1152 # Main entry point
1156 1153 #-----------------------------------------------------------------------------
1157 1154
1158 1155 def main():
1159 1156 app = IPythonQtConsoleApp()
1160 1157 app.initialize()
1161 1158 app.start()
1162 1159
1163 1160
1164 1161 if __name__ == '__main__':
1165 1162 main()
General Comments 0
You need to be logged in to leave comments. Login now