Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR: Show kernel initialization errors in the IPython console #3861

Merged
merged 17 commits into from
Dec 21, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f98fc7a
Main Window: Create our temp directory as part of its initialization
ccordoba12 Dec 20, 2016
c7ce5dd
IPython Console: Save kernel errors on a file in our temp directory
ccordoba12 Dec 20, 2016
e4ba007
IPython Console: Show kernel initialization error messages in its cli…
ccordoba12 Dec 20, 2016
410abaf
IPython Console: Replace eol chars with <br>'s in kernel error messages
ccordoba12 Dec 20, 2016
fa4c837
IPython Console: Add missing slot signature
ccordoba12 Dec 20, 2016
b8fd216
IPython Console: Simplify how clients are bound to shellwidgets
ccordoba12 Dec 20, 2016
62e660c
IPython Console: Add a shortcut to restart kernels
ccordoba12 Dec 20, 2016
77e814d
IPython Console: Make restart action to show the config shortcut just…
ccordoba12 Dec 20, 2016
e06c968
IPython Console: Simplify how the loading page is shown
ccordoba12 Dec 20, 2016
0ea6087
IPython Console: Refactor methods to show and hide the kernel loading…
ccordoba12 Dec 20, 2016
7ee81f5
IPython Console: Hide info widget if visible when restarting the kernel
ccordoba12 Dec 20, 2016
05971b0
IPython Console: Reset console when restarting the kernel
ccordoba12 Dec 20, 2016
45106dd
IPython Console: Remove kernel_stderr attribute because it was not wo…
ccordoba12 Dec 20, 2016
1eaf4db
IPython Console: Remove unnecessary menu action
ccordoba12 Dec 20, 2016
b098152
Fix style issues
ccordoba12 Dec 20, 2016
ca7d482
IPython Console: Rename set_exit_action to set_exit_callback
ccordoba12 Dec 21, 2016
b1a97a3
IPython Console: Fix error when creating kernel stderr file in Python 2
ccordoba12 Dec 21, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions spyder/app/mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@ def signal_handler(signum, frame=None):
mac_style = mac_style.replace('$IMAGE_PATH', img_path)
self.setStyleSheet(mac_style)

# Create our TEMPDIR
if not osp.isdir(programs.TEMPDIR):
os.mkdir(programs.TEMPDIR)

# Shortcut management data
self.shortcut_data = []

Expand Down
1 change: 1 addition & 0 deletions spyder/config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@
# ---- In widgets/ipythonconsole/shell.py ----
'ipython_console/new tab': "Ctrl+T",
'ipython_console/reset namespace': "Ctrl+Alt+R",
'ipython_console/restart kernel': "Ctrl+.",
# ---- In widgets/arraybuider.py ----
'array_builder/enter array inline': "Ctrl+Alt+M",
'array_builder/enter array table': "Ctrl+M",
Expand Down
59 changes: 38 additions & 21 deletions spyder/plugins/ipythonconsole.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

# Standard library imports
import atexit
import codecs
import os
import os.path as osp
import uuid
Expand Down Expand Up @@ -728,20 +729,22 @@ def refresh_plugin(self):
self.update_plugin_title.emit()

def get_plugin_actions(self):
"""Return a list of actions related to plugin"""
ctrl = "Cmd" if sys.platform == "darwin" else "Ctrl"
main_create_client_action = create_action(self,
_("Open an &IPython console"),
None, ima.icon('ipython_console'),
triggered=self.create_new_client,
tip=_("Use %s+T when the console is selected "
"to open a new one") % ctrl)
create_client_action = create_action(self,
_("Open a new console"),
QKeySequence("Ctrl+T"),
ima.icon('ipython_console'),
triggered=self.create_new_client,
context=Qt.WidgetWithChildrenShortcut)
"""Return a list of actions related to plugin."""
create_client_action = create_action(
self,
_("Open an &IPython console"),
icon=ima.icon('ipython_console'),
triggered=self.create_new_client,
context=Qt.WidgetWithChildrenShortcut)
self.register_shortcut(create_client_action, context="ipython_console",
name="New tab")

