qtconsoleapp.py
421 lines
| 15.6 KiB
| text/x-python
|
PythonLexer
|
r2801 | """ A minimal application using the Qt console-style IPython frontend. | ||
|
r4021 | |||
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. | ||||
Authors: | ||||
* Evan Patterson | ||||
* Min RK | ||||
* Erik Tollerud | ||||
* Fernando Perez | ||||
|
r2758 | """ | ||
|
r2961 | #----------------------------------------------------------------------------- | ||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
|
r3971 | # stdlib imports | ||
import os | ||||
import signal | ||||
import sys | ||||
# System library imports | ||||
|
r3304 | from IPython.external.qt import QtGui | ||
|
r3170 | from pygments.styles import get_all_styles | ||
|
r3304 | |||
|
r2758 | # Local imports | ||
|
r3983 | from IPython.config.application import boolean_flag | ||
|
r4024 | from IPython.core.application import BaseIPythonApplication | ||
from IPython.core.profiledir import ProfileDir | ||||
|
r2801 | from IPython.frontend.qt.console.frontend_widget import FrontendWidget | ||
from IPython.frontend.qt.console.ipython_widget import IPythonWidget | ||||
from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget | ||||
|
r3170 | from IPython.frontend.qt.console import styles | ||
|
r2758 | from IPython.frontend.qt.kernelmanager import QtKernelManager | ||
|
r3971 | from IPython.utils.traitlets import ( | ||
|
r3983 | Dict, List, Unicode, Int, CaselessStrEnum, CBool, Any | ||
|
r3971 | ) | ||
from IPython.zmq.ipkernel import ( | ||||
flags as ipkernel_flags, | ||||
aliases as ipkernel_aliases, | ||||
IPKernelApp | ||||
) | ||||
|
r4015 | from IPython.zmq.session import Session | ||
|
r3971 | from IPython.zmq.zmqshell import ZMQInteractiveShell | ||
|
r2758 | |||
|
r2961 | #----------------------------------------------------------------------------- | ||
|
r3144 | # Network Constants | ||
|
r2961 | #----------------------------------------------------------------------------- | ||
|
r3144 | from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS | ||
|
r2823 | |||
|
r2961 | #----------------------------------------------------------------------------- | ||
# Classes | ||||
#----------------------------------------------------------------------------- | ||||
class MainWindow(QtGui.QMainWindow): | ||||
#--------------------------------------------------------------------------- | ||||
# 'object' interface | ||||
#--------------------------------------------------------------------------- | ||||
|
r3983 | def __init__(self, app, frontend, existing=False, may_close=True, | ||
confirm_exit=True): | ||||
|
r2961 | """ Create a MainWindow for the specified FrontendWidget. | ||
|
r3100 | |||
|
r3105 | The app is passed as an argument to allow for different | ||
closing behavior depending on whether we are the Kernel's parent. | ||||
|
r3142 | If existing is True, then this Console does not own the Kernel. | ||
If may_close is True, then this Console is permitted to close the kernel | ||||
|
r2961 | """ | ||
super(MainWindow, self).__init__() | ||||
|
r3104 | self._app = app | ||
|
r2961 | self._frontend = frontend | ||
|
r3090 | self._existing = existing | ||
|
r3144 | if existing: | ||
|
r3142 | self._may_close = may_close | ||
else: | ||||
self._may_close = True | ||||
|
r2961 | self._frontend.exit_requested.connect(self.close) | ||
|
r3983 | self._confirm_exit = confirm_exit | ||
|
r2961 | self.setCentralWidget(frontend) | ||
#--------------------------------------------------------------------------- | ||||
# QWidget interface | ||||
#--------------------------------------------------------------------------- | ||||
def closeEvent(self, event): | ||||
|
r3190 | """ Close the window and the kernel (if necessary). | ||
This will prompt the user if they are finished with the kernel, and if | ||||
so, closes the kernel cleanly. Alternatively, if the exit magic is used, | ||||
it closes without prompt. | ||||
|
r2961 | """ | ||
|
r3185 | keepkernel = None #Use the prompt by default | ||
if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic | ||||
keepkernel = self._frontend._keep_kernel_on_exit | ||||
|
r2982 | kernel_manager = self._frontend.kernel_manager | ||
|
r3183 | |||
|
r3983 | if keepkernel is None and not self._confirm_exit: | ||
# don't prompt, just terminate the kernel if we own it | ||||
# or leave it alone if we don't | ||||
keepkernel = not self._existing | ||||
|
r3185 | if keepkernel is None: #show prompt | ||
|
r3183 | if kernel_manager and kernel_manager.channels_running: | ||
title = self.window().windowTitle() | ||||
cancel = QtGui.QMessageBox.Cancel | ||||
okay = QtGui.QMessageBox.Ok | ||||
if self._may_close: | ||||
msg = "You are closing this Console window." | ||||
info = "Would you like to quit the Kernel and all attached Consoles as well?" | ||||
justthis = QtGui.QPushButton("&No, just this Console", self) | ||||
justthis.setShortcut('N') | ||||
closeall = QtGui.QPushButton("&Yes, quit everything", self) | ||||
closeall.setShortcut('Y') | ||||
|
r3304 | box = QtGui.QMessageBox(QtGui.QMessageBox.Question, | ||
title, msg) | ||||
|
r3183 | box.setInformativeText(info) | ||
box.addButton(cancel) | ||||
box.addButton(justthis, QtGui.QMessageBox.NoRole) | ||||
box.addButton(closeall, QtGui.QMessageBox.YesRole) | ||||
box.setDefaultButton(closeall) | ||||
box.setEscapeButton(cancel) | ||||
reply = box.exec_() | ||||
if reply == 1: # close All | ||||
kernel_manager.shutdown_kernel() | ||||
#kernel_manager.stop_channels() | ||||
event.accept() | ||||
elif reply == 0: # close Console | ||||
if not self._existing: | ||||
# Have kernel: don't quit, just close the window | ||||
self._app.setQuitOnLastWindowClosed(False) | ||||
self.deleteLater() | ||||
event.accept() | ||||
else: | ||||
event.ignore() | ||||
|
r3142 | else: | ||
|
r3183 | reply = QtGui.QMessageBox.question(self, title, | ||
"Are you sure you want to close this Console?"+ | ||||
"\nThe Kernel and other Consoles will remain active.", | ||||
okay|cancel, | ||||
defaultButton=okay | ||||
) | ||||
if reply == okay: | ||||
event.accept() | ||||
else: | ||||
event.ignore() | ||||
|
r3185 | elif keepkernel: #close console but leave kernel running (no prompt) | ||
|
r3183 | if kernel_manager and kernel_manager.channels_running: | ||
if not self._existing: | ||||
# I have the kernel: don't quit, just close the window | ||||
self._app.setQuitOnLastWindowClosed(False) | ||||
event.accept() | ||||
|
r3185 | else: #close console and kernel (no prompt) | ||
|
r3183 | if kernel_manager and kernel_manager.channels_running: | ||
kernel_manager.shutdown_kernel() | ||||
event.accept() | ||||
|
r2961 | |||
#----------------------------------------------------------------------------- | ||||
|
r3971 | # Aliases and Flags | ||
|
r2961 | #----------------------------------------------------------------------------- | ||
|
r2758 | |||
|
r3971 | flags = dict(ipkernel_flags) | ||
flags.update({ | ||||
'existing' : ({'IPythonQtConsoleApp' : {'existing' : True}}, | ||||
"Connect to an existing kernel."), | ||||
'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}}, | ||||
"Use a pure Python kernel instead of an IPython kernel."), | ||||
'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}}, | ||||
"Disable rich text support."), | ||||
}) | ||||
|
r3983 | flags.update(boolean_flag( | ||
'gui-completion', 'ConsoleWidget.gui_completion', | ||||
"use a GUI widget for tab completion", | ||||
"use plaintext output for completion" | ||||
)) | ||||
flags.update(boolean_flag( | ||||
'confirm-exit', 'IPythonQtConsoleApp.confirm_exit', | ||||
"""Set to display confirmation dialog on exit. You can always use 'exit' or 'quit', | ||||
to force a direct exit without any confirmation. | ||||
""", | ||||
"""Don't prompt the user when exiting. This will terminate the kernel | ||||
if it is owned by the frontend, and leave it alive if it is external. | ||||
""" | ||||
)) | ||||
# the flags that are specific to the frontend | ||||
# these must be scrubbed before being passed to the kernel, | ||||
# or it will raise an error on unrecognized flags | ||||
qt_flags = ['existing', 'pure', 'plain', 'gui-completion', 'no-gui-completion', | ||||
'confirm-exit', 'no-confirm-exit'] | ||||
|
r3971 | |||
aliases = dict(ipkernel_aliases) | ||||
aliases.update(dict( | ||||
hb = 'IPythonQtConsoleApp.hb_port', | ||||
shell = 'IPythonQtConsoleApp.shell_port', | ||||
iopub = 'IPythonQtConsoleApp.iopub_port', | ||||
stdin = 'IPythonQtConsoleApp.stdin_port', | ||||
ip = 'IPythonQtConsoleApp.ip', | ||||
plain = 'IPythonQtConsoleApp.plain', | ||||
pure = 'IPythonQtConsoleApp.pure', | ||||
|
r3983 | gui_completion = 'ConsoleWidget.gui_completion', | ||
|
r3971 | style = 'IPythonWidget.syntax_style', | ||
stylesheet = 'IPythonQtConsoleApp.stylesheet', | ||||
colors = 'ZMQInteractiveShell.colors', | ||||
editor = 'IPythonWidget.editor', | ||||
)) | ||||
#----------------------------------------------------------------------------- | ||||
# IPythonQtConsole | ||||
#----------------------------------------------------------------------------- | ||||
class IPythonQtConsoleApp(BaseIPythonApplication): | ||||
name = 'ipython-qtconsole' | ||||
default_config_file_name='ipython_config.py' | ||||
|
r4021 | |||
description = """ | ||||
The IPython QtConsole. | ||||
This launches a Console-style application using Qt. It is not a full | ||||
console, in that launched terminal subprocesses will not. | ||||
The QtConsole supports various extra features beyond the | ||||
""" | ||||
|
r4015 | classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session] | ||
|
r3971 | flags = Dict(flags) | ||
aliases = Dict(aliases) | ||||
kernel_argv = List(Unicode) | ||||
# connection info: | ||||
ip = Unicode(LOCALHOST, config=True, | ||||
help="""Set the kernel\'s IP address [default localhost]. | ||||
If the IP address is something other than localhost, then | ||||
Consoles on other machines will be able to connect | ||||
to the Kernel, so be careful!""" | ||||
) | ||||
hb_port = Int(0, config=True, | ||||
help="set the heartbeat port [default: random]") | ||||
shell_port = Int(0, config=True, | ||||
help="set the shell (XREP) port [default: random]") | ||||
iopub_port = Int(0, config=True, | ||||
help="set the iopub (PUB) port [default: random]") | ||||
stdin_port = Int(0, config=True, | ||||
help="set the stdin (XREQ) port [default: random]") | ||||
|
r3983 | existing = CBool(False, config=True, | ||
|
r3971 | help="Whether to connect to an already running Kernel.") | ||
stylesheet = Unicode('', config=True, | ||||
help="path to a custom CSS stylesheet") | ||||
|
r3983 | pure = CBool(False, config=True, | ||
|
r3971 | help="Use a pure Python kernel instead of an IPython kernel.") | ||
|
r3983 | plain = CBool(False, config=True, | ||
|
r3976 | help="Use a plaintext widget instead of rich text (plain can't print/save).") | ||
|
r3971 | |||
def _pure_changed(self, name, old, new): | ||||
kind = 'plain' if self.plain else 'rich' | ||||
self.config.ConsoleWidget.kind = kind | ||||
if self.pure: | ||||
self.widget_factory = FrontendWidget | ||||
elif self.plain: | ||||
self.widget_factory = IPythonWidget | ||||
|
r3173 | else: | ||
|
r3971 | self.widget_factory = RichIPythonWidget | ||
_plain_changed = _pure_changed | ||||
|
r3983 | confirm_exit = CBool(True, config=True, | ||
help=""" | ||||
Set to display confirmation dialog on exit. You can always use 'exit' or 'quit', | ||||
to force a direct exit without any confirmation.""", | ||||
) | ||||
|
r3971 | # the factory for creating a widget | ||
widget_factory = Any(RichIPythonWidget) | ||||
def parse_command_line(self, argv=None): | ||||
super(IPythonQtConsoleApp, self).parse_command_line(argv) | ||||
if argv is None: | ||||
argv = sys.argv[1:] | ||||
self.kernel_argv = list(argv) # copy | ||||
# scrub frontend-specific flags | ||||
for a in argv: | ||||
if a.startswith('--') and a[2:] in qt_flags: | ||||
self.kernel_argv.remove(a) | ||||
def init_kernel_manager(self): | ||||
# Don't let Qt or ZMQ swallow KeyboardInterupts. | ||||
signal.signal(signal.SIGINT, signal.SIG_DFL) | ||||
# Create a KernelManager and start a kernel. | ||||
self.kernel_manager = QtKernelManager( | ||||
|
r3974 | shell_address=(self.ip, self.shell_port), | ||
|
r3971 | sub_address=(self.ip, self.iopub_port), | ||
|
r3974 | stdin_address=(self.ip, self.stdin_port), | ||
|
r4015 | hb_address=(self.ip, self.hb_port), | ||
config=self.config | ||||
|
r3971 | ) | ||
# start the kernel | ||||
if not self.existing: | ||||
kwargs = dict(ip=self.ip, ipython=not self.pure) | ||||
kwargs['extra_arguments'] = self.kernel_argv | ||||
self.kernel_manager.start_kernel(**kwargs) | ||||
self.kernel_manager.start_channels() | ||||
def init_qt_elements(self): | ||||
# Create the widget. | ||||
self.app = QtGui.QApplication([]) | ||||
local_kernel = (not self.existing) or self.ip in LOCAL_IPS | ||||
self.widget = self.widget_factory(config=self.config, | ||||
local_kernel=local_kernel) | ||||
self.widget.kernel_manager = self.kernel_manager | ||||
self.window = MainWindow(self.app, self.widget, self.existing, | ||||
|
r3983 | may_close=local_kernel, | ||
confirm_exit=self.confirm_exit) | ||||
|
r3971 | self.window.setWindowTitle('Python' if self.pure else 'IPython') | ||
def init_colors(self): | ||||
"""Configure the coloring of the widget""" | ||||
# Note: This will be dramatically simplified when colors | ||||
# are removed from the backend. | ||||
if self.pure: | ||||
# only IPythonWidget supports styling | ||||
return | ||||
# parse the colors arg down to current known labels | ||||
try: | ||||
colors = self.config.ZMQInteractiveShell.colors | ||||
except AttributeError: | ||||
colors = None | ||||
try: | ||||
style = self.config.IPythonWidget.colors | ||||
except AttributeError: | ||||
style = 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' | ||||
|
r3173 | else: | ||
|
r3971 | colors=None | ||
# Configure the style. | ||||
widget = self.widget | ||||
if style: | ||||
widget.style_sheet = styles.sheet_from_template(style, colors) | ||||
widget.syntax_style = style | ||||
|
r3170 | widget._syntax_style_changed() | ||
widget._style_sheet_changed() | ||||
|
r3171 | elif colors: | ||
# use a default style | ||||
widget.set_default_style(colors=colors) | ||||
|
r3170 | else: | ||
# this is redundant for now, but allows the widget's | ||||
# defaults to change | ||||
|
r3171 | widget.set_default_style() | ||
|
r3170 | |||
|
r3971 | if self.stylesheet: | ||
|
r3170 | # we got an expicit stylesheet | ||
|
r3971 | if os.path.isfile(self.stylesheet): | ||
with open(self.stylesheet) as f: | ||||
|
r3170 | sheet = f.read() | ||
widget.style_sheet = sheet | ||||
widget._style_sheet_changed() | ||||
else: | ||||
|
r3971 | raise IOError("Stylesheet %r not found."%self.stylesheet) | ||
def initialize(self, argv=None): | ||||
super(IPythonQtConsoleApp, self).initialize(argv) | ||||
self.init_kernel_manager() | ||||
self.init_qt_elements() | ||||
self.init_colors() | ||||
def start(self): | ||||
# draw the window | ||||
self.window.show() | ||||
|
r3170 | |||
|
r3971 | # Start the application main loop. | ||
self.app.exec_() | ||||
|
r2841 | |||
|
r3971 | #----------------------------------------------------------------------------- | ||
# Main entry point | ||||
#----------------------------------------------------------------------------- | ||||
def main(): | ||||
app = IPythonQtConsoleApp() | ||||
app.initialize() | ||||
app.start() | ||||
|
r2758 | |||
if __name__ == '__main__': | ||||
main() | ||||