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: Implement dedicated IPython consoles #4691

Merged
merged 15 commits into from
Jul 4, 2017
Merged
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions spyder/app/mainwindow.py
Original file line number Diff line number Diff line change
@@ -2107,7 +2107,8 @@ def set_splash(self, message):

def remove_tmpdir(self):
"""Remove Spyder temporary directory"""
shutil.rmtree(programs.TEMPDIR, ignore_errors=True)
if CONF.get('main', 'single_instance') and not self.new_instance:
shutil.rmtree(programs.TEMPDIR, ignore_errors=True)

def closeEvent(self, event):
"""closeEvent reimplementation"""
@@ -2433,8 +2434,8 @@ def open_external_console(self, fname, wdir, args, interact, debug, python,

def execute_in_external_console(self, lines, focus_to_editor):
"""
Execute lines in external or IPython console and eventually set focus
to the editor
Execute lines in IPython console and eventually set focus
to the Editor.
"""
console = self.ipyconsole
console.visibility_changed(True)
6 changes: 3 additions & 3 deletions spyder/plugins/editor.py
Original file line number Diff line number Diff line change
@@ -369,7 +369,7 @@ class Editor(SpyderPluginWidget):
DISABLE_ACTIONS_WHEN_HIDDEN = False # SpyderPluginWidget class attribute

# Signals
run_in_current_ipyclient = Signal(str, str, str, bool, bool, bool)
run_in_current_ipyclient = Signal(str, str, str, bool, bool, bool, bool)
exec_in_extconsole = Signal(str, bool)
redirect_stdio = Signal(bool)
open_dir = Signal(str)
@@ -2408,10 +2408,10 @@ def re_run_file(self):
(fname, wdir, args, interact, debug,
python, python_args, current, systerm,
post_mortem, clear_namespace) = self.__last_ec_exec
if current:
if not systerm:
self.run_in_current_ipyclient.emit(fname, wdir, args,
debug, post_mortem,
clear_namespace)
current, clear_namespace)
else:
self.main.open_external_console(fname, wdir, args, interact,
debug, python, python_args,
92 changes: 65 additions & 27 deletions spyder/plugins/ipythonconsole.py
Original file line number Diff line number Diff line change
@@ -621,7 +621,8 @@ def __init__(self, parent, testing=False):
os.mkdir(programs.TEMPDIR)

layout = QVBoxLayout()
self.tabwidget = Tabs(self, self.menu_actions, rename_tabs=True)
self.tabwidget = Tabs(self, self.menu_actions, rename_tabs=True,
split_char='/', split_index=0)
if hasattr(self.tabwidget, 'setDocumentMode')\
and not sys.platform == 'darwin':
# Don't set document mode to true on OSX because it generates
@@ -799,8 +800,7 @@ def register_plugin(self):
self.editor.load(fname, lineno, word,
processevents=processevents))
self.editor.breakpoints_saved.connect(self.set_spyder_breakpoints)
self.editor.run_in_current_ipyclient.connect(
self.run_script_in_current_client)
self.editor.run_in_current_ipyclient.connect(self.run_script)
self.main.workingdirectory.set_current_console_wd.connect(
self.set_current_client_working_directory)
self.explorer.open_interpreter.connect(self.create_client_from_path)
@@ -830,11 +830,20 @@ def get_current_shellwidget(self):
if client is not None:
return client.shellwidget

def run_script_in_current_client(self, filename, wdir, args, debug,
post_mortem, clear_variables):
"""Run script in current client, if any"""
def run_script(self, filename, wdir, args, debug, post_mortem,
current_client, clear_variables):
"""Run script in current or dedicated client"""
norm = lambda text: remove_backslashes(to_text_string(text))
client = self.get_current_client()

# Select client to execute code on it
if current_client:
client = self.get_current_client()
else:
client = self.get_client_for_file(filename)
if client is None:
self.create_client_for_file(filename)
client = self.get_current_client()