restart_action = create_action(self, _("Restart kernel"),
icon=ima.icon('restart'),
triggered=self.restart_kernel,
context=Qt.WidgetWithChildrenShortcut)
self.register_shortcut(restart_action, context="ipython_console",
name="Restart kernel")

connect_to_kernel_action = create_action(self,
_("Connect to an existing kernel"), None, None,
Expand All @@ -750,11 +753,13 @@ def get_plugin_actions(self):

# Add the action to the 'Consoles' menu on the main window
main_consoles_menu = self.main.consoles_menu_actions
main_consoles_menu.insert(0, main_create_client_action)
main_consoles_menu += [MENU_SEPARATOR, connect_to_kernel_action]
main_consoles_menu.insert(0, create_client_action)
main_consoles_menu += [MENU_SEPARATOR, restart_action,
connect_to_kernel_action]

# Plugin actions
self.menu_actions = [create_client_action, connect_to_kernel_action]
self.menu_actions = [restart_action, MENU_SEPARATOR,
create_client_action, connect_to_kernel_action]

return self.menu_actions

Expand Down Expand Up @@ -915,7 +920,9 @@ def create_client_for_kernel(self):
def connect_client_to_kernel(self, client):
"""Connect a client to its kernel"""
connection_file = client.connection_file
km, kc = self.create_kernel_manager_and_kernel_client(connection_file)
stderr_file = client.stderr_file
km, kc = self.create_kernel_manager_and_kernel_client(connection_file,
stderr_file)

kc.started_channels.connect(lambda c=client: self.process_started(c))
kc.stopped_channels.connect(lambda c=client: self.process_finished(c))
Expand Down Expand Up @@ -1294,13 +1301,17 @@ def create_kernel_spec(self):

return KernelSpec(resource_dir='', **kernel_dict)

def create_kernel_manager_and_kernel_client(self, connection_file):
"""Create kernel manager and client"""
def create_kernel_manager_and_kernel_client(self, connection_file,
stderr_file):
"""Create kernel manager and client."""
# Kernel manager
kernel_manager = QtKernelManager(connection_file=connection_file,
config=None, autorestart=True)
kernel_manager._kernel_spec = self.create_kernel_spec()
kernel_manager.start_kernel()

# Save stderr in a file to read it later in case of errors
stderr = codecs.open(stderr_file, 'w', encoding='utf-8')
kernel_manager.start_kernel(stderr=stderr)

# Kernel client
kernel_client = kernel_manager.client()
Expand All @@ -1311,6 +1322,12 @@ def create_kernel_manager_and_kernel_client(self, connection_file):

return kernel_manager, kernel_client

def restart_kernel(self):
"""Restart kernel of current client."""
client = self.get_current_client()
if client is not None:
client.restart_kernel()

#------ Public API (for tabs) ---------------------------------------------
def add_tab(self, widget, name):
"""Add tab"""
Expand Down
3 changes: 0 additions & 3 deletions spyder/utils/programs.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,9 +396,6 @@ def is_module_installed(module_name, version=None, installed_version=None,
in a determined interpreter
"""
if interpreter:
if not osp.isdir(TEMPDIR):
os.mkdir(TEMPDIR)

if osp.isfile(interpreter) and ('python' in interpreter):
checkver = inspect.getsource(check_version)
get_modver = inspect.getsource(get_module_version)
Expand Down
87 changes: 60 additions & 27 deletions spyder/widgets/ipythonconsole/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# Standard library imports
from __future__ import absolute_import # Fix for Issue 1356

import codecs
import os
import os.path as osp
from string import Template
Expand All @@ -30,6 +31,8 @@
get_module_source_path)
from spyder.config.gui import get_font, get_shortcut
from spyder.utils import icon_manager as ima
from spyder.utils import sourcecode
from spyder.utils.programs import TEMPDIR
from spyder.utils.qthelpers import (add_actions, create_action,
create_toolbutton)
from spyder.widgets.browser import WebView
Expand Down Expand Up @@ -103,16 +106,15 @@ def __init__(self, plugin, name, history_filename, config_options,

# --- Widgets
self.shellwidget = ShellWidget(config=config_options,
ipyclient=self,
additional_options=additional_options,
interpreter_versions=interpreter_versions,
external_kernel=external_kernel,
local_kernel=True)
self.shellwidget.hide()
self.infowidget = WebView(self)
self.set_infowidget_font()
self.loading_page = self._create_loading_page()
self.infowidget.setHtml(self.loading_page,
QUrl.fromLocalFile(CSS_PATH))
self._show_loading_page()

# --- Layout
vlayout = QVBoxLayout()
Expand All @@ -133,16 +135,24 @@ def __init__(self, plugin, name, history_filename, config_options,
# As soon as some content is printed in the console, stop
# our loading animation
document = self.get_control().document()
document.contentsChange.connect(self._stop_loading_animation)
document.contentsChange.connect(self._hide_loading_page)

#------ Public API --------------------------------------------------------
@property
def stderr_file(self):
"""Filename to save kernel stderr output."""
json_file = osp.basename(self.connection_file)
stderr_file = json_file.split('json')[0] + 'stderr'
stderr_file = osp.join(TEMPDIR, stderr_file)
return stderr_file

def configure_shellwidget(self, give_focus=True):
"""Configure shellwidget after kernel is started"""
if give_focus:
self.get_control().setFocus()

# Connect shellwidget to the client
self.shellwidget.set_ipyclient(self)
# Set exit callback
self.shellwidget.set_exit_callback()

# To save history
self.shellwidget.executing.connect(self.add_to_history)
Expand All @@ -163,6 +173,10 @@ def configure_shellwidget(self, give_focus=True):
# To disable the stop button after execution stopped
self.shellwidget.executed.connect(self.disable_stop_button)

# To show kernel restarted/died messages
self.shellwidget.sig_kernel_restarted.connect(
self.kernel_restarted_message)

def enable_stop_button(self):
self.stop_button.setEnabled(True)

Expand All @@ -180,7 +194,12 @@ def stop_button_click_handler(self):
self.shellwidget.write_to_stdin('exit')

def show_kernel_error(self, error):
"""Show kernel initialization errors in infowidget"""
"""Show kernel initialization errors in infowidget."""
# Replace end of line chars with <br>
eol = sourcecode.get_eol_chars(error)
if eol:
error = error.replace(eol, '<br>')

# Don't break lines in hyphens
# From http://stackoverflow.com/q/7691569/438386
error = error.replace('-', '&#8209')
Expand Down Expand Up @@ -217,27 +236,18 @@ def get_kernel(self):

def get_options_menu(self):
"""Return options menu"""
restart_action = create_action(self, _("Restart kernel"),
shortcut=QKeySequence("Ctrl+."),
icon=ima.icon('restart'),
triggered=self.restart_kernel,
context=Qt.WidgetWithChildrenShortcut)

# Main menu
if self.menu_actions is not None:
actions = [restart_action, None] + self.menu_actions
else:
actions = [restart_action]
return actions
return self.menu_actions

def get_toolbar_buttons(self):
"""Return toolbar buttons list"""
"""Return toolbar buttons list."""
buttons = []
# Code to add the stop button
if self.stop_button is None:
self.stop_button = create_toolbutton(self, text=_("Stop"),
icon=self.stop_icon,
tip=_("Stop the current command"))
self.stop_button = create_toolbutton(
self,
text=_("Stop"),
icon=self.stop_icon,
tip=_("Stop the current command"))
self.disable_stop_button()
# set click event handler
self.stop_button.clicked.connect(self.stop_button_click_handler)
Expand Down Expand Up @@ -320,6 +330,9 @@ def restart_kernel(self):
if result == QMessageBox.Yes:
sw = self.shellwidget
if sw.kernel_manager:
if self.infowidget.isVisible():
self.infowidget.hide()
sw.show()
try:
sw.kernel_manager.restart_kernel()
except RuntimeError as e:
Expand All @@ -328,15 +341,28 @@ def restart_kernel(self):
before_prompt=True
)
else:
sw.reset(clear=True)
sw._append_html(_("<br>Restarting kernel...\n<hr><br>"),
before_prompt=True,
before_prompt=False,
)
else:
sw._append_plain_text(
_('Cannot restart a kernel not started by Spyder\n'),
before_prompt=True
)

@Slot(str)
def kernel_restarted_message(self, msg):
"""Show kernel restarted/died messages."""
stderr = codecs.open(self.stderr_file, 'r', encoding='utf-8').read()

if stderr:
self.show_kernel_error('<tt>%s</tt>' % stderr)
else:
self.shellwidget._append_html("<br>%s<hr><br>" % msg,
before_prompt=False)


@Slot()
def inspect_object(self):
"""Show how to inspect an object with our Help plugin"""
Expand Down Expand Up @@ -390,11 +416,18 @@ def _create_loading_page(self):
message=message)
return page

def _stop_loading_animation(self):
"""Stop animation shown while the kernel is starting"""
def _show_loading_page(self):
"""Show animation while the kernel is loading."""
self.shellwidget.hide()
self.infowidget.show()
self.infowidget.setHtml(self.loading_page,
QUrl.fromLocalFile(CSS_PATH))

def _hide_loading_page(self):
"""Hide animation shown while the kernel is loading."""
self.infowidget.hide()
self.shellwidget.show()
self.infowidget.setHtml(BLANK)

document = self.get_control().document()
document.contentsChange.disconnect(self._stop_loading_animation)
document.contentsChange.disconnect(self._hide_loading_page)
28 changes: 17 additions & 11 deletions spyder/widgets/ipythonconsole/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,33 +45,32 @@ class ShellWidget(NamepaceBrowserWidget, HelpWidget, DebuggingWidget):
focus_changed = Signal()
new_client = Signal()
sig_got_reply = Signal()
sig_kernel_restarted = Signal(str)

def __init__(self, additional_options, interpreter_versions,
def __init__(self, ipyclient, additional_options, interpreter_versions,
external_kernel, *args, **kw):
# To override the Qt widget used by RichJupyterWidget
self.custom_control = ControlWidget
self.custom_page_control = PageControlWidget
super(ShellWidget, self).__init__(*args, **kw)

self.ipyclient = ipyclient
self.additional_options = additional_options
self.interpreter_versions = interpreter_versions
self.external_kernel = external_kernel

self.set_background_color()

# Additional variables
self.ipyclient = None
self.external_kernel = external_kernel

# Keyboard shortcuts
self.shortcuts = self.create_shortcuts()

# To save kernel replies in silent execution
self._kernel_reply = None

#---- Public API ----------------------------------------------------------
def set_ipyclient(self, ipyclient):
"""Bind this shell widget to an IPython client one"""
self.ipyclient = ipyclient
self.exit_requested.connect(ipyclient.exit_callback)
def set_exit_callback(self):
"""Set exit callback for this shell."""
self.exit_requested.connect(self.ipyclient.exit_callback)

def is_running(self):
if self.kernel_client is not None and \
Expand Down Expand Up @@ -165,6 +164,9 @@ def create_shortcuts(self):
parent=self)
clear_console = config_shortcut(self.clear_console, context='Console',
name='Clear shell', parent=self)
restart_kernel = config_shortcut(self.ipyclient.restart_kernel,
context='ipython_console',
name='Restart kernel', parent=self)
new_tab = config_shortcut(lambda: self.new_client.emit(),
context='ipython_console', name='new tab', parent=self)
reset_namespace = config_shortcut(lambda: self.reset_namespace(),
Expand All @@ -177,8 +179,8 @@ def create_shortcuts(self):
context='array_builder',
name='enter array table', parent=self)

return [inspect, clear_console, new_tab, reset_namespace,
array_inline, array_table]
return [inspect, clear_console, restart_kernel, new_tab,
reset_namespace, array_inline, array_table]

# --- To communicate with the kernel
def silent_execute(self, code):
Expand Down Expand Up @@ -274,6 +276,10 @@ def _banner_default(self):
else:
return self.short_banner()

def _kernel_restarted_message(self, died=True):
msg = _("Kernel died, restarting") if died else _("Kernel restarting")
self.sig_kernel_restarted.emit(msg)

#---- Qt methods ----------------------------------------------------------
def focusInEvent(self, event):
"""Reimplement Qt method to send focus change notification"""
Expand Down