123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- """ A minimal application using the Qt console-style Jupyter frontend.
- This is not a complete console app, as subprocess will not be able to receive
- input, there is no real readline support, among other limitations.
- """
- # Copyright (c) Jupyter Development Team.
- # Distributed under the terms of the Modified BSD License.
- import os
- import signal
- import sys
- from warnings import warn
- # If run on Windows:
- #
- # 1. Install an exception hook which pops up a message box.
- # Pythonw.exe hides the console, so without this the application
- # silently fails to load.
- #
- # We always install this handler, because the expectation is for
- # qtconsole to bring up a GUI even if called from the console.
- # The old handler is called, so the exception is printed as well.
- # If desired, check for pythonw with an additional condition
- # (sys.executable.lower().find('pythonw.exe') >= 0).
- #
- # 2. Set AppUserModelID for Windows 7 and later so that qtconsole
- # uses its assigned taskbar icon instead of grabbing the one with
- # the same AppUserModelID
- #
- if os.name == 'nt':
- # 1.
- old_excepthook = sys.excepthook
- # Exclude this from our autogenerated API docs.
- undoc = lambda func: func
- @undoc
- def gui_excepthook(exctype, value, tb):
- try:
- import ctypes, traceback
- MB_ICONERROR = 0x00000010
- title = u'Error starting QtConsole'
- msg = u''.join(traceback.format_exception(exctype, value, tb))
- ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
- finally:
- # Also call the old exception hook to let it do
- # its thing too.
- old_excepthook(exctype, value, tb)
- sys.excepthook = gui_excepthook
- # 2.
- try:
- from ctypes import windll
- windll.shell32.SetCurrentProcessExplicitAppUserModelID("Jupyter.Qtconsole")
- except AttributeError:
- pass
- from qtpy import QtCore, QtGui, QtWidgets
- from traitlets.config.application import boolean_flag
- from traitlets.config.application import catch_config_error
- from qtconsole.jupyter_widget import JupyterWidget
- from qtconsole.rich_jupyter_widget import RichJupyterWidget
- from qtconsole import styles, __version__
- from qtconsole.mainwindow import MainWindow
- from qtconsole.client import QtKernelClient
- from qtconsole.manager import QtKernelManager
- from traitlets import (
- Dict, Unicode, CBool, Any
- )
- from jupyter_core.application import JupyterApp, base_flags, base_aliases
- from jupyter_client.consoleapp import (
- JupyterConsoleApp, app_aliases, app_flags,
- )
- from jupyter_client.localinterfaces import is_local_ip
- _examples = """
- jupyter qtconsole # start the qtconsole
- """
- #-----------------------------------------------------------------------------
- # Aliases and Flags
- #-----------------------------------------------------------------------------
- # FIXME: workaround bug in jupyter_client < 4.1 excluding base_flags,aliases
- flags = dict(base_flags)
- qt_flags = {
- 'plain' : ({'JupyterQtConsoleApp' : {'plain' : True}},
- "Disable rich text support."),
- }
- qt_flags.update(boolean_flag(
- 'banner', 'JupyterQtConsoleApp.display_banner',
- "Display a banner upon starting the QtConsole.",
- "Don't display a banner upon starting the QtConsole."
- ))
- # and app_flags from the Console Mixin
- qt_flags.update(app_flags)
- # add frontend flags to the full set
- flags.update(qt_flags)
- # start with copy of base jupyter aliases
- aliases = dict(base_aliases)
- qt_aliases = dict(
- style = 'JupyterWidget.syntax_style',
- stylesheet = 'JupyterQtConsoleApp.stylesheet',
- editor = 'JupyterWidget.editor',
- paging = 'ConsoleWidget.paging',
- )
- # and app_aliases from the Console Mixin
- qt_aliases.update(app_aliases)
- qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
- # add frontend aliases to the full set
- aliases.update(qt_aliases)
- # get flags&aliases into sets, and remove a couple that
- # shouldn't be scrubbed from backend flags:
- qt_aliases = set(qt_aliases.keys())
- qt_flags = set(qt_flags.keys())
- class JupyterQtConsoleApp(JupyterApp, JupyterConsoleApp):
- name = 'jupyter-qtconsole'
- version = __version__
- description = """
- The Jupyter QtConsole.
- This launches a Console-style application using Qt. It is not a full
- console, in that launched terminal subprocesses will not be able to accept
- input.
- """
- examples = _examples
- classes = [JupyterWidget] + JupyterConsoleApp.classes
- flags = Dict(flags)
- aliases = Dict(aliases)
- frontend_flags = Any(qt_flags)
- frontend_aliases = Any(qt_aliases)
- kernel_client_class = QtKernelClient
- kernel_manager_class = QtKernelManager
- stylesheet = Unicode('', config=True,
- help="path to a custom CSS stylesheet")
- hide_menubar = CBool(False, config=True,
- help="Start the console window with the menu bar hidden.")
- maximize = CBool(False, config=True,
- help="Start the console window maximized.")
- plain = CBool(False, config=True,
- help="Use a plaintext widget instead of rich text (plain can't print/save).")
- display_banner = CBool(True, config=True,
- help="Whether to display a banner upon starting the QtConsole."
- )
- def _plain_changed(self, name, old, new):
- kind = 'plain' if new else 'rich'
- self.config.ConsoleWidget.kind = kind
- if new:
- self.widget_factory = JupyterWidget
- else:
- self.widget_factory = RichJupyterWidget
- # the factory for creating a widget
- widget_factory = Any(RichJupyterWidget)
- def parse_command_line(self, argv=None):
- super(JupyterQtConsoleApp, self).parse_command_line(argv)
- self.build_kernel_argv(self.extra_args)
- def new_frontend_master(self):
- """ Create and return new frontend attached to new kernel, launched on localhost.
- """
- kernel_manager = self.kernel_manager_class(
- connection_file=self._new_connection_file(),
- parent=self,
- autorestart=True,
- )
- # start the kernel
- kwargs = {}
- # FIXME: remove special treatment of IPython kernels
- if self.kernel_manager.ipykernel:
- kwargs['extra_arguments'] = self.kernel_argv
- kernel_manager.start_kernel(**kwargs)
- kernel_manager.client_factory = self.kernel_client_class
- kernel_client = kernel_manager.client()
- kernel_client.start_channels(shell=True, iopub=True)
- widget = self.widget_factory(config=self.config,
- local_kernel=True)
- self.init_colors(widget)
- widget.kernel_manager = kernel_manager
- widget.kernel_client = kernel_client
- widget._existing = False
- widget._may_close = True
- widget._confirm_exit = self.confirm_exit
- widget._display_banner = self.display_banner
- return widget
- def new_frontend_connection(self, connection_file):
- """Create and return a new frontend attached to an existing kernel.
- Parameters
- ----------
- connection_file : str
- The connection_file path this frontend is to connect to
- """
- kernel_client = self.kernel_client_class(
- connection_file=connection_file,
- config=self.config,
- )
- kernel_client.load_connection_file()
- kernel_client.start_channels()
- widget = self.widget_factory(config=self.config,
- local_kernel=False)
- self.init_colors(widget)
- widget._existing = True
- widget._may_close = False
- widget._confirm_exit = False
- widget._display_banner = self.display_banner
- widget.kernel_client = kernel_client
- widget.kernel_manager = None
- return widget
- def new_frontend_slave(self, current_widget):
- """Create and return a new frontend attached to an existing kernel.
- Parameters
- ----------
- current_widget : JupyterWidget
- The JupyterWidget whose kernel this frontend is to share
- """
- kernel_client = self.kernel_client_class(
- connection_file=current_widget.kernel_client.connection_file,
- config = self.config,
- )
- kernel_client.load_connection_file()
- kernel_client.start_channels()
- widget = self.widget_factory(config=self.config,
- local_kernel=False)
- self.init_colors(widget)
- widget._existing = True
- widget._may_close = False
- widget._confirm_exit = False
- widget._display_banner = self.display_banner
- widget.kernel_client = kernel_client
- widget.kernel_manager = current_widget.kernel_manager
- return widget
- def init_qt_app(self):
- # separate from qt_elements, because it must run first
- self.app = QtWidgets.QApplication(['jupyter-qtconsole'])
- self.app.setApplicationName('jupyter-qtconsole')
- def init_qt_elements(self):
- # Create the widget.
- base_path = os.path.abspath(os.path.dirname(__file__))
- icon_path = os.path.join(base_path, 'resources', 'icon', 'JupyterConsole.svg')
- self.app.icon = QtGui.QIcon(icon_path)
- QtWidgets.QApplication.setWindowIcon(self.app.icon)
- ip = self.ip
- local_kernel = (not self.existing) or is_local_ip(ip)
- self.widget = self.widget_factory(config=self.config,
- local_kernel=local_kernel)
- self.init_colors(self.widget)
- self.widget._existing = self.existing
- self.widget._may_close = not self.existing
- self.widget._confirm_exit = self.confirm_exit
- self.widget._display_banner = self.display_banner
- self.widget.kernel_manager = self.kernel_manager
- self.widget.kernel_client = self.kernel_client
- self.window = MainWindow(self.app,
- confirm_exit=self.confirm_exit,
- new_frontend_factory=self.new_frontend_master,
- slave_frontend_factory=self.new_frontend_slave,
- connection_frontend_factory=self.new_frontend_connection,
- )
- self.window.log = self.log
- self.window.add_tab_with_frontend(self.widget)
- self.window.init_menu_bar()
- # Ignore on OSX, where there is always a menu bar
- if sys.platform != 'darwin' and self.hide_menubar:
- self.window.menuBar().setVisible(False)
- self.window.setWindowTitle('Jupyter QtConsole')
- def init_colors(self, widget):
- """Configure the coloring of the widget"""
- # Note: This will be dramatically simplified when colors
- # are removed from the backend.
- # parse the colors arg down to current known labels
- cfg = self.config
- colors = cfg.ZMQInteractiveShell.colors if 'ZMQInteractiveShell.colors' in cfg else None
- style = cfg.JupyterWidget.syntax_style if 'JupyterWidget.syntax_style' in cfg else None
- sheet = cfg.JupyterWidget.style_sheet if 'JupyterWidget.style_sheet' in cfg else None
- # find the value for colors:
- if colors:
- colors=colors.lower()
- if colors in ('lightbg', 'light'):
- colors='lightbg'
- elif colors in ('dark', 'linux'):
- colors='linux'
- else:
- colors='nocolor'
- elif style:
- if style=='bw':
- colors='nocolor'
- elif styles.dark_style(style):
- colors='linux'
- else:
- colors='lightbg'
- else:
- colors=None
- # Configure the style
- if style:
- widget.style_sheet = styles.sheet_from_template(style, colors)
- widget.syntax_style = style
- widget._syntax_style_changed()
- widget._style_sheet_changed()
- elif colors:
- # use a default dark/light/bw style
- widget.set_default_style(colors=colors)
- if self.stylesheet:
- # we got an explicit stylesheet
- if os.path.isfile(self.stylesheet):
- with open(self.stylesheet) as f:
- sheet = f.read()
- else:
- raise IOError("Stylesheet %r not found." % self.stylesheet)
- if sheet:
- widget.style_sheet = sheet
- widget._style_sheet_changed()
- def init_signal(self):
- """allow clean shutdown on sigint"""
- signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
- # need a timer, so that QApplication doesn't block until a real
- # Qt event fires (can require mouse movement)
- # timer trick from http://stackoverflow.com/q/4938723/938949
- timer = QtCore.QTimer()
- # Let the interpreter run each 200 ms:
- timer.timeout.connect(lambda: None)
- timer.start(200)
- # hold onto ref, so the timer doesn't get cleaned up
- self._sigint_timer = timer
- def _deprecate_config(self, cfg, old_name, new_name):
- """Warn about deprecated config."""
- if old_name in cfg:
- self.log.warning(
- "Use %s in config, not %s. Outdated config:\n %s",
- new_name, old_name,
- '\n '.join(
- '{name}.{key} = {value!r}'.format(key=key, value=value,
- name=old_name)
- for key, value in self.config[old_name].items()
- )
- )
- cfg = cfg.copy()
- cfg[new_name].merge(cfg[old_name])
- return cfg
- def _init_asyncio_patch(self):
- """
- Same workaround fix as https://github.com/ipython/ipykernel/pull/456
- Set default asyncio policy to be compatible with tornado
- Tornado 6 (at least) is not compatible with the default
- asyncio implementation on Windows
- Pick the older SelectorEventLoopPolicy on Windows
- if the known-incompatible default policy is in use.
- do this as early as possible to make it a low priority and overrideable
- ref: https://github.com/tornadoweb/tornado/issues/2608
- FIXME: if/when tornado supports the defaults in asyncio,
- remove and bump tornado requirement for py38
- """
- if sys.platform.startswith("win") and sys.version_info >= (3, 8):
- import asyncio
- try:
- from asyncio import (
- WindowsProactorEventLoopPolicy,
- WindowsSelectorEventLoopPolicy,
- )
- except ImportError:
- pass
- # not affected
- else:
- if type(asyncio.get_event_loop_policy()) is WindowsProactorEventLoopPolicy:
- # WindowsProactorEventLoopPolicy is not compatible with tornado 6
- # fallback to the pre-3.8 default of Selector
- asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
- @catch_config_error
- def initialize(self, argv=None):
- self._init_asyncio_patch()
- self.init_qt_app()
- super(JupyterQtConsoleApp, self).initialize(argv)
- if self._dispatching:
- return
- # handle deprecated renames
- for old_name, new_name in [
- ('IPythonQtConsoleApp', 'JupyterQtConsole'),
- ('IPythonWidget', 'JupyterWidget'),
- ('RichIPythonWidget', 'RichJupyterWidget'),
- ]:
- cfg = self._deprecate_config(self.config, old_name, new_name)
- if cfg:
- self.update_config(cfg)
- JupyterConsoleApp.initialize(self,argv)
- self.init_qt_elements()
- self.init_signal()
- def start(self):
- super(JupyterQtConsoleApp, self).start()
- # draw the window
- if self.maximize:
- self.window.showMaximized()
- else:
- self.window.show()
- self.window.raise_()
- # Start the application main loop.
- self.app.exec_()
- class IPythonQtConsoleApp(JupyterQtConsoleApp):
- def __init__(self, *a, **kw):
- warn("IPythonQtConsoleApp is deprecated; use JupyterQtConsoleApp",
- DeprecationWarning)
- super(IPythonQtConsoleApp, self).__init__(*a, **kw)
- # -----------------------------------------------------------------------------
- # Main entry point
- # -----------------------------------------------------------------------------
- def main():
- JupyterQtConsoleApp.launch_instance()
- if __name__ == '__main__':
- main()
|