if client is not None:
# Internal kernels, use runfile
if client.get_kernel() is not None:
@@ -854,7 +863,7 @@ def run_script_in_current_client(self, filename, wdir, args, debug,
line += "\"%s\"" % to_text_string(filename)
if args:
line += " %s" % norm(args)
self.execute_code(line, clear_variables)
self.execute_code(line, current_client, clear_variables)
self.visibility_changed(True)
self.raise_()
else:
@@ -871,14 +880,21 @@ def set_current_client_working_directory(self, directory):
directory = encoding.to_unicode_from_fs(directory)
shellwidget.set_cwd(directory)

def execute_code(self, lines, clear_variables=False):
def execute_code(self, lines, current_client=True, clear_variables=False):
"""Execute code instructions."""
sw = self.get_current_shellwidget()
if sw is not None:
if sw._reading:
pass
else:
if clear_variables:
if not current_client:
# Clear console and reset namespace for
# dedicated clients
sw.silent_execute('%clear')
sw.silent_execute(
'get_ipython().kernel.close_all_mpl_figures()')
sw.reset_namespace(force=True)
elif current_client and clear_variables:
sw.reset_namespace(force=True)
sw.execute(to_text_string(to_text_string(lines)))
self.activateWindow()
@@ -891,9 +907,7 @@ def write_to_stdin(self, line):

@Slot()
@Slot(bool)
@Slot(str)
@Slot(bool, str)
def create_new_client(self, give_focus=True, path=''):
def create_new_client(self, give_focus=True):
"""Create a new client"""
self.master_clients += 1
client_id = dict(int_id=to_text_string(self.master_clients),
@@ -935,7 +949,7 @@ def create_new_client(self, give_focus=True, path=''):
"<tt>conda install ipykernel</tt>"))
return

self.connect_client_to_kernel(client, path)
self.connect_client_to_kernel(client)
if client.shellwidget.kernel_manager is None:
return
self.register_client(client)
@@ -951,7 +965,7 @@ def create_client_for_kernel(self):
self._create_client_for_kernel(connection_file, hostname, sshkey,
password)

def connect_client_to_kernel(self, client, path):
def connect_client_to_kernel(self, client):
"""Connect a client to its kernel"""
connection_file = client.connection_file
stderr_file = client.stderr_file
@@ -971,9 +985,6 @@ def connect_client_to_kernel(self, client, path):
shellwidget.kernel_manager = km
shellwidget.kernel_client = kc

if path:
shellwidget.set_cwd(path)

def set_editor(self):
"""Set the editor used by the %edit magic"""
python = sys.executable
@@ -1246,8 +1257,32 @@ def set_spyder_breakpoints(self):

@Slot(str)
def create_client_from_path(self, path):
"""Create a client with its cwd pointing to path"""
self.create_new_client(path=path)
"""Create a client with its cwd pointing to path."""
self.create_new_client()
sw = self.get_current_shellwidget()
sw.set_cwd(path)

def create_client_for_file(self, filename):
"""Create a client to execute code related to a file."""
# Create client
self.create_new_client()

# Don't increase the count of master clients
self.master_clients -= 1

# Rename client tab with filename
client = self.get_current_client()
client.allow_rename = False
self.rename_client_tab(client, filename)

def get_client_for_file(self, filename):
"""Get client associated with a given file."""
client = None
for cl in self.get_clients():
if cl.given_name == filename:
client = cl
break
return client

#------ Public API (for kernels) ------------------------------------------
def ssh_tunnel(self, *args, **kwargs):
@@ -1339,24 +1374,27 @@ def move_tab(self, index_from, index_to):
self.clients.insert(index_to, client)
self.update_plugin_title.emit()

def rename_client_tab(self, client):
def rename_client_tab(self, client, given_name):
"""Rename client's tab"""
index = self.get_client_index_from_id(id(client))

if given_name is not None:
client.given_name = given_name
self.tabwidget.setTabText(index, client.get_name())

def rename_tabs_after_change(self, given_name):
client = self.get_current_client()

