qtconsoleapp.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. """ A minimal application using the Qt console-style Jupyter frontend.
  2. This is not a complete console app, as subprocess will not be able to receive
  3. input, there is no real readline support, among other limitations.
  4. """
  5. # Copyright (c) Jupyter Development Team.
  6. # Distributed under the terms of the Modified BSD License.
  7. import os
  8. import signal
  9. import sys
  10. from warnings import warn
  11. # If run on Windows:
  12. #
  13. # 1. Install an exception hook which pops up a message box.
  14. # Pythonw.exe hides the console, so without this the application
  15. # silently fails to load.
  16. #
  17. # We always install this handler, because the expectation is for
  18. # qtconsole to bring up a GUI even if called from the console.
  19. # The old handler is called, so the exception is printed as well.
  20. # If desired, check for pythonw with an additional condition
  21. # (sys.executable.lower().find('pythonw.exe') >= 0).
  22. #
  23. # 2. Set AppUserModelID for Windows 7 and later so that qtconsole
  24. # uses its assigned taskbar icon instead of grabbing the one with
  25. # the same AppUserModelID
  26. #
  27. if os.name == 'nt':
  28. # 1.
  29. old_excepthook = sys.excepthook
  30. # Exclude this from our autogenerated API docs.
  31. undoc = lambda func: func
  32. @undoc
  33. def gui_excepthook(exctype, value, tb):
  34. try:
  35. import ctypes, traceback
  36. MB_ICONERROR = 0x00000010
  37. title = u'Error starting QtConsole'
  38. msg = u''.join(traceback.format_exception(exctype, value, tb))
  39. ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
  40. finally:
  41. # Also call the old exception hook to let it do
  42. # its thing too.
  43. old_excepthook(exctype, value, tb)
  44. sys.excepthook = gui_excepthook
  45. # 2.
  46. try:
  47. from ctypes import windll
  48. windll.shell32.SetCurrentProcessExplicitAppUserModelID("Jupyter.Qtconsole")
  49. except AttributeError:
  50. pass
  51. from qtpy import QtCore, QtGui, QtWidgets
  52. from traitlets.config.application import boolean_flag
  53. from traitlets.config.application import catch_config_error
  54. from qtconsole.jupyter_widget import JupyterWidget
  55. from qtconsole.rich_jupyter_widget import RichJupyterWidget
  56. from qtconsole import styles, __version__
  57. from qtconsole.mainwindow import MainWindow
  58. from qtconsole.client import QtKernelClient
  59. from qtconsole.manager import QtKernelManager
  60. from traitlets import (
  61. Dict, Unicode, CBool, Any
  62. )
  63. from jupyter_core.application import JupyterApp, base_flags, base_aliases
  64. from jupyter_client.consoleapp import (
  65. JupyterConsoleApp, app_aliases, app_flags,
  66. )
  67. from jupyter_client.localinterfaces import is_local_ip
  68. _examples = """
  69. jupyter qtconsole # start the qtconsole
  70. """
  71. #-----------------------------------------------------------------------------
  72. # Aliases and Flags
  73. #-----------------------------------------------------------------------------
  74. # FIXME: workaround bug in jupyter_client < 4.1 excluding base_flags,aliases
  75. flags = dict(base_flags)
  76. qt_flags = {
  77. 'plain' : ({'JupyterQtConsoleApp' : {'plain' : True}},
  78. "Disable rich text support."),
  79. }
  80. qt_flags.update(boolean_flag(
  81. 'banner', 'JupyterQtConsoleApp.display_banner',
  82. "Display a banner upon starting the QtConsole.",
  83. "Don't display a banner upon starting the QtConsole."
  84. ))
  85. # and app_flags from the Console Mixin
  86. qt_flags.update(app_flags)
  87. # add frontend flags to the full set
  88. flags.update(qt_flags)
  89. # start with copy of base jupyter aliases
  90. aliases = dict(base_aliases)
  91. qt_aliases = dict(
  92. style = 'JupyterWidget.syntax_style',
  93. stylesheet = 'JupyterQtConsoleApp.stylesheet',
  94. editor = 'JupyterWidget.editor',
  95. paging = 'ConsoleWidget.paging',
  96. )
  97. # and app_aliases from the Console Mixin
  98. qt_aliases.update(app_aliases)
  99. qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
  100. # add frontend aliases to the full set
  101. aliases.update(qt_aliases)
  102. # get flags&aliases into sets, and remove a couple that
  103. # shouldn't be scrubbed from backend flags:
  104. qt_aliases = set(qt_aliases.keys())
  105. qt_flags = set(qt_flags.keys())
  106. class JupyterQtConsoleApp(JupyterApp, JupyterConsoleApp):
  107. name = 'jupyter-qtconsole'
  108. version = __version__
  109. description = """
  110. The Jupyter QtConsole.
  111. This launches a Console-style application using Qt. It is not a full
  112. console, in that launched terminal subprocesses will not be able to accept
  113. input.
  114. """
  115. examples = _examples
  116. classes = [JupyterWidget] + JupyterConsoleApp.classes
  117. flags = Dict(flags)
  118. aliases = Dict(aliases)
  119. frontend_flags = Any(qt_flags)
  120. frontend_aliases = Any(qt_aliases)
  121. kernel_client_class = QtKernelClient
  122. kernel_manager_class = QtKernelManager
  123. stylesheet = Unicode('', config=True,
  124. help="path to a custom CSS stylesheet")
  125. hide_menubar = CBool(False, config=True,
  126. help="Start the console window with the menu bar hidden.")
  127. maximize = CBool(False, config=True,
  128. help="Start the console window maximized.")
  129. plain = CBool(False, config=True,
  130. help="Use a plaintext widget instead of rich text (plain can't print/save).")
  131. display_banner = CBool(True, config=True,
  132. help="Whether to display a banner upon starting the QtConsole."
  133. )
  134. def _plain_changed(self, name, old, new):
  135. kind = 'plain' if new else 'rich'
  136. self.config.ConsoleWidget.kind = kind
  137. if new:
  138. self.widget_factory = JupyterWidget
  139. else:
  140. self.widget_factory = RichJupyterWidget
  141. # the factory for creating a widget
  142. widget_factory = Any(RichJupyterWidget)
  143. def parse_command_line(self, argv=None):
  144. super(JupyterQtConsoleApp, self).parse_command_line(argv)
  145. self.build_kernel_argv(self.extra_args)
  146. def new_frontend_master(self):
  147. """ Create and return new frontend attached to new kernel, launched on localhost.
  148. """
  149. kernel_manager = self.kernel_manager_class(
  150. connection_file=self._new_connection_file(),
  151. parent=self,
  152. autorestart=True,
  153. )
  154. # start the kernel
  155. kwargs = {}
  156. # FIXME: remove special treatment of IPython kernels
  157. if self.kernel_manager.ipykernel:
  158. kwargs['extra_arguments'] = self.kernel_argv
  159. kernel_manager.start_kernel(**kwargs)
  160. kernel_manager.client_factory = self.kernel_client_class
  161. kernel_client = kernel_manager.client()
  162. kernel_client.start_channels(shell=True, iopub=True)
  163. widget = self.widget_factory(config=self.config,
  164. local_kernel=True)
  165. self.init_colors(widget)
  166. widget.kernel_manager = kernel_manager
  167. widget.kernel_client = kernel_client
  168. widget._existing = False
  169. widget._may_close = True
  170. widget._confirm_exit = self.confirm_exit
  171. widget._display_banner = self.display_banner
  172. return widget
  173. def new_frontend_connection(self, connection_file):
  174. """Create and return a new frontend attached to an existing kernel.
  175. Parameters
  176. ----------
  177. connection_file : str
  178. The connection_file path this frontend is to connect to
  179. """
  180. kernel_client = self.kernel_client_class(
  181. connection_file=connection_file,
  182. config=self.config,
  183. )
  184. kernel_client.load_connection_file()
  185. kernel_client.start_channels()
  186. widget = self.widget_factory(config=self.config,
  187. local_kernel=False)
  188. self.init_colors(widget)
  189. widget._existing = True
  190. widget._may_close = False
  191. widget._confirm_exit = False
  192. widget._display_banner = self.display_banner
  193. widget.kernel_client = kernel_client
  194. widget.kernel_manager = None
  195. return widget
  196. def new_frontend_slave(self, current_widget):
  197. """Create and return a new frontend attached to an existing kernel.
  198. Parameters
  199. ----------
  200. current_widget : JupyterWidget
  201. The JupyterWidget whose kernel this frontend is to share
  202. """
  203. kernel_client = self.kernel_client_class(
  204. connection_file=current_widget.kernel_client.connection_file,
  205. config = self.config,
  206. )
  207. kernel_client.load_connection_file()
  208. kernel_client.start_channels()
  209. widget = self.widget_factory(config=self.config,
  210. local_kernel=False)
  211. self.init_colors(widget)
  212. widget._existing = True
  213. widget._may_close = False
  214. widget._confirm_exit = False
  215. widget._display_banner = self.display_banner
  216. widget.kernel_client = kernel_client
  217. widget.kernel_manager = current_widget.kernel_manager
  218. return widget
  219. def init_qt_app(self):
  220. # separate from qt_elements, because it must run first
  221. self.app = QtWidgets.QApplication(['jupyter-qtconsole'])
  222. self.app.setApplicationName('jupyter-qtconsole')
  223. def init_qt_elements(self):
  224. # Create the widget.
  225. base_path = os.path.abspath(os.path.dirname(__file__))
  226. icon_path = os.path.join(base_path, 'resources', 'icon', 'JupyterConsole.svg')
  227. self.app.icon = QtGui.QIcon(icon_path)
  228. QtWidgets.QApplication.setWindowIcon(self.app.icon)
  229. ip = self.ip
  230. local_kernel = (not self.existing) or is_local_ip(ip)
  231. self.widget = self.widget_factory(config=self.config,
  232. local_kernel=local_kernel)
  233. self.init_colors(self.widget)
  234. self.widget._existing = self.existing
  235. self.widget._may_close = not self.existing
  236. self.widget._confirm_exit = self.confirm_exit
  237. self.widget._display_banner = self.display_banner
  238. self.widget.kernel_manager = self.kernel_manager
  239. self.widget.kernel_client = self.kernel_client
  240. self.window = MainWindow(self.app,
  241. confirm_exit=self.confirm_exit,
  242. new_frontend_factory=self.new_frontend_master,
  243. slave_frontend_factory=self.new_frontend_slave,
  244. connection_frontend_factory=self.new_frontend_connection,
  245. )
  246. self.window.log = self.log
  247. self.window.add_tab_with_frontend(self.widget)
  248. self.window.init_menu_bar()
  249. # Ignore on OSX, where there is always a menu bar
  250. if sys.platform != 'darwin' and self.hide_menubar:
  251. self.window.menuBar().setVisible(False)
  252. self.window.setWindowTitle('Jupyter QtConsole')
  253. def init_colors(self, widget):
  254. """Configure the coloring of the widget"""
  255. # Note: This will be dramatically simplified when colors
  256. # are removed from the backend.
  257. # parse the colors arg down to current known labels
  258. cfg = self.config
  259. colors = cfg.ZMQInteractiveShell.colors if 'ZMQInteractiveShell.colors' in cfg else None
  260. style = cfg.JupyterWidget.syntax_style if 'JupyterWidget.syntax_style' in cfg else None
  261. sheet = cfg.JupyterWidget.style_sheet if 'JupyterWidget.style_sheet' in cfg else None
  262. # find the value for colors:
  263. if colors:
  264. colors=colors.lower()
  265. if colors in ('lightbg', 'light'):
  266. colors='lightbg'
  267. elif colors in ('dark', 'linux'):
  268. colors='linux'
  269. else:
  270. colors='nocolor'
  271. elif style:
  272. if style=='bw':
  273. colors='nocolor'
  274. elif styles.dark_style(style):
  275. colors='linux'
  276. else:
  277. colors='lightbg'
  278. else:
  279. colors=None
  280. # Configure the style
  281. if style:
  282. widget.style_sheet = styles.sheet_from_template(style, colors)
  283. widget.syntax_style = style
  284. widget._syntax_style_changed()
  285. widget._style_sheet_changed()
  286. elif colors:
  287. # use a default dark/light/bw style
  288. widget.set_default_style(colors=colors)
  289. if self.stylesheet:
  290. # we got an explicit stylesheet
  291. if os.path.isfile(self.stylesheet):
  292. with open(self.stylesheet) as f:
  293. sheet = f.read()
  294. else:
  295. raise IOError("Stylesheet %r not found." % self.stylesheet)
  296. if sheet:
  297. widget.style_sheet = sheet
  298. widget._style_sheet_changed()
  299. def init_signal(self):
  300. """allow clean shutdown on sigint"""
  301. signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
  302. # need a timer, so that QApplication doesn't block until a real
  303. # Qt event fires (can require mouse movement)
  304. # timer trick from http://stackoverflow.com/q/4938723/938949
  305. timer = QtCore.QTimer()
  306. # Let the interpreter run each 200 ms:
  307. timer.timeout.connect(lambda: None)
  308. timer.start(200)
  309. # hold onto ref, so the timer doesn't get cleaned up
  310. self._sigint_timer = timer
  311. def _deprecate_config(self, cfg, old_name, new_name):
  312. """Warn about deprecated config."""
  313. if old_name in cfg:
  314. self.log.warning(
  315. "Use %s in config, not %s. Outdated config:\n %s",
  316. new_name, old_name,
  317. '\n '.join(
  318. '{name}.{key} = {value!r}'.format(key=key, value=value,
  319. name=old_name)
  320. for key, value in self.config[old_name].items()
  321. )
  322. )
  323. cfg = cfg.copy()
  324. cfg[new_name].merge(cfg[old_name])
  325. return cfg
  326. def _init_asyncio_patch(self):
  327. """
  328. Same workaround fix as https://github.com/ipython/ipykernel/pull/456
  329. Set default asyncio policy to be compatible with tornado
  330. Tornado 6 (at least) is not compatible with the default
  331. asyncio implementation on Windows
  332. Pick the older SelectorEventLoopPolicy on Windows
  333. if the known-incompatible default policy is in use.
  334. do this as early as possible to make it a low priority and overrideable
  335. ref: https://github.com/tornadoweb/tornado/issues/2608
  336. FIXME: if/when tornado supports the defaults in asyncio,
  337. remove and bump tornado requirement for py38
  338. """
  339. if sys.platform.startswith("win") and sys.version_info >= (3, 8):
  340. import asyncio
  341. try:
  342. from asyncio import (
  343. WindowsProactorEventLoopPolicy,
  344. WindowsSelectorEventLoopPolicy,
  345. )
  346. except ImportError:
  347. pass
  348. # not affected
  349. else:
  350. if type(asyncio.get_event_loop_policy()) is WindowsProactorEventLoopPolicy:
  351. # WindowsProactorEventLoopPolicy is not compatible with tornado 6
  352. # fallback to the pre-3.8 default of Selector
  353. asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
  354. @catch_config_error
  355. def initialize(self, argv=None):
  356. self._init_asyncio_patch()
  357. self.init_qt_app()
  358. super(JupyterQtConsoleApp, self).initialize(argv)
  359. if self._dispatching:
  360. return
  361. # handle deprecated renames
  362. for old_name, new_name in [
  363. ('IPythonQtConsoleApp', 'JupyterQtConsole'),
  364. ('IPythonWidget', 'JupyterWidget'),
  365. ('RichIPythonWidget', 'RichJupyterWidget'),
  366. ]:
  367. cfg = self._deprecate_config(self.config, old_name, new_name)
  368. if cfg:
  369. self.update_config(cfg)
  370. JupyterConsoleApp.initialize(self,argv)
  371. self.init_qt_elements()
  372. self.init_signal()
  373. def start(self):
  374. super(JupyterQtConsoleApp, self).start()
  375. # draw the window
  376. if self.maximize:
  377. self.window.showMaximized()
  378. else:
  379. self.window.show()
  380. self.window.raise_()
  381. # Start the application main loop.
  382. self.app.exec_()
  383. class IPythonQtConsoleApp(JupyterQtConsoleApp):
  384. def __init__(self, *a, **kw):
  385. warn("IPythonQtConsoleApp is deprecated; use JupyterQtConsoleApp",
  386. DeprecationWarning)
  387. super(IPythonQtConsoleApp, self).__init__(*a, **kw)
  388. # -----------------------------------------------------------------------------
  389. # Main entry point
  390. # -----------------------------------------------------------------------------
  391. def main():
  392. JupyterQtConsoleApp.launch_instance()
  393. if __name__ == '__main__':
  394. main()