diff --git a/spyder/config/main.py b/spyder/config/main.py
index e396d87ec24..f775873f1f1 100755
--- a/spyder/config/main.py
+++ b/spyder/config/main.py
@@ -137,27 +137,6 @@
'umr/verbose': True,
'umr/namelist': [],
}),
- ('console',
- {
- 'max_line_count': 500,
- 'wrap': True,
- 'single_tab': True,
- 'calltips': True,
- 'codecompletion/auto': True,
- 'codecompletion/enter_key': True,
- 'codecompletion/case_sensitive': True,
- 'show_elapsed_time': False,
- 'show_icontext': False,
- 'monitor/enabled': True,
- 'qt/api': 'default',
- 'matplotlib/backend/value': 0,
- 'light_background': True,
- 'merge_output_channels': os.name != 'nt',
- 'colorize_sys_stderr': os.name != 'nt',
- 'pythonstartup/default': True,
- 'pythonstartup/custom': False,
- 'ets_backend': 'qt4'
- }),
('ipython_console',
{
'show_banner': True,
@@ -659,7 +638,7 @@
# or if you want to *rename* options, then you need to do a MAJOR update in
# version, e.g. from 3.0.0 to 4.0.0
# 3. You don't need to touch this value if you're just adding a new option
-CONF_VERSION = '37.2.0'
+CONF_VERSION = '38.0.0'
# Main configuration instance
try:
diff --git a/spyder/plugins/externalconsole.py b/spyder/plugins/externalconsole.py
deleted file mode 100644
index f16a2bb3e6d..00000000000
--- a/spyder/plugins/externalconsole.py
+++ /dev/null
@@ -1,949 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © Spyder Project Contributors
-# Licensed under the terms of the MIT License
-# (see spyder/__init__.py for details)
-
-"""External Console plugin"""
-
-# pylint: disable=C0103
-# pylint: disable=R0903
-# pylint: disable=R0911
-# pylint: disable=R0201
-
-# Standard library imports
-import os
-import os.path as osp
-import sys
-
-# Third party imports
-from qtpy.compat import getopenfilename
-from qtpy.QtCore import Qt, Signal, Slot
-from qtpy.QtWidgets import (QButtonGroup, QGroupBox, QHBoxLayout, QLabel,
- QMessageBox, QTabWidget, QVBoxLayout, QWidget)
-
-# Local imports
-from spyder import dependencies
-from spyder.config.base import _, SCIENTIFIC_STARTUP
-from spyder.config.main import CONF
-from spyder.utils import encoding, programs, sourcecode
-from spyder.utils import icon_manager as ima
-from spyder.utils.misc import (get_error_match, get_python_executable,
- is_python_script, remove_backslashes)
-from spyder.utils.qthelpers import create_action, mimedata2url
-from spyder.api.plugins import SpyderPluginWidget
-from spyder.api.preferences import PluginConfigPage
-from spyder.plugins.runconfig import get_run_configuration
-from spyder.py3compat import to_text_string, is_text_string, getcwd
-from spyder.widgets.externalshell.pythonshell import ExternalPythonShell
-from spyder.widgets.externalshell.systemshell import ExternalSystemShell
-from spyder.widgets.findreplace import FindReplace
-from spyder.widgets.tabs import Tabs
-
-
-MPL_REQVER = '>=1.0'
-dependencies.add("matplotlib", _("Interactive data plotting in the consoles"),
- required_version=MPL_REQVER, optional=True)
-
-
-class ExternalConsoleConfigPage(PluginConfigPage):
-
- def __init__(self, plugin, parent):
- PluginConfigPage.__init__(self, plugin, parent)
- self.get_name = lambda: _("Python console")
-
- def setup_page(self):
- interface_group = QGroupBox(_("Interface"))
- newcb = self.create_checkbox
- singletab_box = newcb(_("One tab per script"), 'single_tab')
- showtime_box = newcb(_("Show elapsed time"), 'show_elapsed_time')
- icontext_box = newcb(_("Show icons and text"), 'show_icontext')
-
- # Interface Group
- interface_layout = QVBoxLayout()
- interface_layout.addWidget(singletab_box)
- interface_layout.addWidget(showtime_box)
- interface_layout.addWidget(icontext_box)
- interface_group.setLayout(interface_layout)
-
- # Source Code Group
- display_group = QGroupBox(_("Source code"))
- buffer_spin = self.create_spinbox(
- _("Buffer: "), _(" lines"),
- 'max_line_count', min_=0, max_=1000000, step=100,
- tip=_("Set maximum line count"))
- wrap_mode_box = newcb(_("Wrap lines"), 'wrap')
- merge_channels_box = newcb(
- _("Merge process standard output/error channels"),
- 'merge_output_channels',
- tip=_("Merging the output channels of the process means that\n"
- "the standard error won't be written in red anymore,\n"
- "but this has the effect of speeding up display."))
- colorize_sys_stderr_box = newcb(
- _("Colorize standard error channel using ANSI escape codes"),
- 'colorize_sys_stderr',
- tip=_("This method is the only way to have colorized standard\n"
- "error channel when the output channels have been "
- "merged."))
- merge_channels_box.toggled.connect(colorize_sys_stderr_box.setEnabled)
- merge_channels_box.toggled.connect(colorize_sys_stderr_box.setChecked)
- colorize_sys_stderr_box.setEnabled(
- self.get_option('merge_output_channels'))
-
- display_layout = QVBoxLayout()
- display_layout.addWidget(buffer_spin)
- display_layout.addWidget(wrap_mode_box)
- display_layout.addWidget(merge_channels_box)
- display_layout.addWidget(colorize_sys_stderr_box)
- display_group.setLayout(display_layout)
-
- # Background Color Group
- bg_group = QGroupBox(_("Background color"))
- bg_label = QLabel(_("This option will be applied the next time "
- "a Python console or a terminal is opened."))
- bg_label.setWordWrap(True)
- lightbg_box = newcb(_("Light background (white color)"),
- 'light_background')
- bg_layout = QVBoxLayout()
- bg_layout.addWidget(bg_label)
- bg_layout.addWidget(lightbg_box)
- bg_group.setLayout(bg_layout)
-
- # Advanced settings
- source_group = QGroupBox(_("Source code"))
- completion_box = newcb(_("Automatic code completion"),
- 'codecompletion/auto')
- case_comp_box = newcb(_("Case sensitive code completion"),
- 'codecompletion/case_sensitive')
- comp_enter_box = newcb(_("Enter key selects completion"),
- 'codecompletion/enter_key')
- calltips_box = newcb(_("Display balloon tips"), 'calltips')
-
- source_layout = QVBoxLayout()
- source_layout.addWidget(completion_box)
- source_layout.addWidget(case_comp_box)
- source_layout.addWidget(comp_enter_box)
- source_layout.addWidget(calltips_box)
- source_group.setLayout(source_layout)
-
- # PYTHONSTARTUP replacement
- pystartup_group = QGroupBox(_("PYTHONSTARTUP replacement"))
- pystartup_bg = QButtonGroup(pystartup_group)
- pystartup_label = QLabel(_("This option will override the "
- "PYTHONSTARTUP environment variable which\n"
- "defines the script to be executed during "
- "the Python console startup."))
- def_startup_radio = self.create_radiobutton(
- _("Default PYTHONSTARTUP script"),
- 'pythonstartup/default',
- button_group=pystartup_bg)
- cus_startup_radio = self.create_radiobutton(
- _("Use the following startup script:"),
- 'pythonstartup/custom',
- button_group=pystartup_bg)
- pystartup_file = self.create_browsefile('', 'pythonstartup', '',
- filters=_("Python scripts")+\
- " (*.py)")
- def_startup_radio.toggled.connect(pystartup_file.setDisabled)
- cus_startup_radio.toggled.connect(pystartup_file.setEnabled)
-
- pystartup_layout = QVBoxLayout()
- pystartup_layout.addWidget(pystartup_label)
- pystartup_layout.addWidget(def_startup_radio)
- pystartup_layout.addWidget(cus_startup_radio)
- pystartup_layout.addWidget(pystartup_file)
- pystartup_group.setLayout(pystartup_layout)
-
- # Monitor Group
- monitor_group = QGroupBox(_("Monitor"))
- monitor_label = QLabel(_("The monitor provides introspection "
- "features to console: code completion, "
- "calltips and variable explorer. "
- "Because it relies on several modules, "
- "disabling the monitor may be useful "
- "to accelerate console startup."))
- monitor_label.setWordWrap(True)
- monitor_box = newcb(_("Enable monitor"), 'monitor/enabled')
- for obj in (completion_box, case_comp_box, comp_enter_box,
- calltips_box):
- monitor_box.toggled.connect(obj.setEnabled)
- obj.setEnabled(self.get_option('monitor/enabled'))
-
- monitor_layout = QVBoxLayout()
- monitor_layout.addWidget(monitor_label)
- monitor_layout.addWidget(monitor_box)
- monitor_group.setLayout(monitor_layout)
-
- # Qt Group
- opts = [
- (_("Default library"), 'default'),
- ('PyQt5', 'pyqt5'),
- ('PyQt4', 'pyqt'),
- ('PySide', 'pyside'),
- ]
- qt_group = QGroupBox(_("Qt-Python Bindings"))
- qt_setapi_box = self.create_combobox(
- _("Library:") + " ", opts,
- 'qt/api', default='default',
- tip=_("This option will act on
"
- "libraries such as Matplotlib, guidata "
- "or ETS"))
-
- qt_layout = QVBoxLayout()
- qt_layout.addWidget(qt_setapi_box)
- qt_group.setLayout(qt_layout)
-
- # Matplotlib Group
- mpl_group = QGroupBox(_("Graphics"))
- mpl_label = QLabel(_("Decide which backend to use to display graphics. "
- "If unsure, please select the Automatic "
- "backend.
"
- "Note: We support a very limited number "
- "of backends in our Python consoles. If you "
- "prefer to work with a different one, please use "
- "an IPython console."))
- mpl_label.setWordWrap(True)
-
- backends = [(_("Automatic"), 0), (_("None"), 1)]
- if not os.name == 'nt' and programs.is_module_installed('_tkinter'):
- backends.append( ("Tkinter", 2) )
- backends = tuple(backends)
-
- mpl_backend_box = self.create_combobox( _("Backend:")+" ", backends,
- 'matplotlib/backend/value',
- tip=_("This option will be applied the "
- "next time a console is opened."))
-
- mpl_installed = programs.is_module_installed('matplotlib')
- mpl_layout = QVBoxLayout()
- mpl_layout.addWidget(mpl_label)
- mpl_layout.addWidget(mpl_backend_box)
- mpl_group.setLayout(mpl_layout)
- mpl_group.setEnabled(mpl_installed)
-
- # ETS Group
- ets_group = QGroupBox(_("Enthought Tool Suite"))
- ets_label = QLabel(_("Enthought Tool Suite (ETS) supports "
- "PyQt4 (qt4) and wxPython (wx) graphical "
- "user interfaces."))
- ets_label.setWordWrap(True)
- ets_edit = self.create_lineedit(_("ETS_TOOLKIT:"), 'ets_backend',
- alignment=Qt.Horizontal)
-
- ets_layout = QVBoxLayout()
- ets_layout.addWidget(ets_label)
- ets_layout.addWidget(ets_edit)
- ets_group.setLayout(ets_layout)
-
- if CONF.get('main_interpreter','default'):
- interpreter = get_python_executable()
- else:
- interpreter = CONF.get('main_interpreter', 'executable')
- ets_group.setEnabled(programs.is_module_installed(
- "enthought.etsconfig.api",
- interpreter=interpreter))
-
- tabs = QTabWidget()
- tabs.addTab(self.create_tab(interface_group, display_group,
- bg_group),
- _("Display"))
- tabs.addTab(self.create_tab(monitor_group, source_group),
- _("Introspection"))
- tabs.addTab(self.create_tab(pystartup_group), _("Advanced settings"))
- tabs.addTab(self.create_tab(qt_group, mpl_group, ets_group),
- _("External modules"))
-
- vlayout = QVBoxLayout()
- vlayout.addWidget(tabs)
- self.setLayout(vlayout)
-
-
-class ExternalConsole(SpyderPluginWidget):
- """
- Console widget
- """
- CONF_SECTION = 'console'
- CONFIGWIDGET_CLASS = ExternalConsoleConfigPage
- DISABLE_ACTIONS_WHEN_HIDDEN = False
-
- edit_goto = Signal((str, int, str), (str, int, str, bool))
- focus_changed = Signal()
- redirect_stdio = Signal(bool)
-
- def __init__(self, parent):
- SpyderPluginWidget.__init__(self, parent)
-
- self.tabwidget = None
- self.menu_actions = None
-
- self.historylog = None # History log plugin
-
- self.python_count = 0
- self.terminal_count = 0
-
- # Python startup file selection
- if not osp.isfile(self.get_option('pythonstartup', '')):
- self.set_option('pythonstartup', SCIENTIFIC_STARTUP)
- # default/custom settings are mutually exclusive:
- self.set_option('pythonstartup/custom',
- not self.get_option('pythonstartup/default'))
-
- self.shellwidgets = []
- self.filenames = []
- self.icons = []
- self.runfile_args = ""
-
- # Initialize plugin
- self.initialize_plugin()
-
- layout = QVBoxLayout()
- self.tabwidget = Tabs(self, self.menu_actions)
- if hasattr(self.tabwidget, 'setDocumentMode')\
- and not sys.platform == 'darwin':
- # Don't set document mode to true on OSX because it generates
- # a crash when the console is detached from the main window
- # Fixes Issue 561
- self.tabwidget.setDocumentMode(True)
- self.tabwidget.currentChanged.connect(self.refresh_plugin)
- self.tabwidget.move_data.connect(self.move_tab)
- self.main.sig_pythonpath_changed.connect(self.set_path)
-
- self.tabwidget.set_close_function(self.close_console)
-
- if sys.platform == 'darwin':
- tab_container = QWidget()
- tab_container.setObjectName('tab-container')
- tab_layout = QHBoxLayout(tab_container)
- tab_layout.setContentsMargins(0, 0, 0, 0)
- tab_layout.addWidget(self.tabwidget)
- layout.addWidget(tab_container)
- else:
- layout.addWidget(self.tabwidget)
-
- # Find/replace widget
- self.find_widget = FindReplace(self)
- self.find_widget.hide()
- self.register_widget_shortcuts(self.find_widget)
-
- layout.addWidget(self.find_widget)
-
- self.setLayout(layout)
-
- # Accepting drops
- self.setAcceptDrops(True)
-
- def move_tab(self, index_from, index_to):
- """
- Move tab (tabs themselves have already been moved by the tabwidget)
- """
- filename = self.filenames.pop(index_from)
- shell = self.shellwidgets.pop(index_from)
- icons = self.icons.pop(index_from)
-
- self.filenames.insert(index_to, filename)
- self.shellwidgets.insert(index_to, shell)
- self.icons.insert(index_to, icons)
- self.sig_update_plugin_title.emit()
-
- def get_shell_index_from_id(self, shell_id):
- """Return shellwidget index from id"""
- for index, shell in enumerate(self.shellwidgets):
- if id(shell) == shell_id:
- return index
-
- def close_console(self, index=None):
- """Close console tab from index or widget (or close current tab)"""
- # Get tab index
- if not self.tabwidget.count():
- return
- if index is None:
- index = self.tabwidget.currentIndex()
-
- # Closing logic
- self.tabwidget.widget(index).close()
- self.tabwidget.removeTab(index)
- self.filenames.pop(index)
- self.shellwidgets.pop(index)
- self.icons.pop(index)
- self.sig_update_plugin_title.emit()
- self.update_tabs_text()
-
- def set_path(self):
- """Set consoles PYTHONPATH if changed by the user"""
- from spyder.widgets.externalshell import pythonshell
- for sw in self.shellwidgets:
- if isinstance(sw, pythonshell.ExternalPythonShell):
- if sw.is_interpreter and sw.is_running():
- sw.path = self.main.get_spyder_pythonpath()
- sw.shell.path = sw.path
-
- def __find_python_shell(self, interpreter_only=False):
- current_index = self.tabwidget.currentIndex()
- if current_index == -1:
- return
- from spyder.widgets.externalshell import pythonshell
- for index in [current_index]+list(range(self.tabwidget.count())):
- shellwidget = self.tabwidget.widget(index)
- if isinstance(shellwidget, pythonshell.ExternalPythonShell):
- if interpreter_only and not shellwidget.is_interpreter:
- continue
- elif not shellwidget.is_running():
- continue
- else:
- self.tabwidget.setCurrentIndex(index)
- return shellwidget
-
- def get_current_shell(self):
- """
- Called by Help to retrieve the current shell instance
- """
- shellwidget = self.__find_python_shell()
- return shellwidget.shell
-
- def get_running_python_shell(self):
- """
- Called by Help to retrieve a running Python shell instance
- """
- current_index = self.tabwidget.currentIndex()
- if current_index == -1:
- return
- from spyder.widgets.externalshell import pythonshell
- shellwidgets = [self.tabwidget.widget(index)
- for index in range(self.tabwidget.count())]
- shellwidgets = [_w for _w in shellwidgets
- if isinstance(_w, pythonshell.ExternalPythonShell) \
- and _w.is_running()]
- if shellwidgets:
- # First, iterate on interpreters only:
- for shellwidget in shellwidgets:
- if shellwidget.is_interpreter:
- return shellwidget.shell
- else:
- return shellwidgets[0].shell
-
- def run_script_in_current_shell(self, filename, wdir, args, debug,
- post_mortem):
- """Run script in current shell, if any"""
- norm = lambda text: remove_backslashes(to_text_string(text))
- line = "%s('%s'" % ('debugfile' if debug else 'runfile',
- norm(filename))
- if args:
- line += ", args='%s'" % norm(args)
- if wdir:
- line += ", wdir='%s'" % norm(wdir)
- if post_mortem:
- line += ', post_mortem=True'
- line += ")"
- if not self.execute_code(line, interpreter_only=True):
- QMessageBox.warning(self, _('Warning'),
- _("No Python console is currently selected to run %s."
- "
Please select or open a new Python console "
- "and try again."
- ) % osp.basename(norm(filename)), QMessageBox.Ok)
- else:
- self.visibility_changed(True)
- self.raise_()
-
- def set_current_shell_working_directory(self, directory):
- """Set current shell working directory"""
- shellwidget = self.__find_python_shell()
- if shellwidget is not None:
- directory = encoding.to_unicode_from_fs(directory)
- shellwidget.shell.set_cwd(directory)
-
- def execute_code(self, lines, interpreter_only=False):
- """Execute code in an already opened Python interpreter"""
- shellwidget = self.__find_python_shell(
- interpreter_only=interpreter_only)
- if shellwidget is not None:
- shellwidget.shell.execute_lines(to_text_string(lines))
- self.activateWindow()
- shellwidget.shell.setFocus()
- return True
- else:
- return False
-
- def pdb_has_stopped(self, fname, lineno, shellwidget):
- """Python debugger has just stopped at frame (fname, lineno)"""
- # This is a unique form of the edit_goto signal that is intended to
- # prevent keyboard input from accidentally entering the editor
- # during repeated, rapid entry of debugging commands.
- self.edit_goto[str, int, str, bool].emit(fname, lineno, '', False)
- self.activateWindow()
- shellwidget.shell.setFocus()
-
- def set_spyder_breakpoints(self):
- """Set all Spyder breakpoints into all shells"""
- for shellwidget in self.shellwidgets:
- shellwidget.shell.set_spyder_breakpoints()
-
- def start(self, fname, wdir=None, args='', interact=False, debug=False,
- python=True, python_args='', post_mortem=True):
- """
- Start new console
-
- fname:
- string: filename of script to run
- None: open an interpreter
- wdir: working directory
- args: command line options of the Python script
- interact: inspect script interactively after its execution
- debug: run pdb
- python: True: Python interpreter, False: terminal
- python_args: additionnal Python interpreter command line options
- (option "-u" is mandatory, see widgets.externalshell package)
- """
- # Note: fname is None <=> Python interpreter
- if fname is not None and not is_text_string(fname):
- fname = to_text_string(fname)
- if wdir is not None and not is_text_string(wdir):
- wdir = to_text_string(wdir)
-
- if fname is not None and fname in self.filenames:
- index = self.filenames.index(fname)
- if self.get_option('single_tab'):
- old_shell = self.shellwidgets[index]
- if old_shell.is_running():
- runconfig = get_run_configuration(fname)
- if runconfig is None or runconfig.show_kill_warning:
- if PYQT5:
- answer = QMessageBox.question(self, self.get_plugin_title(),
- _("%s is already running in a separate process.\n"
- "Do you want to kill the process before starting "
- "a new one?") % osp.basename(fname),
- QMessageBox.Yes | QMessageBox.Cancel)
- else:
- mb = QMessageBox(self)
- answer = mb.question(mb, self.get_plugin_title(),
- _("%s is already running in a separate process.\n"
- "Do you want to kill the process before starting "
- "a new one?") % osp.basename(fname),
- QMessageBox.Yes | QMessageBox.Cancel)
- else:
- answer = QMessageBox.Yes
-
- if answer == QMessageBox.Yes:
- old_shell.process.kill()
- old_shell.process.waitForFinished()
- else:
- return
- self.close_console(index)
- else:
- index = self.tabwidget.count()
-
- # Creating a new external shell
- pythonpath = self.main.get_spyder_pythonpath()
- light_background = self.get_option('light_background')
- show_elapsed_time = self.get_option('show_elapsed_time')
- if python:
- if CONF.get('main_interpreter', 'default'):
- pythonexecutable = get_python_executable()
- external_interpreter = False
- else:
- pythonexecutable = CONF.get('main_interpreter', 'executable')
- external_interpreter = True
- if self.get_option('pythonstartup/default'):
- pythonstartup = None
- else:
- pythonstartup = self.get_option('pythonstartup', None)
- monitor_enabled = self.get_option('monitor/enabled')
- mpl_backend = self.get_option('matplotlib/backend/value')
- ets_backend = self.get_option('ets_backend')
- qt_api = self.get_option('qt/api')
- if qt_api not in ('pyqt', 'pyside', 'pyqt5'):
- qt_api = None
- merge_output_channels = self.get_option('merge_output_channels')
- colorize_sys_stderr = self.get_option('colorize_sys_stderr')
- umr_enabled = CONF.get('main_interpreter', 'umr/enabled')
- umr_namelist = CONF.get('main_interpreter', 'umr/namelist')
- umr_verbose = CONF.get('main_interpreter', 'umr/verbose')
-
- sa_settings = None
- shellwidget = ExternalPythonShell(self, fname, wdir,
- interact, debug, post_mortem=post_mortem,
- path=pythonpath,
- python_args=python_args,
- arguments=args, stand_alone=sa_settings,
- pythonstartup=pythonstartup,
- pythonexecutable=pythonexecutable,
- external_interpreter=external_interpreter,
- umr_enabled=umr_enabled, umr_namelist=umr_namelist,
- umr_verbose=umr_verbose, ets_backend=ets_backend,
- monitor_enabled=monitor_enabled,
- mpl_backend=mpl_backend, qt_api=qt_api,
- merge_output_channels=merge_output_channels,
- colorize_sys_stderr=colorize_sys_stderr,
- light_background=light_background,
- menu_actions=self.menu_actions,
- show_buttons_inside=False,
- show_elapsed_time=show_elapsed_time)
- shellwidget.sig_pdb.connect(
- lambda fname, lineno, shellwidget=shellwidget:
- self.pdb_has_stopped(fname, lineno, shellwidget))
- self.register_widget_shortcuts(shellwidget.shell)
- else:
- if os.name == 'posix':
- cmd = 'gnome-terminal'
- args = []
- if programs.is_program_installed(cmd):
- if wdir:
- args.extend(['--working-directory=%s' % wdir])
- programs.run_program(cmd, args)
- return
- cmd = 'konsole'
- if programs.is_program_installed(cmd):
- if wdir:
- args.extend(['--workdir', wdir])
- programs.run_program(cmd, args)
- return
- shellwidget = ExternalSystemShell(self, wdir, path=pythonpath,
- light_background=light_background,
- menu_actions=self.menu_actions,
- show_buttons_inside=False,
- show_elapsed_time=show_elapsed_time)
-
- # Code completion / calltips
- shellwidget.shell.setMaximumBlockCount(
- self.get_option('max_line_count') )
- shellwidget.shell.set_font( self.get_plugin_font() )
- shellwidget.shell.toggle_wrap_mode( self.get_option('wrap') )
- shellwidget.shell.set_calltips( self.get_option('calltips') )
- shellwidget.shell.set_codecompletion_auto(
- self.get_option('codecompletion/auto') )
- shellwidget.shell.set_codecompletion_case(
- self.get_option('codecompletion/case_sensitive') )
- shellwidget.shell.set_codecompletion_enter(
- self.get_option('codecompletion/enter_key') )
- if self.historylog is not None:
- self.historylog.add_history(shellwidget.shell.history_filename)
- shellwidget.shell.append_to_history.connect(
- self.historylog.append_to_history)
- shellwidget.shell.go_to_error.connect(self.go_to_error)
- shellwidget.shell.focus_changed.connect(
- lambda: self.focus_changed.emit())
- if python:
- if self.main.editor is not None:
- shellwidget.open_file.connect(self.open_file_in_spyder)
- if fname is None:
- self.python_count += 1
- tab_name = "Python %d" % self.python_count
- tab_icon1 = ima.icon('python')
- tab_icon2 = ima.icon('python_t')
- self.filenames.insert(index, fname)
- else:
- self.filenames.insert(index, fname)
- tab_name = self.get_tab_text(fname)
- self.update_tabs_text()
- tab_icon1 = ima.icon('run')
- tab_icon2 = ima.icon('terminated')
- else:
- fname = id(shellwidget)
- if os.name == 'nt':
- tab_name = _("Command Window")
- else:
- tab_name = _("Terminal")
- self.terminal_count += 1
- tab_name += (" %d" % self.terminal_count)
- tab_icon1 = ima.icon('cmdprompt')
- tab_icon2 = ima.icon('cmdprompt_t')
- self.filenames.insert(index, fname)
- self.shellwidgets.insert(index, shellwidget)
- self.icons.insert(index, (tab_icon1, tab_icon2))
- if index is None:
- index = self.tabwidget.addTab(shellwidget, tab_name)
- else:
- self.tabwidget.insertTab(index, shellwidget, tab_name)
-
- shellwidget.started.connect(
- lambda sid=id(shellwidget): self.process_started(sid))
- shellwidget.sig_finished.connect(
- lambda sid=id(shellwidget): self.process_finished(sid))
- self.find_widget.set_editor(shellwidget.shell)
- self.tabwidget.setTabToolTip(index, fname if wdir is None else wdir)
- self.tabwidget.setCurrentIndex(index)
- if self.dockwidget and not self.ismaximized:
- self.dockwidget.setVisible(True)
- self.dockwidget.raise_()
-
- shellwidget.set_icontext_visible(self.get_option('show_icontext'))
-
- # Start process and give focus to console
- shellwidget.start_shell()
-
- def open_file_in_spyder(self, fname, lineno):
- """Open file in Spyder's editor from remote process"""
- self.main.editor.activateWindow()
- self.main.editor.raise_()
- self.main.editor.load(fname, lineno)
-
- def get_tab_text(self, fname):
- """Get tab text without ambiguation."""
- files_path_list = [filename for filename in self.filenames
- if filename is not None]
- return sourcecode.get_file_title(files_path_list, fname)
-
- def update_tabs_text(self):
- """Update the text from the tabs."""
- for index, fname in enumerate(self.filenames):
- if fname is not None:
- self.tabwidget.setTabText(index, self.get_tab_text(fname))
-
- #------ Private API -------------------------------------------------------
- def process_started(self, shell_id):
- index = self.get_shell_index_from_id(shell_id)
- shell = self.shellwidgets[index]
- icon, _icon = self.icons[index]
- self.tabwidget.setTabIcon(index, icon)
-
- def process_finished(self, shell_id):
- index = self.get_shell_index_from_id(shell_id)
- if index is not None:
- # Not sure why it happens, but sometimes the shellwidget has
- # already been removed, so that's not bad if we can't change
- # the tab icon...
- _icon, icon = self.icons[index]
- self.tabwidget.setTabIcon(index, icon)
-
- #------ SpyderPluginWidget API --------------------------------------------
- def get_plugin_title(self):
- """Return widget title"""
- title = _('Python console')
- return title
-
- def get_plugin_icon(self):
- """Return widget icon."""
- return ima.icon('console')
-
- def get_focus_widget(self):
- """Return the widget to give focus to."""
- return self.tabwidget.currentWidget()
-
- def get_plugin_actions(self):
- """Return a list of actions related to plugin"""
- interpreter_action = create_action(self,
- _("Open a &Python console"), None,
- ima.icon('python'),
- triggered=self.open_interpreter)
- if os.name == 'nt':
- text = _("Open &command prompt")
- tip = _("Open a Windows command prompt")
- else:
- text = _("Open a &terminal")
- tip = _("Open a terminal window")
- terminal_action = create_action(self, text, None, None, tip,
- triggered=self.open_terminal)
- run_action = create_action(self,
- _("&Run..."), None,
- ima.icon('run_small'), _("Run a Python script"),
- triggered=self.run_script)
-
- consoles_menu_actions = [interpreter_action]
- tools_menu_actions = [terminal_action]
- self.menu_actions = [interpreter_action, terminal_action, run_action]
-
- self.main.consoles_menu_actions += consoles_menu_actions
- self.main.tools_menu_actions += tools_menu_actions
-
- return self.menu_actions+consoles_menu_actions+tools_menu_actions
-
- def register_plugin(self):
- """Register plugin in Spyder's main window"""
- self.main.add_dockwidget(self)
- self.historylog = self.main.historylog
- self.edit_goto.connect(self.main.editor.load)
- self.edit_goto[str, int, str, bool].connect(
- lambda fname, lineno, word, processevents:
- self.main.editor.load(fname, lineno, word,
- processevents=processevents))
- self.main.editor.run_in_current_extconsole.connect(
- self.run_script_in_current_shell)
- self.main.editor.breakpoints_saved.connect(
- self.set_spyder_breakpoints)
- self.main.editor.open_dir.connect(
- self.set_current_shell_working_directory)
- self.main.workingdirectory.set_current_console_wd.connect(
- self.set_current_shell_working_directory)
- self.focus_changed.connect(
- self.main.plugin_focus_changed)
- self.redirect_stdio.connect(
- self.main.redirect_internalshell_stdio)
-
- def closing_plugin(self, cancelable=False):
- """Perform actions before parent main window is closed."""
- for shellwidget in self.shellwidgets:
- shellwidget.close()
- return True
-
- def restart(self):
- """
- Restart the console.
-
- This is needed when we switch project to update PYTHONPATH
- and the selected interpreter
- """
- self.python_count = 0
- for i in range(len(self.shellwidgets)):
- self.close_console()
- self.open_interpreter()
-
- def refresh_plugin(self):
- """Refresh tabwidget"""
- shellwidget = None
- if self.tabwidget.count():
- shellwidget = self.tabwidget.currentWidget()
- editor = shellwidget.shell
- editor.setFocus()
- widgets = [shellwidget.create_time_label(), 5
- ]+shellwidget.get_toolbar_buttons()+[5]
- else:
- editor = None
- widgets = []
- self.find_widget.set_editor(editor)
- self.tabwidget.set_corner_widgets({Qt.TopRightCorner: widgets})
- if shellwidget:
- shellwidget.update_time_label_visibility()
- self.main.last_console_plugin_focus_was_python = True
- self.sig_update_plugin_title.emit()
-
- def update_font(self):
- """Update font from Preferences"""
- font = self.get_plugin_font()
- for shellwidget in self.shellwidgets:
- shellwidget.shell.set_font(font)
- completion_size = CONF.get('main', 'completion/size')
- comp_widget = shellwidget.shell.completion_widget
- comp_widget.setup_appearance(completion_size, font)
-
- def apply_plugin_settings(self, options):
- """Apply configuration file's plugin settings"""
- showtime_n = 'show_elapsed_time'
- showtime_o = self.get_option(showtime_n)
- icontext_n = 'show_icontext'
- icontext_o = self.get_option(icontext_n)
- calltips_n = 'calltips'
- calltips_o = self.get_option(calltips_n)
- wrap_n = 'wrap'
- wrap_o = self.get_option(wrap_n)
- compauto_n = 'codecompletion/auto'
- compauto_o = self.get_option(compauto_n)
- case_comp_n = 'codecompletion/case_sensitive'
- case_comp_o = self.get_option(case_comp_n)
- compenter_n = 'codecompletion/enter_key'
- compenter_o = self.get_option(compenter_n)
- mlc_n = 'max_line_count'
- mlc_o = self.get_option(mlc_n)
- for shellwidget in self.shellwidgets:
- if showtime_n in options:
- shellwidget.set_elapsed_time_visible(showtime_o)
- if icontext_n in options:
- shellwidget.set_icontext_visible(icontext_o)
- if calltips_n in options:
- shellwidget.shell.set_calltips(calltips_o)
- if wrap_n in options:
- shellwidget.shell.toggle_wrap_mode(wrap_o)
- if compauto_n in options:
- shellwidget.shell.set_codecompletion_auto(compauto_o)
- if case_comp_n in options:
- shellwidget.shell.set_codecompletion_case(case_comp_o)
- if compenter_n in options:
- shellwidget.shell.set_codecompletion_enter(compenter_o)
- if mlc_n in options:
- shellwidget.shell.setMaximumBlockCount(mlc_o)
-
- #------ SpyderPluginMixin API ---------------------------------------------
- def toggle_view(self, checked):
- """Toggle view"""
- if checked:
- self.dockwidget.show()
- self.dockwidget.raise_()
- # Start a console in case there are none shown
- from spyder.widgets.externalshell import pythonshell
- consoles = None
- for sw in self.shellwidgets:
- if isinstance(sw, pythonshell.ExternalPythonShell):
- consoles = True
- break
- if not consoles:
- self.open_interpreter()
- else:
- self.dockwidget.hide()
-
- #------ Public API ---------------------------------------------------------
- @Slot(bool)
- @Slot(str)
- def open_interpreter(self, wdir=None):
- """Open interpreter"""
- if not wdir:
- wdir = getcwd()
- self.visibility_changed(True)
- self.start(fname=None, wdir=to_text_string(wdir), args='',
- interact=True, debug=False, python=True)
-
- @Slot(bool)
- @Slot(str)
- def open_terminal(self, wdir=None):
- """Open terminal"""
- if not wdir:
- wdir = getcwd()
- self.start(fname=None, wdir=to_text_string(wdir), args='',
- interact=True, debug=False, python=False)
-
- @Slot()
- def run_script(self):
- """Run a Python script"""
- self.redirect_stdio.emit(False)
- filename, _selfilter = getopenfilename(self, _("Run Python script"),
- getcwd(), _("Python scripts")+" (*.py ; *.pyw ; *.ipy)")
- self.redirect_stdio.emit(True)
- if filename:
- self.start(fname=filename, wdir=None, args='',
- interact=False, debug=False)
-
- def go_to_error(self, text):
- """Go to error if relevant"""
- match = get_error_match(to_text_string(text))
- if match:
- fname, lnb = match.groups()
- self.edit_goto.emit(osp.abspath(fname), int(lnb), '')
-
- #----Drag and drop
- def dragEnterEvent(self, event):
- """Reimplement Qt method
- Inform Qt about the types of data that the widget accepts"""
- source = event.mimeData()
- if source.hasUrls():
- if mimedata2url(source):
- pathlist = mimedata2url(source)
- shellwidget = self.tabwidget.currentWidget()
- if all([is_python_script(to_text_string(qstr))
- for qstr in pathlist]):
- event.acceptProposedAction()
- elif shellwidget is None or not shellwidget.is_running():
- event.ignore()
- else:
- event.acceptProposedAction()
- else:
- event.ignore()
- elif source.hasText():
- event.acceptProposedAction()
-
- def dropEvent(self, event):
- """Reimplement Qt method
- Unpack dropped data and handle it"""
- source = event.mimeData()
- shellwidget = self.tabwidget.currentWidget()
- if source.hasText():
- qstr = source.text()
- if is_python_script(to_text_string(qstr)):
- self.start(qstr)
- elif shellwidget:
- shellwidget.shell.insert_text(qstr)
- elif source.hasUrls():
- pathlist = mimedata2url(source)
- if all([is_python_script(to_text_string(qstr))
- for qstr in pathlist]):
- for fname in pathlist:
- self.start(fname)
- elif shellwidget:
- shellwidget.shell.drop_pathlist(pathlist)
- event.acceptProposedAction()
diff --git a/spyder/utils/inputhooks.py b/spyder/utils/inputhooks.py
deleted file mode 100644
index c2fddc3b2ac..00000000000
--- a/spyder/utils/inputhooks.py
+++ /dev/null
@@ -1,158 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-Inputhook management for GUI event loop integration
-
-Copyright (C) The IPython Development Team
-Distributed under the terms of the modified BSD license
-"""
-
-# Stdlib imports
-import ctypes
-import os
-import sys
-
-QT_API = os.environ["QT_API"]
-
-# Qt imports
-if QT_API == 'pyqt5':
- from PyQt5 import QtCore
-elif QT_API == 'pyqt':
- from PyQt4 import QtCore
-elif QT_API == 'pyside':
- from PySide import QtCore
-
-
-#-----------------------------------------------------------------------------
-# Utilities
-#-----------------------------------------------------------------------------
-def _stdin_ready_posix():
- """Return True if there's something to read on stdin (posix version)."""
- infds, outfds, erfds = select.select([sys.stdin],[],[],0)
- return bool(infds)
-
-def _stdin_ready_nt():
- """Return True if there's something to read on stdin (nt version)."""
- return msvcrt.kbhit()
-
-def _stdin_ready_other():
- """Return True, assuming there's something to read on stdin."""
- return True
-
-
-def _ignore_CTRL_C_posix():
- """Ignore CTRL+C (SIGINT)."""
- signal.signal(signal.SIGINT, signal.SIG_IGN)
-
-def _allow_CTRL_C_posix():
- """Take CTRL+C into account (SIGINT)."""
- signal.signal(signal.SIGINT, signal.default_int_handler)
-
-def _ignore_CTRL_C_other():
- """Ignore CTRL+C (not implemented)."""
- pass
-
-def _allow_CTRL_C_other():
- """Take CTRL+C into account (not implemented)."""
- pass
-
-
-if os.name == 'posix':
- import select
- import signal
- stdin_ready = _stdin_ready_posix
- ignore_CTRL_C = _ignore_CTRL_C_posix
- allow_CTRL_C = _allow_CTRL_C_posix
-elif os.name == 'nt':
- import msvcrt
- stdin_ready = _stdin_ready_nt
- ignore_CTRL_C = _ignore_CTRL_C_other
- allow_CTRL_C = _allow_CTRL_C_other
-else:
- stdin_ready = _stdin_ready_other
- ignore_CTRL_C = _ignore_CTRL_C_other
- allow_CTRL_C = _allow_CTRL_C_other
-
-
-def clear_inputhook():
- """Set PyOS_InputHook to NULL and return the previous one"""
- pyos_inputhook_ptr = ctypes.c_void_p.in_dll(ctypes.pythonapi,
- "PyOS_InputHook")
- pyos_inputhook_ptr.value = ctypes.c_void_p(None).value
- allow_CTRL_C()
-
-def get_pyos_inputhook():
- """Return the current PyOS_InputHook as a ctypes.c_void_p."""
- return ctypes.c_void_p.in_dll(ctypes.pythonapi, "PyOS_InputHook")
-
-def set_pyft_callback(callback):
- callback = ctypes.PYFUNCTYPE(ctypes.c_int)(callback)
- return callback
-
-def remove_pyqt_inputhook():
- if QT_API == 'pyqt' or QT_API == 'pyqt5':
- QtCore.pyqtRemoveInputHook()
- else:
- pass
-
-
-#------------------------------------------------------------------------------
-# Input hooks
-#------------------------------------------------------------------------------
-def qt4():
- """PyOS_InputHook python hook for Qt4.
-
- Process pending Qt events and if there's no pending keyboard
- input, spend a short slice of time (50ms) running the Qt event
- loop.
-
- As a Python ctypes callback can't raise an exception, we catch
- the KeyboardInterrupt and temporarily deactivate the hook,
- which will let a *second* CTRL+C be processed normally and go
- back to a clean prompt line.
- """
- try:
- allow_CTRL_C()
- app = QtCore.QCoreApplication.instance()
- if not app:
- return 0
- app.processEvents(QtCore.QEventLoop.AllEvents, 300)
- if not stdin_ready():
- # Generally a program would run QCoreApplication::exec()
- # from main() to enter and process the Qt event loop until
- # quit() or exit() is called and the program terminates.
- #
- # For our input hook integration, we need to repeatedly
- # enter and process the Qt event loop for only a short
- # amount of time (say 50ms) to ensure that Python stays
- # responsive to other user inputs.
- #
- # A naive approach would be to repeatedly call
- # QCoreApplication::exec(), using a timer to quit after a
- # short amount of time. Unfortunately, QCoreApplication
- # emits an aboutToQuit signal before stopping, which has
- # the undesirable effect of closing all modal windows.
- #
- # To work around this problem, we instead create a
- # QEventLoop and call QEventLoop::exec(). Other than
- # setting some state variables which do not seem to be
- # used anywhere, the only thing QCoreApplication adds is
- # the aboutToQuit signal which is precisely what we are
- # trying to avoid.
- timer = QtCore.QTimer()
- event_loop = QtCore.QEventLoop()
- timer.timeout.connect(event_loop.quit)
- while not stdin_ready():
- timer.start(50)
- event_loop.exec_()
- timer.stop()
- except KeyboardInterrupt:
- print("\nKeyboardInterrupt - Press Enter for new prompt") # spyder: test-skip
- except: # NO exceptions are allowed to escape from a ctypes callback
- ignore_CTRL_C()
- from traceback import print_exc
- print_exc()
- print("Got exception from inputhook, unregistering.") # spyder: test-skip
- clear_inputhook()
- finally:
- allow_CTRL_C()
- return 0
diff --git a/spyder/utils/ipython/kernelspec.py b/spyder/utils/ipython/kernelspec.py
index 517727306e3..e244bc2970a 100644
--- a/spyder/utils/ipython/kernelspec.py
+++ b/spyder/utils/ipython/kernelspec.py
@@ -97,7 +97,6 @@ def env(self):
CONF.set('main_interpreter', 'umr/namelist', umr_namelist)
env_vars = {
- 'IPYTHON_KERNEL': 'True',
'EXTERNAL_INTERPRETER': not default_interpreter,
'UMR_ENABLED': CONF.get('main_interpreter', 'umr/enabled'),
'UMR_VERBOSE': CONF.get('main_interpreter', 'umr/verbose'),
diff --git a/spyder/utils/site/sitecustomize.py b/spyder/utils/site/sitecustomize.py
index b028146da5c..6f5cdf18717 100644
--- a/spyder/utils/site/sitecustomize.py
+++ b/spyder/utils/site/sitecustomize.py
@@ -17,7 +17,6 @@
import shlex
import sys
import time
-import traceback
PY2 = sys.version[0] == '2'
@@ -35,7 +34,6 @@
#==============================================================================
# Main constants
#==============================================================================
-IS_IPYKERNEL = os.environ.get("IPYTHON_KERNEL", "").lower() == "true"
IS_EXT_INTERPRETER = os.environ.get('EXTERNAL_INTERPRETER', '').lower() == "true"
@@ -103,40 +101,6 @@ def execfile(filename, namespace):
exec(compile(f.read(), filename, 'exec'), namespace)
-#==============================================================================
-# Colorization of sys.stderr (standard Python interpreter)
-#==============================================================================
-if os.environ.get("COLORIZE_SYS_STDERR", "").lower() == "true":
- class StderrProxy(object):
- """Proxy to sys.stderr file object overriding only the `write` method
- to provide red colorization for the whole stream, and blue-underlined
- for traceback file links"""
- def __init__(self):
- self.old_stderr = sys.stderr
- self.__buffer = ''
- sys.stderr = self
-
- def __getattr__(self, name):
- return getattr(self.old_stderr, name)
-
- def write(self, text):
- if os.name == 'nt' and '\n' not in text:
- self.__buffer += text
- return
- for text in (self.__buffer+text).splitlines(True):
- if text.startswith(' File') \
- and not text.startswith(' File "<'):
- # Show error links in blue underlined text
- colored_text = ' '+'\x1b[4;34m'+text[2:]+'\x1b[0m'
- else:
- # Show error messages in red
- colored_text = '\x1b[31m'+text+'\x1b[0m'
- self.old_stderr.write(colored_text)
- self.__buffer = ''
-
- stderrproxy = StderrProxy()
-
-
#==============================================================================
# Prepending this spyder package's path to sys.path to be sure
# that another version of spyder won't be imported instead:
@@ -176,6 +140,7 @@ def write(self, text):
#==============================================================================
# Settings for our MacOs X app
#==============================================================================
+# FIXME: If/when we create new apps we need to revisit this!
if sys.platform == 'darwin':
from spyder.config.base import MAC_APP_NAME
if MAC_APP_NAME in __file__:
@@ -264,7 +229,6 @@ def _getfilesystemencoding_wrapper():
#==============================================================================
-# Importing matplotlib before creating the monitor.
# This prevents a kernel crash with the inline backend in our IPython
# consoles on Linux and Python 3 (Fixes Issue 2257)
#==============================================================================
@@ -274,234 +238,33 @@ def _getfilesystemencoding_wrapper():
matplotlib = None # analysis:ignore
-#==============================================================================
-# Monitor-based functionality
-#==============================================================================
-if IS_IPYKERNEL or os.environ.get('SPYDER_SHELL_ID') is None:
- monitor = None
-else:
- from spyder.widgets.externalshell.monitor import Monitor
- monitor = Monitor("127.0.0.1",
- int(os.environ['SPYDER_I_PORT']),
- int(os.environ['SPYDER_N_PORT']),
- os.environ['SPYDER_SHELL_ID'])
- monitor.start()
-
- def open_in_spyder(source, lineno=1):
- """
- Open a source file in Spyder's editor (it could be a filename or a
- Python module/package).
-
- If you want to use IPython's %edit use %ed instead
- """
- try:
- source = sys.modules[source]
- except KeyError:
- source = source
- if not isinstance(source, basestring):
- try:
- source = source.__file__
- except AttributeError:
- raise ValueError("source argument must be either "
- "a string or a module object")
- if source.endswith('.pyc'):
- source = source[:-1]
- source = osp.abspath(source)
- if osp.exists(source):
- monitor.notify_open_file(source, lineno=lineno)
- else:
- _print("Can't open file %s" % source, file=sys.stderr)
- builtins.open_in_spyder = open_in_spyder
-
- # Our own input hook, monitor based and for Windows only
- if os.name == 'nt' and matplotlib and not IS_IPYKERNEL:
- # Qt imports
- if os.environ["QT_API"] == 'pyqt5':
- from PyQt5 import QtCore
- from PyQt5 import QtWidgets
- elif os.environ["QT_API"] == 'pyqt':
- from PyQt4 import QtCore # analysis:ignore
- from PyQt4 import QtGui as QtWidgets
- elif os.environ["QT_API"] == 'pyside':
- from PySide import QtCore # analysis:ignore
- from PySide import QtGui as QtWidgets
-
- def qt_nt_inputhook():
- """Qt input hook for Windows
-
- This input hook wait for available stdin data (notified by
- ExternalPythonShell through the monitor's inputhook_flag
- attribute), and in the meantime it processes Qt events.
- """
- # Refreshing variable explorer, except on first input hook call:
- # (otherwise, on slow machines, this may freeze Spyder)
- monitor.refresh_from_inputhook()
-
- # NOTE: This is making the inputhoook to fail completely!!
- # That's why it's commented.
- #try:
- # This call fails for Python without readline support
- # (or on Windows platforms) when PyOS_InputHook is called
- # for the second consecutive time, because the 100-bytes
- # stdin buffer is full.
- # For more details, see the `PyOS_StdioReadline` function
- # in Python source code (Parser/myreadline.c)
- # sys.stdin.tell()
- #except IOError:
- # return 0
-
- # Input hook
- app = QtCore.QCoreApplication.instance()
- if app is None:
- app = QtWidgets.QApplication([" "])
- if app and app.thread() is QtCore.QThread.currentThread():
- try:
- timer = QtCore.QTimer()
- timer.timeout.connect(app.quit)
- monitor.toggle_inputhook_flag(False)
- while not monitor.inputhook_flag:
- timer.start(50)
- QtCore.QCoreApplication.exec_()
- timer.stop()
- except KeyboardInterrupt:
- _print("\nKeyboardInterrupt - Press Enter for new prompt")
-
- # Socket-based alternative:
- #socket = QtNetwork.QLocalSocket()
- #socket.connectToServer(os.environ['SPYDER_SHELL_ID'])
- #socket.waitForConnected(-1)
- #while not socket.waitForReadyRead(10):
- # timer.start(50)
- # QtCore.QCoreApplication.exec_()
- # timer.stop()
- #socket.read(3)
- #socket.disconnectFromServer()
- return 0
-
-
#==============================================================================
# Matplotlib settings
#==============================================================================
if matplotlib is not None:
- if not IS_IPYKERNEL:
- mpl_backend = os.environ.get("SPY_MPL_BACKEND", "")
- mpl_ion = os.environ.get("MATPLOTLIB_ION", "")
-
- # Setting no backend if the user asks for it
- if not mpl_backend or mpl_backend.lower() == 'none':
- mpl_backend = ""
-
- # Set backend automatically
- if mpl_backend.lower() == 'automatic':
- if not IS_EXT_INTERPRETER:
- if os.environ["QT_API"] == 'pyqt5':
- mpl_backend = 'Qt5Agg'
- else:
- mpl_backend = 'Qt4Agg'
- else:
- # Test for backend libraries on external interpreters
- def set_mpl_backend(backend):
- mod, bend, qt_api = backend
- try:
- if mod:
- __import__(mod)
- if qt_api and (os.environ["QT_API"] != qt_api):
- return None
- else:
- matplotlib.use(bend)
- return bend
- except (ImportError, ValueError):
- return None
-
- backends = [('PyQt5', 'Qt5Agg', 'pyqt5'),
- ('PyQt4', 'Qt4Agg', 'pyqt'),
- ('PySide', 'Qt4Agg', 'pyqt')]
- if not os.name == 'nt':
- backends.append( ('_tkinter', 'TkAgg', None) )
-
- for b in backends:
- mpl_backend = set_mpl_backend(b)
- if mpl_backend:
- break
-
- if not mpl_backend:
- _print("NOTE: No suitable Matplotlib backend was found!\n"
- " You won't be able to create plots\n")
-
- # To have mpl docstrings as rst
- matplotlib.rcParams['docstring.hardcopy'] = True
-
- # Activate interactive mode when needed
- if mpl_ion.lower() == "true":
- matplotlib.rcParams['interactive'] = True
-
- from spyder.utils import inputhooks
- if mpl_backend:
- import ctypes
-
- # Grab QT_API
- qt_api = os.environ["QT_API"]
-
- # Setting the user defined backend
- if not IS_EXT_INTERPRETER:
- matplotlib.use(mpl_backend)
-
- # Setting the right input hook according to mpl_backend,
- # IMPORTANT NOTE: Don't try to abstract the steps to set a PyOS
- # input hook callback in a function. It will **crash** the
- # interpreter!!
- if (mpl_backend == "Qt4Agg" or mpl_backend == "Qt5Agg") and \
- os.name == 'nt' and monitor is not None:
- # Removing PyQt4 input hook which is not working well on
- # Windows since opening a subprocess does not attach a real
- # console to it (with keyboard events...)
- if qt_api == 'pyqt' or qt_api == 'pyqt5':
- inputhooks.remove_pyqt_inputhook()
- # Using our own input hook
- # NOTE: it's not working correctly for some configurations
- # (See issue 1831)
- callback = inputhooks.set_pyft_callback(qt_nt_inputhook)
- pyos_ih = inputhooks.get_pyos_inputhook()
- pyos_ih.value = ctypes.cast(callback, ctypes.c_void_p).value
- elif mpl_backend == "Qt4Agg" and qt_api == 'pyside':
- # PySide doesn't have an input hook, so we need to install one
- # to be able to show plots
- # Note: This only works well for Posix systems
- callback = inputhooks.set_pyft_callback(inputhooks.qt4)
- pyos_ih = inputhooks.get_pyos_inputhook()
- pyos_ih.value = ctypes.cast(callback, ctypes.c_void_p).value
- elif (mpl_backend != "Qt4Agg" and qt_api == 'pyqt') \
- or (mpl_backend != "Qt5Agg" and qt_api == 'pyqt5'):
- # Matplotlib backends install their own input hooks, so we
- # need to remove the PyQt one to make them work
- inputhooks.remove_pyqt_inputhook()
- else:
- inputhooks.remove_pyqt_inputhook()
- else:
- # To have mpl docstrings as rst
- matplotlib.rcParams['docstring.hardcopy'] = True
+ # To have mpl docstrings as rst
+ matplotlib.rcParams['docstring.hardcopy'] = True
#==============================================================================
# IPython kernel adjustments
#==============================================================================
-if IS_IPYKERNEL:
- # Use ipydb as the debugger to patch on IPython consoles
- from IPython.core.debugger import Pdb as ipyPdb
- pdb.Pdb = ipyPdb
-
- # Patch unittest.main so that errors are printed directly in the console.
- # See http://comments.gmane.org/gmane.comp.python.ipython.devel/10557
- # Fixes Issue 1370
- import unittest
- from unittest import TestProgram
- class IPyTesProgram(TestProgram):
- def __init__(self, *args, **kwargs):
- test_runner = unittest.TextTestRunner(stream=sys.stderr)
- kwargs['testRunner'] = kwargs.pop('testRunner', test_runner)
- kwargs['exit'] = False
- TestProgram.__init__(self, *args, **kwargs)
- unittest.main = IPyTesProgram
+# Use ipydb as the debugger to patch on IPython consoles
+from IPython.core.debugger import Pdb as ipyPdb
+pdb.Pdb = ipyPdb
+
+# Patch unittest.main so that errors are printed directly in the console.
+# See http://comments.gmane.org/gmane.comp.python.ipython.devel/10557
+# Fixes Issue 1370
+import unittest
+from unittest import TestProgram
+class IPyTesProgram(TestProgram):
+ def __init__(self, *args, **kwargs):
+ test_runner = unittest.TextTestRunner(stream=sys.stderr)
+ kwargs['testRunner'] = kwargs.pop('testRunner', test_runner)
+ kwargs['exit'] = False
+ TestProgram.__init__(self, *args, **kwargs)
+unittest.main = IPyTesProgram
#==============================================================================
@@ -568,11 +331,8 @@ def notify_spyder(self, frame):
if not frame:
return
- if IS_IPYKERNEL:
- from IPython.core.getipython import get_ipython
- ipython_shell = get_ipython()
- else:
- ipython_shell = None
+ from IPython.core.getipython import get_ipython
+ ipython_shell = get_ipython()
# Get filename and line number of the current frame
fname = self.canonic(frame.f_code.co_filename)
@@ -589,9 +349,6 @@ def notify_spyder(self, frame):
if osp.isfile(fname):
if ipython_shell:
step = dict(fname=fname, lineno=lineno)
- elif monitor is not None:
- monitor.notify_pdb_step(fname, lineno)
- time.sleep(0.1)
# Publish Pdb state so we can update the Variable Explorer
# and the Editor on the Spyder side
@@ -684,13 +441,12 @@ def _cmdloop(self):
@monkeypatch_method(pdb.Pdb, 'Pdb')
def reset(self):
self._old_Pdb_reset()
- if IS_IPYKERNEL:
- from IPython.core.getipython import get_ipython
- ipython_shell = get_ipython()
- if ipython_shell:
- ipython_shell.kernel._register_pdb_session(self)
- elif monitor is not None:
- monitor.register_pdb_session(self)
+
+ from IPython.core.getipython import get_ipython
+ ipython_shell = get_ipython()
+ if ipython_shell:
+ ipython_shell.kernel._register_pdb_session(self)
+
self.set_spyder_breakpoints()
@@ -742,8 +498,7 @@ def break_here(self, frame):
# 'site-packages' directory from sys.path!
#==============================================================================
try:
- sys.path.remove(osp.join(spyder_path, "spyder", "widgets",
- "externalshell"))
+ sys.path.remove(osp.join(spyder_path, "spyder", "utils", "site"))
except ValueError:
pass
@@ -815,13 +570,10 @@ def clear_post_mortem():
"""
Remove the post mortem excepthook and replace with a standard one.
"""
- if IS_IPYKERNEL:
- from IPython.core.getipython import get_ipython
- ipython_shell = get_ipython()
- if ipython_shell:
- ipython_shell.set_custom_exc((), None)
- else:
- sys.excepthook = sys.__excepthook__
+ from IPython.core.getipython import get_ipython
+ ipython_shell = get_ipython()
+ if ipython_shell:
+ ipython_shell.set_custom_exc((), None)
def post_mortem_excepthook(type, value, tb):
@@ -830,14 +582,11 @@ def post_mortem_excepthook(type, value, tb):
mortem debugging.
"""
clear_post_mortem()
- if IS_IPYKERNEL:
- from IPython.core.getipython import get_ipython
- ipython_shell = get_ipython()
- ipython_shell.showtraceback((type, value, tb))
- p = pdb.Pdb(ipython_shell.colors)
- else:
- traceback.print_exception(type, value, tb, file=sys.stderr)
- p = pdb.Pdb()
+
+ from IPython.core.getipython import get_ipython
+ ipython_shell = get_ipython()
+ ipython_shell.showtraceback((type, value, tb))
+ p = pdb.Pdb(ipython_shell.colors)
if not type == SyntaxError:
# wait for stderr to print (stderr.flush does not work in this case)
@@ -863,15 +612,12 @@ def set_post_mortem():
"""
Enable the post mortem debugging excepthook.
"""
- if IS_IPYKERNEL:
- from IPython.core.getipython import get_ipython
- def ipython_post_mortem_debug(shell, etype, evalue, tb,
- tb_offset=None):
- post_mortem_excepthook(etype, evalue, tb)
- ipython_shell = get_ipython()
- ipython_shell.set_custom_exc((Exception,), ipython_post_mortem_debug)
- else:
- sys.excepthook = post_mortem_excepthook
+ from IPython.core.getipython import get_ipython
+ def ipython_post_mortem_debug(shell, etype, evalue, tb,
+ tb_offset=None):
+ post_mortem_excepthook(etype, evalue, tb)
+ ipython_shell = get_ipython()
+ ipython_shell.set_custom_exc((Exception,), ipython_post_mortem_debug)
# Add post mortem debugging if requested and in a dedicated interpreter
# existing interpreters use "runfile" below
@@ -940,10 +686,9 @@ def runfile(filename, args=None, wdir=None, namespace=None, post_mortem=False):
if HAS_CYTHON and os.path.splitext(filename)[1].lower() == '.pyx':
# Cython files
with io.open(filename, encoding='utf-8') as f:
- if IS_IPYKERNEL:
- from IPython.core.getipython import get_ipython
- ipython_shell = get_ipython()
- ipython_shell.run_cell_magic('cython', '', f.read())
+ from IPython.core.getipython import get_ipython
+ ipython_shell = get_ipython()
+ ipython_shell.run_cell_magic('cython', '', f.read())
else:
execfile(filename, namespace)
@@ -973,52 +718,6 @@ def debugfile(filename, args=None, wdir=None, post_mortem=False):
builtins.debugfile = debugfile
-#==============================================================================
-# Evaluate external commands
-#==============================================================================
-def evalsc(command):
- """Evaluate special commands
- (analog to IPython's magic commands but far less powerful/complete)"""
- assert command.startswith('%')
- from spyder.utils import programs
-
- namespace = _get_globals()
- command = command[1:].strip() # Remove leading %
-
- import re
- clear_match = re.match(r"^clear ([a-zA-Z0-9_, ]+)", command)
- cd_match = re.match(r"^cd \"?\'?([a-zA-Z0-9_\ \:\\\/\.]+)", command)
-
- if cd_match:
- os.chdir(eval('r"%s"' % cd_match.groups()[0].strip()))
- elif clear_match:
- varnames = clear_match.groups()[0].replace(' ', '').split(',')
- for varname in varnames:
- try:
- namespace.pop(varname)
- except KeyError:
- pass
- elif command in ('cd', 'pwd'):
- try:
- _print(os.getcwdu())
- except AttributeError:
- _print(os.getcwd())
- elif command == 'ls':
- if os.name == 'nt':
- programs.run_shell_command('dir')
- _print('\n')
- else:
- programs.run_shell_command('ls')
- _print('\n')
- elif command == 'scientific':
- from spyder.config import base
- execfile(base.SCIENTIFIC_STARTUP, namespace)
- else:
- raise NotImplementedError("Unsupported command: '%s'" % command)
-
-builtins.evalsc = evalsc
-
-
#==============================================================================
# Restoring original PYTHONPATH
#==============================================================================
diff --git a/spyder/widgets/externalshell/__init__.py b/spyder/widgets/externalshell/__init__.py
deleted file mode 100644
index 042ce5d4b1b..00000000000
--- a/spyder/widgets/externalshell/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © Spyder Project Contributors
-# Licensed under the terms of the MIT License
-# (see spyder/__init__.py for details)
-
-"""
-spyder.widgets.externalshell
-============================
-
-External Shell widget: execute Python script/terminal in a separate process
-"""
diff --git a/spyder/widgets/externalshell/baseshell.py b/spyder/widgets/externalshell/baseshell.py
deleted file mode 100644
index 6dd7869d01c..00000000000
--- a/spyder/widgets/externalshell/baseshell.py
+++ /dev/null
@@ -1,318 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © Spyder Project Contributors
-# Licensed under the terms of the MIT License
-# (see spyder/__init__.py for details)
-
-# pylint: disable=C0103
-# pylint: disable=R0903
-# pylint: disable=R0911
-# pylint: disable=R0201
-
-# Standard library imports
-from time import time, strftime, gmtime
-import os.path as osp
-
-# Third party imports
-from qtpy.QtCore import (QByteArray, QProcess, Qt, QTextCodec, QTimer,
- Signal, Slot, QMutex)
-from qtpy.QtWidgets import (QHBoxLayout, QInputDialog, QLabel, QLineEdit,
- QMenu, QToolButton, QVBoxLayout, QWidget)
-
-# Local imports
-from spyder.config.base import _, get_conf_path
-from spyder.py3compat import to_text_string
-from spyder.utils import icon_manager as ima
-from spyder.utils.qthelpers import (add_actions, create_action,
- create_toolbutton)
-
-
-LOCALE_CODEC = QTextCodec.codecForLocale()
-
-
-#TODO: code refactoring/cleaning (together with systemshell.py and pythonshell.py)
-class ExternalShellBase(QWidget):
- """External Shell widget: execute Python script in a separate process"""
- SHELL_CLASS = None
- redirect_stdio = Signal(bool)
- sig_finished = Signal()
-
- def __init__(self, parent=None, fname=None, wdir=None,
- history_filename=None, show_icontext=True,
- light_background=True, menu_actions=None,
- show_buttons_inside=True, show_elapsed_time=True):
- QWidget.__init__(self, parent)
-
- self.menu_actions = menu_actions
- self.write_lock = QMutex()
- self.buffer_lock = QMutex()
- self.buffer = []
-
- self.run_button = None
- self.kill_button = None
- self.options_button = None
- self.icontext_action = None
-
- self.show_elapsed_time = show_elapsed_time
-
- self.fname = fname
- if wdir is None:
- wdir = osp.dirname(osp.abspath(fname))
- self.wdir = wdir if osp.isdir(wdir) else None
- self.arguments = ""
-
- self.shell = self.SHELL_CLASS(parent, get_conf_path(history_filename))
- self.shell.set_light_background(light_background)
- self.shell.execute.connect(self.send_to_process)
- self.shell.sig_keyboard_interrupt.connect(self.keyboard_interrupt)
- # Redirecting some SIGNALs:
- self.shell.redirect_stdio.connect(
- lambda state: self.redirect_stdio.emit(state))
-
- self.state_label = None
- self.time_label = None
-
- vlayout = QVBoxLayout()
- toolbar_buttons = self.get_toolbar_buttons()
- if show_buttons_inside:
- self.state_label = QLabel()
- hlayout = QHBoxLayout()
- hlayout.addWidget(self.state_label)
- hlayout.addStretch(0)
- hlayout.addWidget(self.create_time_label())
- hlayout.addStretch(0)
- for button in toolbar_buttons:
- hlayout.addWidget(button)
- vlayout.addLayout(hlayout)
- else:
- vlayout.setContentsMargins(0, 0, 0, 0)
- vlayout.addWidget(self.get_shell_widget())
- self.setLayout(vlayout)
- self.resize(640, 480)
- if parent is None:
- self.setWindowIcon(self.get_icon())
- self.setWindowTitle(_("Console"))
-
- self.t0 = None
- self.timer = QTimer(self)
-
- self.process = None
-
- self.is_closing = False
-
- if show_buttons_inside:
- self.update_time_label_visibility()
-
- @Slot(bool)
- def set_elapsed_time_visible(self, state):
- self.show_elapsed_time = state
- if self.time_label is not None:
- self.time_label.setVisible(state)
-
- def create_time_label(self):
- """Create elapsed time label widget (if necessary) and return it"""
- if self.time_label is None:
- self.time_label = QLabel()
- return self.time_label
-
- def update_time_label_visibility(self):
- self.time_label.setVisible(self.show_elapsed_time)
-
- def is_running(self):
- if self.process is not None:
- return self.process.state() == QProcess.Running
-
- def get_toolbar_buttons(self):
- if self.run_button is None:
- self.run_button = create_toolbutton(self, text=_("Run"),
- icon=ima.icon('run'),
- tip=_("Run again this program"),
- triggered=self.start_shell)
- if self.kill_button is None:
- self.kill_button = create_toolbutton(self, text=_("Kill"),
- icon=ima.icon('kill'),
- tip=_("Kills the current process, "
- "causing it to exit immediately"))
- buttons = [self.run_button]
- if self.options_button is None:
- options = self.get_options_menu()
- if options:
- self.options_button = create_toolbutton(self, text=_('Options'),
- icon=ima.icon('tooloptions'))
- self.options_button.setPopupMode(QToolButton.InstantPopup)
- menu = QMenu(self)
- add_actions(menu, options)
- self.options_button.setMenu(menu)
- if self.options_button is not None:
- buttons.append(self.options_button)
- buttons.append(self.kill_button)
- return buttons
-
- def set_icontext_visible(self, state):
- """Set icon text visibility"""
- for widget in self.get_toolbar_buttons():
- if state:
- widget.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
- else:
- widget.setToolButtonStyle(Qt.ToolButtonIconOnly)
-
- def get_options_menu(self):
- self.show_time_action = create_action(self, _("Show elapsed time"),
- toggled=self.set_elapsed_time_visible)
- self.show_time_action.setChecked(self.show_elapsed_time)
- actions = [self.show_time_action]
- if self.menu_actions is not None:
- actions += [None]+self.menu_actions
- return actions
-
- def get_shell_widget(self):
- return self.shell
-
- def get_icon(self):
- raise NotImplementedError
-
- def show_time(self, end=False):
- if self.time_label is None:
- return
- elapsed_time = time()-self.t0
- if elapsed_time > 24*3600: # More than a day...!
- format = "%d %H:%M:%S"
- else:
- format = "%H:%M:%S"
- if end:
- color = "#AAAAAA"
- else:
- color = "#AA6655"
- text = "%s" \
- "" % (color, strftime(format, gmtime(elapsed_time)))
- self.time_label.setText(text)
-
- def closeEvent(self, event):
- if self.process is not None:
- self.is_closing = True
- self.process.kill()
- self.process.waitForFinished(100)
-
- try:
- self.timer.timeout.disconnect(self.show_time)
- except (RuntimeError, TypeError):
- pass
-
- def set_running_state(self, state=True):
- self.set_buttons_runnning_state(state)
- self.shell.setReadOnly(not state)
- if state:
- if self.state_label is not None:
- self.state_label.setText(_(
- "Running..."))
- self.t0 = time()
- self.timer.timeout.connect(self.show_time)
- self.timer.start(1000)
- else:
- if self.state_label is not None:
- self.state_label.setText(_('Terminated.'))
- try:
- self.timer.timeout.disconnect(self.show_time)
- except (RuntimeError, TypeError):
- pass
-
- def set_buttons_runnning_state(self, state):
- self.run_button.setVisible(not state)
- self.kill_button.setVisible(state)
-
- @Slot(bool)
- def start_shell(self, ask_for_arguments=False):
- """Start shell"""
- if ask_for_arguments and not self.get_arguments():
- self.set_running_state(False)
- return
- try:
- self.terminate_button.clicked.disconnect(self.process.terminate)
- self.kill_button.clicked.disconnect(self.process.terminate)
- except (AttributeError, RuntimeError, TypeError):
- pass
- self.create_process()
-
- @Slot()
- def get_arguments(self):
- arguments, valid = QInputDialog.getText(self, _('Arguments'),
- _('Command line arguments:'),
- QLineEdit.Normal,
- self.arguments)
- if valid:
- self.arguments = to_text_string(arguments)
- return valid
-
- def create_process(self):
- raise NotImplementedError
-
- def finished(self, exit_code, exit_status):
- self.shell.flush()
- self.sig_finished.emit()
- if self.is_closing:
- return
- self.set_running_state(False)
- self.show_time(end=True)
-
-#===============================================================================
-# Input/Output
-#===============================================================================
- def transcode(self, qba):
- try:
- return to_text_string(qba.data(), 'utf8')
- except UnicodeDecodeError:
- return qba.data()
-
- def get_stdout(self):
- self.process.setReadChannel(QProcess.StandardOutput)
- qba = QByteArray()
- while self.process.bytesAvailable():
- qba += self.process.readAllStandardOutput()
- return self.transcode(qba)
-
- def get_stderr(self):
- self.process.setReadChannel(QProcess.StandardError)
- qba = QByteArray()
- while self.process.bytesAvailable():
- qba += self.process.readAllStandardError()
- return self.transcode(qba)
-
- def write_output(self):
- # if we are already writing something else,
- # store the present message in a buffer
- if not self.write_lock.tryLock():
- self.buffer_lock.lock()
- self.buffer.append(self.get_stdout())
- self.buffer_lock.unlock()
-
- if not self.write_lock.tryLock():
- return
-
- self.shell.write(self.get_stdout(), flush=True)
-
- while True:
- self.buffer_lock.lock()
- messages = self.buffer
- self.buffer = []
-
- if not messages:
- self.write_lock.unlock()
- self.buffer_lock.unlock()
- return
-
- self.buffer_lock.unlock()
- self.shell.write("\n".join(messages), flush=True)
-
- def send_to_process(self, qstr):
- raise NotImplementedError
-
- def send_ctrl_to_process(self, letter):
- char = chr("abcdefghijklmnopqrstuvwxyz".index(letter) + 1)
- byte_array = QByteArray()
- byte_array.append(char)
- self.process.write(byte_array)
- self.process.waitForBytesWritten(-1)
- self.shell.write(LOCALE_CODEC.toUnicode(byte_array), flush=True)
-
- def keyboard_interrupt(self):
- raise NotImplementedError
diff --git a/spyder/widgets/externalshell/introspection.py b/spyder/widgets/externalshell/introspection.py
deleted file mode 100644
index 2705331971b..00000000000
--- a/spyder/widgets/externalshell/introspection.py
+++ /dev/null
@@ -1,204 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © Spyder Project Contributors
-# Licensed under the terms of the MIT License
-# (see spyder/__init__.py for details)
-
-"""External shell's introspection and notification servers"""
-
-# Standard library imports
-import errno
-import os
-import socket
-import threading
-
-# Third party imports
-from qtpy.QtCore import QThread, Signal
-
-# Local imports
-from spyder.config.base import get_conf_path, DEBUG
-from spyder.utils.debug import log_last_error
-from spyder.utils.bsdsocket import read_packet, write_packet
-from spyder.utils.misc import select_port
-
-
-LOG_FILENAME = get_conf_path('introspection.log')
-
-DEBUG_INTROSPECTION = DEBUG >= 2
-
-if DEBUG_INTROSPECTION:
- import logging
- logging.basicConfig(filename=get_conf_path('introspection_debug.log'),
- level=logging.DEBUG)
-
-SPYDER_PORT = 20128
-
-
-class IntrospectionServer(threading.Thread):
- """Introspection server"""
- def __init__(self):
- threading.Thread.__init__(self)
- self.shells = {}
- self.setDaemon(True)
- global SPYDER_PORT
- self.port = SPYDER_PORT = select_port(default_port=SPYDER_PORT)
- SPYDER_PORT += 1
-
- def register(self, shell):
- """Register introspection server
- See notification server below"""
- shell_id = str(id(shell))
- self.shells[shell_id] = shell
-
- def send_socket(self, shell_id, sock):
- """Send socket to the appropriate object for later communication"""
- shell = self.shells[shell_id]
- shell.set_introspection_socket(sock)
- if DEBUG_INTROSPECTION:
- logging.debug('Introspection server: shell [%r] port [%r]'
- % (shell, self.port))
-
- def run(self):
- """Start server"""
- sock = socket.socket(socket.AF_INET)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- sock.bind( ("127.0.0.1", self.port) )
-
- while True:
- sock.listen(2)
- try:
- conn, _addr = sock.accept()
- except socket.error as e:
- # See Issue 1275 for details on why errno EINTR is
- # silently ignored here.
- eintr = errno.WSAEINTR if os.name == 'nt' else errno.EINTR
- if e.args[0] == eintr:
- continue
- raise
- shell_id = read_packet(conn)
- if shell_id is not None:
- self.send_socket(shell_id, conn)
-
-class NotificationServer(IntrospectionServer):
- """Notification server"""
- def __init__(self):
- IntrospectionServer.__init__(self)
- self.notification_threads = {}
-
- def register(self, shell):
- """Register notification server
- See pythonshell.ExternalPythonShell.create_process"""
- IntrospectionServer.register(self, shell)
- shell_id = str(id(shell))
- n_thread = self.notification_threads[shell_id] = NotificationThread()
- return n_thread
-
- def send_socket(self, shell_id, sock):
- """Send socket to the appropriate object for later communication"""
- n_thread = self.notification_threads[shell_id]
- n_thread.set_notify_socket(sock)
- n_thread.start()
- if DEBUG_INTROSPECTION:
- logging.debug('Notification server: shell [%r] port [%r]'
- % (self.shells[shell_id], self.port))
-
-INTROSPECTION_SERVER = None
-
-def start_introspection_server():
- """
- Start introspection server (only one time)
- This server is dedicated to introspection features, i.e. Spyder is calling
- it to retrieve informations on remote objects
- """
- global INTROSPECTION_SERVER
- if INTROSPECTION_SERVER is None:
- if DEBUG_INTROSPECTION:
- import time
- time_str = "Logging time: %s" % time.ctime(time.time())
- logging.debug("="*len(time_str))
- logging.debug(time_str)
- logging.debug("="*len(time_str))
- INTROSPECTION_SERVER = IntrospectionServer()
- INTROSPECTION_SERVER.start()
- return INTROSPECTION_SERVER
-
-NOTIFICATION_SERVER = None
-
-def start_notification_server():
- """
- Start notify server (only one time)
- This server is dedicated to notification features, i.e. remote objects
- are notifying Spyder about anything relevant like debugging data (pdb)
- or "this is the right moment to refresh variable explorer" (syshook)
- """
- global NOTIFICATION_SERVER
- if NOTIFICATION_SERVER is None:
- NOTIFICATION_SERVER = NotificationServer()
- NOTIFICATION_SERVER.start()
- return NOTIFICATION_SERVER
-
-
-class NotificationThread(QThread):
- """Notification thread"""
- sig_process_remote_view = Signal(object)
- sig_pdb = Signal(str, int)
- open_file = Signal(str, int)
- refresh_namespace_browser = Signal()
-
- def __init__(self):
- QThread.__init__(self)
- self.notify_socket = None
-
- def set_notify_socket(self, notify_socket):
- """Set the notification socket"""
- self.notify_socket = notify_socket
-
- def run(self):
- """Start notification thread"""
- while True:
- if self.notify_socket is None:
- continue
- output = None
- try:
- try:
- cdict = read_packet(self.notify_socket)
- except:
- # This except statement is intended to handle a struct.error
- # (but when writing 'except struct.error', it doesn't work)
- # Note: struct.error is raised when the communication has
- # been interrupted and the received data is not a string
- # of length 8 as required by struct.unpack (see read_packet)
- break
- if cdict is None:
- # Another notification thread has just terminated and
- # then wrote 'None' in the notification socket
- # (see the 'finally' statement below)
- continue
- if not isinstance(cdict, dict):
- raise TypeError("Invalid data type: %r" % cdict)
- command = cdict['command']
- data = cdict.get('data')
- if command == 'pdb_step':
- fname, lineno = data
- self.sig_pdb.emit(fname, lineno)
- self.refresh_namespace_browser.emit()
- elif command == 'refresh':
- self.refresh_namespace_browser.emit()
- elif command == 'remote_view':
- self.sig_process_remote_view.emit(data)
- elif command == 'open_file':
- fname, lineno = data
- self.open_file.emit(fname, lineno)
- else:
- raise RuntimeError('Unsupported command: %r' % command)
- if DEBUG_INTROSPECTION:
- logging.debug("received command: %r" % command)
- except:
- log_last_error(LOG_FILENAME, "notification thread")
- finally:
- try:
- write_packet(self.notify_socket, output)
- except:
- # The only reason why it should fail is that Spyder is
- # closing while this thread is still alive
- break
diff --git a/spyder/widgets/externalshell/monitor.py b/spyder/widgets/externalshell/monitor.py
deleted file mode 100644
index 99114c20323..00000000000
--- a/spyder/widgets/externalshell/monitor.py
+++ /dev/null
@@ -1,521 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © Spyder Project Contributors
-# Licensed under the terms of the MIT License
-# (see spyder/__init__.py for details)
-
-"""External shell's monitor"""
-
-#TODO: The "disable auto-refresh when variable explorer is hidden" feature
-# broken since we removed the "shell" widget reference from notification
-# thread. We must find another mechanism to avoid refreshing systematically
-# remote views for all consoles...!
-
-import os
-import socket
-import struct
-import threading
-
-# Local imports
-from spyder.config.base import get_conf_path, DEBUG
-from spyder.py3compat import getcwd, is_text_string, pickle, _thread
-from spyder.utils.misc import fix_reference_name
-from spyder.utils.debug import log_last_error
-from spyder.utils.dochelpers import (getargtxt, getdoc, getsource,
- getobjdir, isdefined)
-from spyder.utils.bsdsocket import (communicate, read_packet, write_packet,
- PACKET_NOT_RECEIVED, PICKLE_HIGHEST_PROTOCOL)
-from spyder.utils.introspection.module_completion import module_completion
-from spyder.widgets.variableexplorer.utils import (get_remote_data,
- make_remote_view)
-
-
-LOG_FILENAME = get_conf_path('monitor.log')
-DEBUG_MONITOR = DEBUG >= 2
-if DEBUG_MONITOR:
- import logging
- logging.basicConfig(filename=get_conf_path('monitor_debug.log'),
- level=logging.DEBUG)
-
-
-def monitor_save_globals(sock, settings, filename):
- """Save globals() to file"""
- return communicate(sock, '__save_globals__()',
- settings=[settings, filename])
-
-def monitor_load_globals(sock, filename, ext):
- """Load globals() from file"""
- return communicate(sock, '__load_globals__()', settings=[filename, ext])
-
-def monitor_get_global(sock, name):
- """Get global variable *name* value"""
- return communicate(sock, '__get_global__("%s")' % name)
-
-def monitor_set_global(sock, name, value):
- """Set global variable *name* value to *value*"""
- return communicate(sock, '__set_global__("%s")' % name,
- settings=[value])
-
-def monitor_del_global(sock, name):
- """Del global variable *name*"""
- return communicate(sock, '__del_global__("%s")' % name)
-
-def monitor_copy_global(sock, orig_name, new_name):
- """Copy global variable *orig_name* to *new_name*"""
- return communicate(sock, '__copy_global__("%s", "%s")' \
- % (orig_name, new_name))
-
-def _getcdlistdir():
- """Return current directory list dir"""
- return os.listdir(getcwd())
-
-
-class Monitor(threading.Thread):
- """Monitor server"""
- def __init__(self, host, introspection_port, notification_port,
- shell_id, timeout=2000, auto_refresh=False):
- threading.Thread.__init__(self)
- self.setDaemon(True)
-
- self.pdb_obj = None
-
- self.timeout = None
- self.set_timeout(timeout)
- self.auto_refresh = auto_refresh
- self.refresh_after_eval = False
- self.remote_view_settings = None
-
- self.inputhook_flag = False
- self.first_inputhook_call = True
-
- # Connecting to introspection server
- self.i_request = socket.socket( socket.AF_INET )
- self.i_request.connect( (host, introspection_port) )
- write_packet(self.i_request, shell_id)
-
- # Connecting to notification server
- self.n_request = socket.socket( socket.AF_INET )
- self.n_request.connect( (host, notification_port) )
- write_packet(self.n_request, shell_id)
-
- self._mlocals = {
- "refresh": self.enable_refresh_after_eval,
- "setlocal": self.setlocal,
- "is_array": self.is_array,
- "is_image": self.is_image,
- "get_globals_keys": self.get_globals_keys,
- "getmodcomplist": self.getmodcomplist,
- "getcdlistdir": _getcdlistdir,
- "getcwd": self.getcwd,
- "setcwd": self.setcwd,
- "getsyspath": self.getsyspath,
- "getenv": self.getenv,
- "setenv": self.setenv,
- "isdefined": self.isdefined,
- "thread": _thread,
- "toggle_inputhook_flag": self.toggle_inputhook_flag,
- "set_monitor_timeout": self.set_timeout,
- "set_monitor_auto_refresh": self.set_auto_refresh,
- "set_remote_view_settings":
- self.set_remote_view_settings,
- "set_spyder_breakpoints": self.set_spyder_breakpoints,
- "__get_dir__": self.get_dir,
- "__iscallable__": self.iscallable,
- "__get_arglist__": self.get_arglist,
- "__get__doc____": self.get__doc__,
- "__get_doc__": self.get_doc,
- "__get_source__": self.get_source,
- "__get_global__": self.getglobal,
- "__set_global__": self.setglobal,
- "__del_global__": self.delglobal,
- "__copy_global__": self.copyglobal,
- "__save_globals__": self.saveglobals,
- "__load_globals__": self.loadglobals,
- "_" : None}
- self._mglobals = None
-
- @property
- def pdb_frame(self):
- """Return current Pdb frame if there is any"""
- if self.pdb_obj is not None and self.pdb_obj.curframe is not None:
- return self.pdb_obj.curframe
-
- @property
- def pdb_locals(self):
- """Return current Pdb frame locals if available
- Otherwise return an empty dictionary"""
- if self.pdb_frame:
- return self.pdb_obj.curframe_locals
- else:
- return {}
-
- def mlocals(self):
- """Return current locals -- handles Pdb frames"""
- ns = {}
- ns.update(self._mlocals)
- ns.update(self.pdb_locals)
- return ns
-
- def mglobals(self):
- """Return current globals -- handles Pdb frames"""
- if self.pdb_frame is not None:
- return self.pdb_frame.f_globals
- else:
- if self._mglobals is None:
- from __main__ import __dict__ as glbs
- self._mglobals = glbs
- else:
- glbs = self._mglobals
- self._mglobals = glbs
- return glbs
-
- def get_current_namespace(self):
- """Return current namespace, i.e. globals() if not debugging,
- or a dictionary containing both locals() and globals()
- for current frame when debugging"""
- ns = {}
- glbs = self.mglobals()
-
- if self.pdb_frame is None:
- ns.update(glbs)
- else:
- ns.update(glbs)
- ns.update(self.pdb_locals)
-
- return ns
-
- def get_reference_namespace(self, name):
- """Return namespace where reference name is defined,
- eventually returns the globals() if reference has not yet been defined"""
- glbs = self.mglobals()
- if self.pdb_frame is None:
- return glbs
- else:
- lcls = self.pdb_locals
- if name in lcls:
- return lcls
- else:
- return glbs
-
- def get_globals_keys(self):
- """Return globals() keys or globals() and locals() keys if debugging"""
- ns = self.get_current_namespace()
- return list(ns.keys())
-
- def isdefined(self, obj, force_import=False):
- """Return True if object is defined in current namespace"""
- ns = self.get_current_namespace()
- return isdefined(obj, force_import=force_import, namespace=ns)
-
- def toggle_inputhook_flag(self, state):
- """Toggle the input hook flag
-
- The only purpose of this flag is to unblock the PyOS_InputHook
- callback when text is available in stdin (see sitecustomize.py)"""
- self.inputhook_flag = state
-
- def set_timeout(self, timeout):
- """Set monitor timeout (in milliseconds!)"""
- self.timeout = float(timeout)/1000.
-
- def set_auto_refresh(self, state):
- """Enable/disable namespace browser auto refresh feature"""
- self.auto_refresh = state
-
- def enable_refresh_after_eval(self):
- self.refresh_after_eval = True
-
- #------ Notifications
- def refresh(self):
- """Refresh variable explorer in ExternalPythonShell"""
- communicate(self.n_request, dict(command="refresh"))
-
- def refresh_from_inputhook(self):
- """Refresh variable explorer from the PyOS_InputHook.
- See sitecustomize.py"""
- # Refreshing variable explorer, except on first input hook call
- # (otherwise, on slow machines, this may freeze Spyder)
- if self.first_inputhook_call:
- self.first_inputhook_call = False
- else:
- self.refresh()
-
- def register_pdb_session(self, pdb_obj):
- self.pdb_obj = pdb_obj
-
- def notify_pdb_step(self, fname, lineno):
- """Notify the ExternalPythonShell regarding pdb current frame"""
- communicate(self.n_request,
- dict(command="pdb_step", data=(fname, lineno)))
-
- def set_spyder_breakpoints(self):
- """Set all Spyder breakpoints in active pdb session"""
- if not self.pdb_obj:
- return
- self.pdb_obj.set_spyder_breakpoints()
-
- def notify_open_file(self, fname, lineno=1):
- """Open file in Spyder's editor"""
- communicate(self.n_request,
- dict(command="open_file", data=(fname, lineno)))
-
- #------ Code completion / Calltips
- def _eval(self, text):
- """
- Evaluate text and return (obj, valid)
- where *obj* is the object represented by *text*
- and *valid* is True if object evaluation did not raise any exception
- """
- assert is_text_string(text)
- ns = self.get_current_namespace()
- try:
- return eval(text, ns), True
- except:
- return None, False
-
- def get_dir(self, objtxt):
- """Return dir(object)"""
- obj, valid = self._eval(objtxt)
- if valid:
- return getobjdir(obj)
-
- def iscallable(self, objtxt):
- """Is object callable?"""
- obj, valid = self._eval(objtxt)
- if valid:
- return callable(obj)
-
- def get_arglist(self, objtxt):
- """Get func/method argument list"""
- obj, valid = self._eval(objtxt)
- if valid:
- return getargtxt(obj)
-
- def get__doc__(self, objtxt):
- """Get object __doc__"""
- obj, valid = self._eval(objtxt)
- if valid:
- return obj.__doc__
-
- def get_doc(self, objtxt):
- """Get object documentation dictionary"""
- obj, valid = self._eval(objtxt)
- if valid:
- return getdoc(obj)
-
- def get_source(self, objtxt):
- """Get object source"""
- obj, valid = self._eval(objtxt)
- if valid:
- return getsource(obj)
-
- def getmodcomplist(self, name, path):
- """Return module completion list for object named *name*"""
- return module_completion(name, path)
-
- #------ Other
- def is_array(self, name):
- """Return True if object is an instance of class numpy.ndarray"""
- ns = self.get_current_namespace()
- try:
- import numpy
- return isinstance(ns[name], numpy.ndarray)
- except ImportError:
- return False
-
- def is_image(self, name):
- """Return True if object is an instance of class PIL.Image.Image"""
- ns = self.get_current_namespace()
- try:
- from spyder.pil_patch import Image
- return isinstance(ns[name], Image.Image)
- except ImportError:
- return False
-
- def getcwd(self):
- """Return current working directory"""
- return getcwd()
-
- def setcwd(self, dirname):
- """Set current working directory"""
- return os.chdir(dirname)
-
- def getenv(self):
- """Return os.environ"""
- return os.environ.copy()
-
- def setenv(self):
- """Set os.environ"""
- env = read_packet(self.i_request)
- os.environ = env
-
- def getsyspath(self):
- """Return sys.path[:]"""
- import sys
- return sys.path[:]
-
- def setlocal(self, name, value):
- """
- Set local reference value
- Not used right now - could be useful in the future
- """
- self._mlocals[name] = value
-
- def set_remote_view_settings(self):
- """
- Set the namespace remote view settings
- (see the namespace browser widget)
- """
- self.remote_view_settings = read_packet(self.i_request)
- self.enable_refresh_after_eval()
-
- def update_remote_view(self):
- """
- Return remote view of globals()
- """
- settings = self.remote_view_settings
- if settings:
- ns = self.get_current_namespace()
- remote_view = make_remote_view(ns, settings)
- communicate(self.n_request,
- dict(command="remote_view", data=remote_view))
-
- def saveglobals(self):
- """Save globals() into filename"""
- ns = self.get_current_namespace()
- from spyder.utils.iofuncs import iofunctions
- settings = read_packet(self.i_request)
- filename = read_packet(self.i_request)
- data = get_remote_data(ns, settings, mode='picklable').copy()
- return iofunctions.save(data, filename)
-
- def loadglobals(self):
- """Load globals() from filename"""
- glbs = self.mglobals()
- from spyder.utils.iofuncs import iofunctions
- filename = read_packet(self.i_request)
- ext = read_packet(self.i_request)
- load_func = iofunctions.load_funcs[ext]
- data, error_message = load_func(filename)
- if error_message:
- return error_message
- for key in list(data.keys()):
- new_key = fix_reference_name(key, blacklist=list(glbs.keys()))
- if new_key != key:
- data[new_key] = data.pop(key)
- try:
- glbs.update(data)
- except Exception as error:
- return str(error)
- self.refresh_after_eval = True
-
- def getglobal(self, name):
- """
- Get global reference value
- """
- ns = self.get_current_namespace()
- return ns[name]
-
- def setglobal(self, name):
- """
- Set global reference value
- """
- ns = self.get_reference_namespace(name)
- ns[name] = read_packet(self.i_request)
- self.refresh_after_eval = True
-
- def delglobal(self, name):
- """
- Del global reference
- """
- ns = self.get_reference_namespace(name)
- ns.pop(name)
- self.refresh_after_eval = True
-
- def copyglobal(self, orig_name, new_name):
- """
- Copy global reference
- """
- ns = self.get_reference_namespace(orig_name)
- ns[new_name] = ns[orig_name]
- self.refresh_after_eval = True
-
- def run(self):
- while True:
- output = pickle.dumps(None, PICKLE_HIGHEST_PROTOCOL)
- glbs = self.mglobals()
- try:
- if DEBUG_MONITOR:
- logging.debug("****** Introspection request /Begin ******")
- command = PACKET_NOT_RECEIVED
- try:
- timeout = self.timeout if self.auto_refresh else None
- command = read_packet(self.i_request, timeout=timeout)
- if command is None:
- continue
- timed_out = False
- except socket.timeout:
- timed_out = True
- except struct.error:
- # This should mean that Spyder GUI has crashed
- if DEBUG_MONITOR:
- logging.debug("struct.error -> quitting monitor")
- break
- if timed_out:
- if DEBUG_MONITOR:
- logging.debug("connection timed out -> updating remote view")
- self.update_remote_view()
- if DEBUG_MONITOR:
- logging.debug("****** Introspection request /End ******")
- continue
- if DEBUG_MONITOR:
- logging.debug("command: %r" % command)
- lcls = self.mlocals()
- result = eval(command, glbs, lcls)
- if DEBUG_MONITOR:
- logging.debug(" result: %r" % result)
- if self.pdb_obj is None:
- lcls["_"] = result
- # old com implementation: (see solution (1) in Issue 434)
- output = pickle.dumps(result, PICKLE_HIGHEST_PROTOCOL)
-# # new com implementation: (see solution (2) in Issue 434)
-# output = pickle.dumps((command, result),
-# PICKLE_HIGHEST_PROTOCOL)
- except SystemExit:
- break
- except:
- if DEBUG_MONITOR:
- logging.debug("error!")
- log_last_error(LOG_FILENAME, command)
- finally:
- try:
- if DEBUG_MONITOR:
- logging.debug("updating remote view")
- if self.refresh_after_eval:
- self.update_remote_view()
- self.refresh_after_eval = False
- if DEBUG_MONITOR:
- logging.debug("sending result")
- logging.debug("****** Introspection request /End ******")
- if command is not PACKET_NOT_RECEIVED:
- if write_packet is None:
- # This may happen during interpreter shutdown
- break
- else:
- write_packet(self.i_request, output,
- already_pickled=True)
- except AttributeError as error:
- if "'NoneType' object has no attribute" in str(error):
- # This may happen during interpreter shutdown
- break
- else:
- raise
- except TypeError as error:
- if "'NoneType' object is not subscriptable" in str(error):
- # This may happen during interpreter shutdown
- break
- else:
- raise
-
- self.i_request.close()
- self.n_request.close()
diff --git a/spyder/widgets/externalshell/pythonshell.py b/spyder/widgets/externalshell/pythonshell.py
deleted file mode 100644
index b78f70a01b0..00000000000
--- a/spyder/widgets/externalshell/pythonshell.py
+++ /dev/null
@@ -1,657 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © Spyder Project Contributors
-# Licensed under the terms of the MIT License
-# (see spyder/__init__.py for details)
-
-"""External Python Shell widget: execute Python script in a separate process"""
-
-# Standard library imports
-import os
-import os.path as osp
-import socket
-import sys
-
-# Third party imports
-from qtpy.compat import getexistingdirectory
-from qtpy.QtCore import QProcess, QProcessEnvironment, Qt, Signal, Slot
-from qtpy.QtWidgets import QApplication, QMenu, QMessageBox, QSplitter
-
-# Local imports
-from spyder.config.base import (_, DEBUG, get_module_source_path,
- MAC_APP_NAME, running_in_mac_app)
-from spyder.py3compat import (is_text_string, to_binary_string,
- to_text_string)
-from spyder.utils import icon_manager as ima
-from spyder.utils.bsdsocket import communicate, write_packet
-from spyder.utils.environ import RemoteEnvDialog
-from spyder.utils.misc import add_pathlist_to_PYTHONPATH, get_python_executable
-from spyder.utils.programs import get_python_args
-from spyder.utils.qthelpers import (add_actions, create_action,
- create_toolbutton, DialogManager)
-from spyder.widgets.externalshell.baseshell import ExternalShellBase
-from spyder.widgets.shell import PythonShellWidget
-from spyder.widgets.variableexplorer.namespacebrowser import NamespaceBrowser
-from spyder.widgets.variableexplorer.collectionseditor import CollectionsEditor
-
-
-class ExtPythonShellWidget(PythonShellWidget):
-
- wait_for_ready_read = Signal()
- go_to_error = Signal(str)
- focus_changed = Signal()
-
- def __init__(self, parent, history_filename, profile=False):
- initial_message = _("NOTE: The Python console is going to "
- "be REMOVED in Spyder 3.2. Please start "
- "to migrate your work to the "
- "IPython console instead.\n\n")
- PythonShellWidget.__init__(self, parent, history_filename, profile,
- initial_message=initial_message)
- self.path = []
-
- def set_externalshell(self, externalshell):
- # ExternalShellBase instance:
- self.externalshell = externalshell
-
- def clear_terminal(self):
- """Reimplement ShellBaseWidget method"""
- self.clear()
- self.execute.emit("\n")
-
- def execute_lines(self, lines):
- """
- Execute a set of lines as multiple command
- lines: multiple lines of text to be executed as single commands
- """
- for line in lines.splitlines():
- stripped_line = line.strip()
- if stripped_line.startswith('#'):
- continue
- self.write(line+os.linesep, flush=True)
- self.execute_command(line)
- # Workaround for Issue 502
- # Emmiting wait_for_ready_read was making the console hang
- # in Mac OS X
- if sys.platform.startswith("darwin"):
- import time
- time.sleep(0.025)
- else:
- self.wait_for_ready_read.emit()
- self.flush()
-
- #------ Code completion / Calltips
- def ask_monitor(self, command, settings=[]):
- sock = self.externalshell.introspection_socket
- if sock is None:
- return
- try:
- return communicate(sock, command, settings=settings)
- except socket.error:
- # Process was just closed
- pass
- except MemoryError:
- # Happens when monitor is not ready on slow machines
- pass
-
- def get_dir(self, objtxt):
- """Return dir(object)"""
- return self.ask_monitor("__get_dir__('%s')" % objtxt)
-
- def get_globals_keys(self):
- """Return shell globals() keys"""
- return self.ask_monitor("get_globals_keys()")
-
- def get_cdlistdir(self):
- """Return shell current directory list dir"""
- return self.ask_monitor("getcdlistdir()")
-
- def iscallable(self, objtxt):
- """Is object callable?"""
- return self.ask_monitor("__iscallable__('%s')" % objtxt)
-
- def get_arglist(self, objtxt):
- """Get func/method argument list"""
- return self.ask_monitor("__get_arglist__('%s')" % objtxt)
-
- def get__doc__(self, objtxt):
- """Get object __doc__"""
- return self.ask_monitor("__get__doc____('%s')" % objtxt)
-
- def get_doc(self, objtxt):
- """Get object documentation dictionary"""
- return self.ask_monitor("__get_doc__('%s')" % objtxt)
-
- def get_source(self, objtxt):
- """Get object source"""
- return self.ask_monitor("__get_source__('%s')" % objtxt)
-
- def is_defined(self, objtxt, force_import=False):
- """Return True if object is defined"""
- return self.ask_monitor("isdefined('%s', force_import=%s)"
- % (objtxt, force_import))
-
- def get_module_completion(self, objtxt):
- """Return module completion list associated to object name"""
- return self.ask_monitor("getmodcomplist('%s', %s)" % \
- (objtxt, self.path))
-
- def get_cwd(self):
- """Return shell current working directory"""
- return self.ask_monitor("getcwd()")
-
- def set_cwd(self, dirname):
- """Set shell current working directory"""
- return self.ask_monitor("setcwd(r'%s')" % dirname)
-
- def get_env(self):
- """Return environment variables: os.environ"""
- return self.ask_monitor("getenv()")
-
- def set_env(self, env):
- """Set environment variables via os.environ"""
- return self.ask_monitor('setenv()', settings=[env])
-
- def get_syspath(self):
- """Return sys.path[:]"""
- return self.ask_monitor("getsyspath()")
-
- def set_spyder_breakpoints(self):
- """Set Spyder breakpoints into debugging session"""
- return self.ask_monitor("set_spyder_breakpoints()")
-
- def is_running(self):
- """Check if parent is running"""
- return self.parent().is_running()
-
-
-class ExternalPythonShell(ExternalShellBase):
- """External Shell widget: execute Python script in a separate process"""
- SHELL_CLASS = ExtPythonShellWidget
- sig_pdb = Signal(str, int)
- open_file = Signal(str, int)
- started = Signal()
- sig_finished = Signal()
-
- def __init__(self, parent=None, fname=None, wdir=None,
- interact=False, debug=False, post_mortem=False,
- path=[], python_args='',
- arguments='', stand_alone=None,
- umr_enabled=True, umr_namelist=[], umr_verbose=True,
- pythonstartup=None, pythonexecutable=None,
- external_interpreter=False,
- monitor_enabled=True, mpl_backend=None, ets_backend='qt4',
- qt_api=None, merge_output_channels=False,
- colorize_sys_stderr=False,
- light_background=True,
- menu_actions=None, show_buttons_inside=True,
- show_elapsed_time=True):
-
- assert qt_api in (None, 'pyqt', 'pyside', 'pyqt5')
-
- self.namespacebrowser = None # namespace browser widget!
- self.dialog_manager = DialogManager()
-
- self.stand_alone = stand_alone # stand alone settings (None: plugin)
- self.interact = interact
- self.pythonstartup = pythonstartup
- self.pythonexecutable = pythonexecutable
- self.external_interpreter = external_interpreter
- self.monitor_enabled = monitor_enabled
- self.mpl_backend = mpl_backend
- self.ets_backend = ets_backend
- self.qt_api = qt_api
- self.merge_output_channels = merge_output_channels
- self.colorize_sys_stderr = colorize_sys_stderr
- self.umr_enabled = umr_enabled
- self.umr_namelist = umr_namelist
- self.umr_verbose = umr_verbose
-
- self.namespacebrowser_button = None
- self.cwd_button = None
- self.env_button = None
- self.syspath_button = None
- self.terminate_button = None
-
- self.notification_thread = None
-
- ExternalShellBase.__init__(self, parent=parent, fname=fname, wdir=wdir,
- history_filename='history.py',
- light_background=light_background,
- menu_actions=menu_actions,
- show_buttons_inside=show_buttons_inside,
- show_elapsed_time=show_elapsed_time)
-
- if self.pythonexecutable is None:
- self.pythonexecutable = get_python_executable()
-
- self.python_args = None
- if python_args:
- assert is_text_string(python_args)
- self.python_args = python_args
-
- assert is_text_string(arguments)
- self.arguments = arguments
-
- self.connection_file = None
- self.shell.set_externalshell(self)
-
- self.toggle_globals_explorer(False)
- self.interact_action.setChecked(self.interact)
- self.debug_action.setChecked(debug)
-
-
- self.introspection_socket = None
- self.is_interpreter = fname is None
-
- if self.is_interpreter:
- self.terminate_button.hide()
-
- self.post_mortem_action.setChecked(post_mortem and not self.is_interpreter)
-
- # Additional python path list
- self.path = path
- self.shell.path = path
-
- def set_introspection_socket(self, introspection_socket):
- self.introspection_socket = introspection_socket
- if self.namespacebrowser is not None:
- settings = self.namespacebrowser.get_view_settings()
- communicate(introspection_socket,
- 'set_remote_view_settings()', settings=[settings])
-
- def closeEvent(self, event):
- self.quit_monitor()
- ExternalShellBase.closeEvent(self, event)
-
- def get_toolbar_buttons(self):
- ExternalShellBase.get_toolbar_buttons(self)
- if self.namespacebrowser_button is None \
- and self.stand_alone is not None:
- self.namespacebrowser_button = create_toolbutton(self,
- text=_("Variables"), icon=ima.icon('dictedit'),
- tip=_("Show/hide global variables explorer"),
- toggled=self.toggle_globals_explorer, text_beside_icon=True)
- if self.terminate_button is None:
- self.terminate_button = create_toolbutton(self,
- text=_("Terminate"), icon=ima.icon('stop'),
- tip=_("Attempts to stop the process. The process\n"
- "may not exit as a result of clicking this\n"
- "button (it is given the chance to prompt\n"
- "the user for any unsaved files, etc)."))
- buttons = []
- if self.namespacebrowser_button is not None:
- buttons.append(self.namespacebrowser_button)
- buttons += [self.run_button, self.terminate_button, self.kill_button,
- self.options_button]
- return buttons
-
- def get_options_menu(self):
- ExternalShellBase.get_options_menu(self)
- self.interact_action = create_action(self, _("Interact"))
- self.interact_action.setCheckable(True)
- self.debug_action = create_action(self, _("Debug"))
- self.debug_action.setCheckable(True)
- self.args_action = create_action(self, _("Arguments..."),
- triggered=self.get_arguments)
- self.post_mortem_action = create_action(self, _("Post Mortem Debug"))
- self.post_mortem_action.setCheckable(True)
- run_settings_menu = QMenu(_("Run settings"), self)
- add_actions(run_settings_menu,
- (self.interact_action, self.debug_action, self.args_action,
- self.post_mortem_action))
- self.cwd_button = create_action(self, _("Working directory"),
- icon=ima.icon('DirOpenIcon'),
- tip=_("Set current working directory"),
- triggered=self.set_current_working_directory)
- self.env_button = create_action(self, _("Environment variables"),
- icon=ima.icon('environ'),
- triggered=self.show_env)
- self.syspath_button = create_action(self,
- _("Show sys.path contents"),
- icon=ima.icon('syspath'),
- triggered=self.show_syspath)
- actions = [run_settings_menu, self.show_time_action, None,
- self.cwd_button, self.env_button, self.syspath_button]
- if self.menu_actions is not None:
- actions += [None]+self.menu_actions
- return actions
-
- def is_interpreter(self):
- """Return True if shellwidget is a Python interpreter"""
- return self.is_interpreter
-
- def get_shell_widget(self):
- if self.stand_alone is None:
- return self.shell
- else:
- self.namespacebrowser = NamespaceBrowser(self)
- settings = self.stand_alone
- self.namespacebrowser.set_shellwidget(self)
- self.namespacebrowser.setup(**settings)
- self.namespacebrowser.sig_collapse.connect(
- lambda: self.toggle_globals_explorer(False))
- # Shell splitter
- self.splitter = splitter = QSplitter(Qt.Vertical, self)
- self.splitter.splitterMoved.connect(self.splitter_moved)
- splitter.addWidget(self.shell)
- splitter.setCollapsible(0, False)
- splitter.addWidget(self.namespacebrowser)
- splitter.setStretchFactor(0, 1)
- splitter.setStretchFactor(1, 0)
- splitter.setHandleWidth(5)
- splitter.setSizes([2, 1])
- return splitter
-
- def get_icon(self):
- return ima.icon('python')
-
- def set_buttons_runnning_state(self, state):
- ExternalShellBase.set_buttons_runnning_state(self, state)
- self.interact_action.setEnabled(not state and not self.is_interpreter)
- self.debug_action.setEnabled(not state and not self.is_interpreter)
- self.args_action.setEnabled(not state and not self.is_interpreter)
- self.post_mortem_action.setEnabled(not state and not self.is_interpreter)
- if state:
- if self.arguments:
- argstr = _("Arguments: %s") % self.arguments
- else:
- argstr = _("No argument")
- else:
- argstr = _("Arguments...")
- self.args_action.setText(argstr)
- self.terminate_button.setVisible(not self.is_interpreter and state)
- if not state:
- self.toggle_globals_explorer(False)
- for btn in (self.cwd_button, self.env_button, self.syspath_button):
- btn.setEnabled(state and self.monitor_enabled)
- if self.namespacebrowser_button is not None:
- self.namespacebrowser_button.setEnabled(state)
-
- def set_namespacebrowser(self, namespacebrowser):
- """
- Set namespace browser *widget*
- Note: this method is not used in stand alone mode
- """
- self.namespacebrowser = namespacebrowser
- self.configure_namespacebrowser()
-
- def configure_namespacebrowser(self):
- """Connect the namespace browser to the notification thread"""
- if self.notification_thread is not None:
- self.notification_thread.refresh_namespace_browser.connect(
- self.namespacebrowser.refresh_table)
- signal = self.notification_thread.sig_process_remote_view
- signal.connect(lambda data:
- self.namespacebrowser.process_remote_view(data))
-
- def create_process(self):
- self.shell.clear()
-
- self.process = QProcess(self)
- if self.merge_output_channels:
- self.process.setProcessChannelMode(QProcess.MergedChannels)
- else:
- self.process.setProcessChannelMode(QProcess.SeparateChannels)
- self.shell.wait_for_ready_read.connect(
- lambda: self.process.waitForReadyRead(250))
-
- # Working directory
- if self.wdir is not None:
- self.process.setWorkingDirectory(self.wdir)
-
- #-------------------------Python specific------------------------------
- # Python arguments
- p_args = ['-u']
- if DEBUG >= 3:
- p_args += ['-v']
- p_args += get_python_args(self.fname, self.python_args,
- self.interact_action.isChecked(),
- self.debug_action.isChecked(),
- self.arguments)
-
- env = [to_text_string(_path)
- for _path in self.process.systemEnvironment()]
- if self.pythonstartup:
- env.append('PYTHONSTARTUP=%s' % self.pythonstartup)
-
- #-------------------------Python specific-------------------------------
- # Post mortem debugging
- if self.post_mortem_action.isChecked():
- env.append('SPYDER_EXCEPTHOOK=True')
-
- # Set standard input/output encoding for Python consoles
- # See http://stackoverflow.com/q/26312400/438386, specifically
- # the comments of Martijn Pieters
- env.append('PYTHONIOENCODING=UTF-8')
-
- # Monitor
- if self.monitor_enabled:
- env.append('SPYDER_SHELL_ID=%s' % id(self))
- from spyder.widgets.externalshell import introspection
- introspection_server = introspection.start_introspection_server()
- introspection_server.register(self)
- notification_server = introspection.start_notification_server()
- self.notification_thread = notification_server.register(self)
- self.notification_thread.sig_pdb.connect(
- lambda fname, lineno:
- self.sig_pdb.emit(fname, lineno))
- self.notification_thread.open_file.connect(
- lambda fname, lineno:
- self.open_file.emit(fname, lineno))
- if self.namespacebrowser is not None:
- self.configure_namespacebrowser()
- env.append('SPYDER_I_PORT=%d' % introspection_server.port)
- env.append('SPYDER_N_PORT=%d' % notification_server.port)
-
- # External modules options
- env.append('ETS_TOOLKIT=%s' % self.ets_backend)
- if self.mpl_backend is not None:
- backends = {0: 'Automatic', 1: 'None', 2: 'TkAgg'}
- env.append('SPY_MPL_BACKEND=%s' % backends[self.mpl_backend])
- if self.qt_api:
- env.append('QT_API=%s' % self.qt_api)
- env.append('COLORIZE_SYS_STDERR=%s' % self.colorize_sys_stderr)
-# # Socket-based alternative (see input hook in sitecustomize.py):
-# if self.install_qt_inputhook:
-# from PyQt4.QtNetwork import QLocalServer
-# self.local_server = QLocalServer()
-# self.local_server.listen(str(id(self)))
-
- # User Module Deleter
- if self.is_interpreter:
- env.append('UMR_ENABLED=%r' % self.umr_enabled)
- env.append('UMR_NAMELIST=%s' % ','.join(self.umr_namelist))
- env.append('UMR_VERBOSE=%r' % self.umr_verbose)
- env.append('MATPLOTLIB_ION=True')
- else:
- if self.interact:
- env.append('MATPLOTLIB_ION=True')
- else:
- env.append('MATPLOTLIB_ION=False')
-
- # External interpreter
- env.append('EXTERNAL_INTERPRETER=%r' % self.external_interpreter)
-
- # Add sitecustomize path to path list
- pathlist = []
- spy_path = get_module_source_path('spyder')
- sc_path = osp.join(spy_path, 'utils', 'site')
- pathlist.append(sc_path)
-
- # Adding Spyder path
- pathlist += self.path
-
- # Adding path list to PYTHONPATH environment variable
- add_pathlist_to_PYTHONPATH(env, pathlist)
-
- #-------------------------Python specific------------------------------
-
- self.process.readyReadStandardOutput.connect(self.write_output)
- self.process.readyReadStandardError.connect(self.write_error)
- self.process.finished.connect(lambda ec, es=QProcess.ExitStatus:
- self.finished(ec, es))
- self.sig_finished.connect(self.dialog_manager.close_all)
- self.terminate_button.clicked.connect(self.process.terminate)
- self.kill_button.clicked.connect(self.process.kill)
-
- #-------------------------Python specific------------------------------
- # Fixes for our Mac app:
- # 1. PYTHONPATH and PYTHONHOME are set while bootstrapping the app,
- # but their values are messing sys.path for external interpreters
- # (e.g. EPD) so we need to remove them from the environment.
- # 2. Set PYTHONPATH again but without grabbing entries defined in the
- # environment (Fixes Issue 1321)
- # 3. Remove PYTHONOPTIMIZE from env so that we can have assert
- # statements working with our interpreters (See Issue 1281)
- if running_in_mac_app():
- if MAC_APP_NAME not in self.pythonexecutable:
- env = [p for p in env if not (p.startswith('PYTHONPATH') or \
- p.startswith('PYTHONHOME'))] # 1.
-
- add_pathlist_to_PYTHONPATH(env, pathlist, drop_env=True) # 2.
- env = [p for p in env if not p.startswith('PYTHONOPTIMIZE')] # 3.
-
- processEnvironment = QProcessEnvironment()
- for envItem in env:
- envName, separator, envValue = envItem.partition('=')
- processEnvironment.insert(envName, envValue)
- self.process.setProcessEnvironment(processEnvironment)
- self.process.start(self.pythonexecutable, p_args)
- #-------------------------Python specific------------------------------
-
- running = self.process.waitForStarted(3000)
- self.set_running_state(running)
- if not running:
- QMessageBox.critical(self, _("Error"),
- _("A Python console failed to start!"))
- else:
- self.shell.setFocus()
- self.started.emit()
- return self.process
-
- def finished(self, exit_code, exit_status):
- """Reimplement ExternalShellBase method"""
- ExternalShellBase.finished(self, exit_code, exit_status)
- self.introspection_socket = None
-
-
-#==============================================================================
-# Input/Output
-#==============================================================================
- def write_error(self):
- if os.name == 'nt':
- #---This is apparently necessary only on Windows (not sure though):
- # emptying standard output buffer before writing error output
- self.process.setReadChannel(QProcess.StandardOutput)
- if self.process.waitForReadyRead(1):
- self.write_output()
- self.shell.write_error(self.get_stderr())
- QApplication.processEvents()
-
- def send_to_process(self, text):
- if not self.is_running():
- return
- if not is_text_string(text):
- text = to_text_string(text)
- if self.mpl_backend == 0 and os.name == 'nt' and \
- self.introspection_socket is not None:
- communicate(self.introspection_socket, "toggle_inputhook_flag(True)")
-# # Socket-based alternative (see input hook in sitecustomize.py):
-# while self.local_server.hasPendingConnections():
-# self.local_server.nextPendingConnection().write('go!')
- if any([text == cmd for cmd in ['%ls', '%pwd', '%scientific']]) or \
- any([text.startswith(cmd) for cmd in ['%cd ', '%clear ']]):
- text = 'evalsc(r"%s")\n' % text
- if not text.endswith('\n'):
- text += '\n'
- self.process.write(to_binary_string(text, 'utf8'))
- self.process.waitForBytesWritten(-1)
-
- # Eventually write prompt faster (when hitting Enter continuously)
- # -- necessary/working on Windows only:
- if os.name == 'nt':
- self.write_error()
-
- def keyboard_interrupt(self):
- if self.introspection_socket is not None:
- communicate(self.introspection_socket, "thread.interrupt_main()")
-
- def quit_monitor(self):
- if self.introspection_socket is not None:
- try:
- write_packet(self.introspection_socket, "thread.exit()")
- except socket.error:
- pass
-
-#==============================================================================
-# Globals explorer
-#==============================================================================
- @Slot(bool)
- def toggle_globals_explorer(self, state):
- if self.stand_alone is not None:
- self.splitter.setSizes([1, 1 if state else 0])
- self.namespacebrowser_button.setChecked(state)
- if state and self.namespacebrowser is not None:
- self.namespacebrowser.refresh_table()
-
- def splitter_moved(self, pos, index):
- self.namespacebrowser_button.setChecked( self.splitter.sizes()[1] )
-
-#==============================================================================
-# Misc.
-#==============================================================================
- @Slot()
- def set_current_working_directory(self):
- """Set current working directory"""
- cwd = self.shell.get_cwd()
- self.redirect_stdio.emit(False)
- directory = getexistingdirectory(self, _("Select directory"), cwd)
- if directory:
- self.shell.set_cwd(directory)
- self.redirect_stdio.emit(True)
-
- @Slot()
- def show_env(self):
- """Show environment variables"""
- get_func = self.shell.get_env
- set_func = self.shell.set_env
- self.dialog_manager.show(RemoteEnvDialog(get_func, set_func))
-
- @Slot()
- def show_syspath(self):
- """Show sys.path contents"""
- editor = CollectionsEditor()
- editor.setup(self.shell.get_syspath(), title="sys.path", readonly=True,
- width=600, icon=ima.icon('syspath'))
- self.dialog_manager.show(editor)
-
-
-def test():
- from spyder.utils.qthelpers import qapplication
- app = qapplication()
-
- from spyder.plugins.variableexplorer import VariableExplorer
-
- settings = VariableExplorer(None).get_settings()
-
- shell = ExternalPythonShell(pythonexecutable=sys.executable,
- interact=True,
- stand_alone=settings,
- wdir=osp.dirname(__file__),
- mpl_backend=0,
- light_background=False)
-
- from spyder.config.gui import get_font
-
- font = get_font()
- shell.shell.set_font(font)
-
- shell.shell.toggle_wrap_mode(True)
- shell.start_shell(False)
- shell.show()
- sys.exit(app.exec_())
-
-
-if __name__ == "__main__":
- test()
diff --git a/spyder/widgets/externalshell/systemshell.py b/spyder/widgets/externalshell/systemshell.py
deleted file mode 100644
index 838a50fa435..00000000000
--- a/spyder/widgets/externalshell/systemshell.py
+++ /dev/null
@@ -1,184 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright © Spyder Project Contributors
-# Licensed under the terms of the MIT License
-# (see spyder/__init__.py for details)
-
-"""External System Shell widget: execute terminal in a separate process"""
-
-# Standard library imports
-import os
-import sys
-
-# Third party imports
-from qtpy.QtCore import QProcess, QProcessEnvironment, QTextCodec, Signal
-from qtpy.QtWidgets import QMessageBox
-
-# Local imports
-from spyder.config.base import _
-from spyder.py3compat import is_text_string, to_text_string
-from spyder.utils import icon_manager as ima
-from spyder.utils.misc import add_pathlist_to_PYTHONPATH
-from spyder.widgets.externalshell.baseshell import ExternalShellBase
-from spyder.widgets.shell import TerminalWidget
-
-
-LOCALE_CODEC = QTextCodec.codecForLocale()
-CP850_CODEC = QTextCodec.codecForName('cp850')
-
-
-class ExternalSystemShell(ExternalShellBase):
- """External Shell widget: execute Python script in a separate process"""
- SHELL_CLASS = TerminalWidget
- started = Signal()
-
- def __init__(self, parent=None, wdir=None, path=[], light_background=True,
- menu_actions=None, show_buttons_inside=True,
- show_elapsed_time=True):
- ExternalShellBase.__init__(self, parent=parent, fname=None, wdir=wdir,
- history_filename='.history',
- light_background=light_background,
- menu_actions=menu_actions,
- show_buttons_inside=show_buttons_inside,
- show_elapsed_time=show_elapsed_time)
-
- # Additional python path list
- self.path = path
-
- # For compatibility with the other shells that can live in the external
- # console
- self.connection_file = None
-
- def get_icon(self):
- return ima.icon('cmdprompt')
-
- def finish_process(self):
- while not self.process.waitForFinished(100):
- self.process.kill();
-
- def create_process(self):
- self.shell.clear()
-
- self.process = QProcess(self)
- self.process.setProcessChannelMode(QProcess.MergedChannels)
-
- # PYTHONPATH (in case we use Python in this terminal, e.g. py2exe)
- env = [to_text_string(_path)
- for _path in self.process.systemEnvironment()]
-
- processEnvironment = QProcessEnvironment()
- for envItem in env:
- envName, separator, envValue = envItem.partition('=')
- processEnvironment.insert(envName, envValue)
-
- add_pathlist_to_PYTHONPATH(env, self.path)
- self.process.setProcessEnvironment(processEnvironment)
-
-
- # Working directory
- if self.wdir is not None:
- self.process.setWorkingDirectory(self.wdir)
-
- # Shell arguments
- if os.name == 'nt':
- p_args = ['/Q']
- else:
- p_args = ['-i']
-
- if self.arguments:
- p_args.extend( shell_split(self.arguments) )
-
- self.process.readyReadStandardOutput.connect(self.write_output)
- self.process.finished.connect(self.finished)
- self.kill_button.clicked.connect(self.process.kill)
-
- if os.name == 'nt':
- self.process.start('cmd.exe', p_args)
- else:
- # Using bash:
- self.process.start('bash', p_args)
- self.send_to_process('PS1="\\u@\\h:\\w> "\n')
-
- running = self.process.waitForStarted()
- self.set_running_state(running)
- if not running:
- QMessageBox.critical(self, _("Error"),
- _("Process failed to start"))
- else:
- self.shell.setFocus()
- self.started.emit()
-
- return self.process
-
-#===============================================================================
-# Input/Output
-#===============================================================================
- def transcode(self, qba):
- if os.name == 'nt':
- return to_text_string( CP850_CODEC.toUnicode(qba.data()) )
- else:
- return ExternalShellBase.transcode(self, qba)
-
- def send_to_process(self, text):
- if not is_text_string(text):
- text = to_text_string(text)
- if text[:-1] in ["clear", "cls", "CLS"]:
- self.shell.clear()
- self.send_to_process(os.linesep)
- return
- if not text.endswith('\n'):
- text += '\n'
- if os.name == 'nt':
- self.process.write(text.encode('cp850'))
- else:
- self.process.write(LOCALE_CODEC.fromUnicode(text))
- self.process.waitForBytesWritten(-1)
-
- def keyboard_interrupt(self):
- # This does not work on Windows:
- # (unfortunately there is no easy way to send a Ctrl+C to cmd.exe)
- self.send_ctrl_to_process('c')
-
-# # The following code will soon be removed:
-# # (last attempt to send a Ctrl+C on Windows)
-# if os.name == 'nt':
-# pid = int(self.process.pid())
-# import ctypes, win32api, win32con
-# class _PROCESS_INFORMATION(ctypes.Structure):
-# _fields_ = [("hProcess", ctypes.c_int),
-# ("hThread", ctypes.c_int),
-# ("dwProcessID", ctypes.c_int),
-# ("dwThreadID", ctypes.c_int)]
-# x = ctypes.cast( ctypes.c_void_p(pid),
-# ctypes.POINTER(_PROCESS_INFORMATION) )
-# win32api.GenerateConsoleCtrlEvent(win32con.CTRL_C_EVENT,
-# x.dwProcessID)
-# else:
-# self.send_ctrl_to_process('c')
-
-
-#==============================================================================
-# Tests
-#==============================================================================
-def test():
- import os.path as osp
- from spyder.utils.qthelpers import qapplication
- app = qapplication(test_time=5)
- shell = ExternalSystemShell(wdir=osp.dirname(__file__),
- light_background=False)
-
- app.aboutToQuit.connect(shell.finish_process)
-
- from qtpy.QtGui import QFont
- font = QFont()
- font.setPointSize(10)
- shell.shell.set_font(font)
-
- shell.shell.toggle_wrap_mode(True)
- shell.start_shell(False)
- shell.show()
- sys.exit(app.exec_())
-
-
-if __name__ == "__main__":
- test()
\ No newline at end of file