# Rename current client tab to add str_id
if client.allow_rename:
client.given_name = given_name
self.rename_client_tab(client)
if client.allow_rename and not u'/' in given_name:
self.rename_client_tab(client, given_name)
else:
self.rename_client_tab(client, None)

# Rename related clients
if client.allow_rename:
if client.allow_rename and not u'/' in given_name:
for cl in self.get_related_clients(client):
cl.given_name = given_name
self.rename_client_tab(cl)
self.rename_client_tab(cl, given_name)

def tab_name_editor(self):
"""Trigger the tab name editor."""
75 changes: 32 additions & 43 deletions spyder/plugins/runconfig.py
Original file line number Diff line number Diff line change
@@ -26,9 +26,9 @@
from spyder.utils import icon_manager as ima


CURRENT_INTERPRETER = _("Execute in current Python or IPython console")
DEDICATED_INTERPRETER = _("Execute in a new dedicated Python console")
SYSTERM_INTERPRETER = _("Execute in an external System terminal")
CURRENT_INTERPRETER = _("Execute in current console")
DEDICATED_INTERPRETER = _("Execute in a dedicated console")
SYSTERM_INTERPRETER = _("Execute in an external system terminal")

CURRENT_INTERPRETER_OPTION = 'default/interpreter/current'
DEDICATED_INTERPRETER_OPTION = 'default/interpreter/dedicated'
@@ -41,11 +41,13 @@
ALWAYS_OPEN_FIRST_RUN = _("Always show %s on a first file run")
ALWAYS_OPEN_FIRST_RUN_OPTION = 'open_on_firstrun'

CLEAR_ALL_VARIABLES = _("Clear all variables before execution (IPython "
"consoles)")
CLEAR_ALL_VARIABLES = _("Clear all variables before execution")
INTERACT = _("Interact with the Python console after execution")


class RunConfiguration(object):
"""Run configuration"""

def __init__(self, fname=None):
self.args = None
self.args_enabled = None
@@ -54,12 +56,13 @@ def __init__(self, fname=None):
self.current = None
self.systerm = None
self.interact = None
self.show_kill_warning =None
self.post_mortem = None
self.python_args = None
self.python_args_enabled = None
self.clear_namespace = None

self.set(CONF.get('run', 'defaultconfiguration', default={}))

if fname is not None and\
CONF.get('run', WDIR_USE_SCRIPT_DIR_OPTION, True):
self.wdir = osp.dirname(fname)
@@ -81,8 +84,6 @@ def set(self, options):
CONF.get('run', SYSTERM_INTERPRETER_OPTION, False))
self.interact = options.get('interact',
CONF.get('run', 'interact', False))
self.show_kill_warning = options.get('show_kill_warning',
CONF.get('run', 'show_kill_warning', False))
self.post_mortem = options.get('post_mortem',
CONF.get('run', 'post_mortem', False))
self.python_args = options.get('python_args', '')
@@ -99,7 +100,6 @@ def get(self):
'current': self.current,
'systerm': self.systerm,
'interact': self.interact,
'show_kill_warning': self.show_kill_warning,
'post_mortem': self.post_mortem,
'python_args/enabled': self.python_args_enabled,
'python_args': self.python_args,
@@ -124,7 +124,7 @@ def get_python_arguments(self):
else:
return ''


def _get_run_configurations():
history_count = CONF.get('run', 'history', 20)
try:
@@ -135,10 +135,12 @@ def _get_run_configurations():
CONF.set('run', 'configurations', [])
return []


def _set_run_configurations(configurations):
history_count = CONF.get('run', 'history', 20)
CONF.set('run', 'configurations', configurations[:history_count])



def get_run_configuration(fname):
"""Return script *fname* run configuration"""
configurations = _get_run_configurations()
@@ -189,9 +191,7 @@ def __init__(self, parent=None):
self.post_mortem_cb = QCheckBox(_("Enter debugging mode when "
"errors appear during execution"))
common_layout.addWidget(self.post_mortem_cb)



# --- Interpreter ---
interpreter_group = QGroupBox(_("Console"))
interpreter_layout = QVBoxLayout()
@@ -202,28 +202,25 @@ def __init__(self, parent=None):
interpreter_layout.addWidget(self.dedicated_radio)
self.systerm_radio = QRadioButton(SYSTERM_INTERPRETER)
interpreter_layout.addWidget(self.systerm_radio)

# --- Dedicated interpreter ---
new_group = QGroupBox(_("Dedicated Python console"))
self.current_radio.toggled.connect(new_group.setDisabled)
new_layout = QGridLayout()
new_group.setLayout(new_layout)
self.interact_cb = QCheckBox(_("Interact with the Python "
"console after execution"))
new_layout.addWidget(self.interact_cb, 1, 0, 1, -1)

self.show_kill_warning_cb = QCheckBox(_("Show warning when killing"
" running process"))

new_layout.addWidget(self.show_kill_warning_cb, 2, 0, 1, -1)
# --- System terminal ---
external_group = QGroupBox(_("External system terminal"))
external_group.setDisabled(True)
self.systerm_radio.toggled.connect(external_group.setEnabled)

external_layout = QGridLayout()
external_group.setLayout(external_layout)
self.interact_cb = QCheckBox(INTERACT)
external_layout.addWidget(self.interact_cb, 1, 0, 1, -1)

self.pclo_cb = QCheckBox(_("Command line options:"))
new_layout.addWidget(self.pclo_cb, 3, 0)
external_layout.addWidget(self.pclo_cb, 3, 0)
self.pclo_edit = QLineEdit()
self.pclo_cb.toggled.connect(self.pclo_edit.setEnabled)
self.pclo_edit.setEnabled(False)
self.pclo_edit.setToolTip(_("<b>-u</b> is added to the "
"other options you set here"))
new_layout.addWidget(self.pclo_edit, 3, 1)
external_layout.addWidget(self.pclo_edit, 3, 1)


# Checkbox to preserve the old behavior, i.e. always open the dialog
@@ -238,7 +235,7 @@ def __init__(self, parent=None):
layout = QVBoxLayout()
layout.addWidget(interpreter_group)
layout.addWidget(common_group)
layout.addWidget(new_group)
layout.addWidget(external_group)
layout.addWidget(hline)
layout.addWidget(self.firstrun_cb)
self.setLayout(layout)
@@ -266,7 +263,6 @@ def set(self, options):
else:
self.dedicated_radio.setChecked(True)
self.interact_cb.setChecked(self.runconf.interact)
self.show_kill_warning_cb.setChecked(self.runconf.show_kill_warning)
self.post_mortem_cb.setChecked(self.runconf.post_mortem)
self.pclo_cb.setChecked(self.runconf.python_args_enabled)
self.pclo_edit.setText(self.runconf.python_args)
@@ -280,7 +276,6 @@ def get(self):
self.runconf.current = self.current_radio.isChecked()
self.runconf.systerm = self.systerm_radio.isChecked()
self.runconf.interact = self.interact_cb.isChecked()
self.runconf.show_kill_warning = self.show_kill_warning_cb.isChecked()
self.runconf.post_mortem = self.post_mortem_cb.isChecked()
self.runconf.python_args_enabled = self.pclo_cb.isChecked()
self.runconf.python_args = to_text_string(self.pclo_edit.text())
@@ -514,18 +509,12 @@ def setup_page(self):
general_layout.addWidget(post_mortem)
general_group.setLayout(general_layout)

dedicated_group = QGroupBox(_("Dedicated Python console"))
interact_after = self.create_checkbox(
_("Interact with the Python console after execution"),
'interact', False)
show_warning = self.create_checkbox(
_("Show warning when killing running processes"),
'show_kill_warning', True)
external_group = QGroupBox(_("External system terminal"))
interact_after = self.create_checkbox(INTERACT, 'interact', False)

dedicated_layout = QVBoxLayout()
dedicated_layout.addWidget(interact_after)
dedicated_layout.addWidget(show_warning)
dedicated_group.setLayout(dedicated_layout)
external_layout = QVBoxLayout()
external_layout.addWidget(interact_after)
external_group.setLayout(external_layout)

firstrun_cb = self.create_checkbox(
ALWAYS_OPEN_FIRST_RUN % _("Run Settings dialog"),
@@ -536,7 +525,7 @@ def setup_page(self):
vlayout.addSpacing(10)
vlayout.addWidget(interpreter_group)
vlayout.addWidget(general_group)
vlayout.addWidget(dedicated_group)
vlayout.addWidget(external_group)
vlayout.addWidget(firstrun_cb)
vlayout.addStretch(1)
self.setLayout(vlayout)
9 changes: 9 additions & 0 deletions spyder/utils/ipython/spyder_kernel.py
Original file line number Diff line number Diff line change
@@ -252,6 +252,15 @@ def get_env(self):
"""Get environment variables."""
return os.environ.copy()

def close_all_mpl_figures(self):
"""Close all Matplotlib figures."""
try:
import matplotlib.pyplot as plt
plt.close('all')
del plt
except:
pass

# -- Private API ---------------------------------------------------
# --- For the Variable Explorer
def _get_current_namespace(self, with_magics=False):
2 changes: 1 addition & 1 deletion spyder/widgets/ipythonconsole/client.py
Original file line number Diff line number Diff line change
@@ -432,7 +432,7 @@ def kernel_restarted_message(self, msg):
stderr = self._read_stderr()
except:
stderr = None
except FileNotFoundError:
except OSError:
stderr = None

if stderr:
28 changes: 21 additions & 7 deletions spyder/widgets/tabs.py
Original file line number Diff line number Diff line change
@@ -35,13 +35,16 @@
class EditTabNamePopup(QLineEdit):
"""Popup on top of the tab to edit its name."""

def __init__(self, parent):
def __init__(self, parent, split_char, split_index):
"""Popup on top of the tab to edit its name."""

# Variables
# Parent (main)
self.main = parent if parent is not None else self.parent()
# Track with tab is being edited
self.split_char = split_char
self.split_index = split_index

# Track which tab is being edited
self.tab_index = None

# Widget setup
@@ -104,7 +107,12 @@ def edit_tab(self, index):
self.move(self.main.mapToGlobal(rect.topLeft()))

# Copies tab name and selects all
self.setText(self.main.tabText(index))
text = self.main.tabText(index)
text = text.replace(u'&', u'')
if self.split_char:
text = text.split(self.split_char)[self.split_index]

self.setText(text)
self.selectAll()

if not self.isVisible():
@@ -128,7 +136,8 @@ class TabBar(QTabBar):
sig_move_tab = Signal((int, int), (str, int, int))
sig_change_name = Signal(str)

def __init__(self, parent, ancestor, rename_tabs=False):
def __init__(self, parent, ancestor, rename_tabs=False, split_char='',
split_index=0):
QTabBar.__init__(self, parent)
self.ancestor = ancestor

@@ -145,7 +154,8 @@ def __init__(self, parent, ancestor, rename_tabs=False):
self.rename_tabs = rename_tabs
if self.rename_tabs:
# Creates tab name editor
self.tab_name_editor = EditTabNamePopup(self)
self.tab_name_editor = EditTabNamePopup(self, split_char,
split_index)
else:
self.tab_name_editor = None

@@ -399,10 +409,14 @@ class Tabs(BaseTabs):

def __init__(self, parent, actions=None, menu=None,
corner_widgets=None, menu_use_tooltips=False,
rename_tabs=False):
rename_tabs=False, split_char='',
split_index=0):
BaseTabs.__init__(self, parent, actions, menu,
corner_widgets, menu_use_tooltips)
tab_bar = TabBar(self, parent, rename_tabs=rename_tabs)
tab_bar = TabBar(self, parent,
rename_tabs=rename_tabs,
split_char=split_char,
split_index=split_index)
tab_bar.sig_move_tab.connect(self.move_tab)
tab_bar.sig_move_tab[(str, int, int)].connect(
self.move_tab_from_another_tabwidget)