# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
"""Main interpreter entry in Preferences."""
# Standard library imports
import os
import os.path as osp
import sys
# Third party imports
from qtpy.QtWidgets import (QButtonGroup, QGroupBox, QInputDialog, QLabel,
QLineEdit, QMessageBox, QPushButton, QVBoxLayout)
# Local imports
from spyder.api.translations import get_translation
from spyder.api.preferences import PluginConfigPage
from spyder.py3compat import PY2, to_text_string
from spyder.utils import programs
from spyder.utils.conda import get_list_conda_envs_cache
from spyder.utils.misc import get_python_executable
from spyder.utils.pyenv import get_list_pyenv_envs_cache
# Localization
_ = get_translation('spyder')
class MainInterpreterConfigPage(PluginConfigPage):
def __init__(self, plugin, parent):
super().__init__(plugin, parent)
self.apply_callback = self.perform_adjustments
self.cus_exec_radio = None
self.pyexec_edit = None
self.cus_exec_combo = None
conda_env = get_list_conda_envs_cache()
pyenv_env = get_list_pyenv_envs_cache()
envs = {**conda_env, **pyenv_env}
valid_custom_list = self.get_option('custom_interpreters_list')
for env in envs.keys():
path, _ = envs[env]
if path not in valid_custom_list:
valid_custom_list.append(path)
self.set_option('custom_interpreters_list', valid_custom_list)
# Python executable selection (initializing default values as well)
executable = self.get_option('executable', get_python_executable())
if self.get_option('default'):
executable = get_python_executable()
if not osp.isfile(executable):
# This is absolutely necessary, in case the Python interpreter
# executable has been moved since last Spyder execution (following
# a Python distribution upgrade for example)
self.set_option('executable', get_python_executable())
elif executable.endswith('pythonw.exe'):
# That should not be necessary because this case is already taken
# care of by the `get_python_executable` function but, this was
# implemented too late, so we have to fix it here too, in case
# the Python executable has already been set with pythonw.exe:
self.set_option('executable',
executable.replace("pythonw.exe", "python.exe"))
if not self.get_option('default'):
if not self.get_option('custom_interpreter'):
self.set_option('custom_interpreter', ' ')
plugin._add_to_custom_interpreters(executable)
self.validate_custom_interpreters_list()
def initialize(self):
super().initialize()
def setup_page(self):
newcb = self.create_checkbox
# Python executable Group
pyexec_group = QGroupBox(_("Python interpreter"))
pyexec_bg = QButtonGroup(pyexec_group)
pyexec_label = QLabel(_("Select the Python interpreter for all Spyder "
"consoles"))
self.def_exec_radio = self.create_radiobutton(
_("Default (i.e. the same as Spyder's)"),
'default',
button_group=pyexec_bg,
)
self.cus_exec_radio = self.create_radiobutton(
_("Use the following Python interpreter:"),
'custom',
button_group=pyexec_bg,
)
if os.name == 'nt':
filters = _("Executables")+" (*.exe)"
else:
filters = None
pyexec_layout = QVBoxLayout()
pyexec_layout.addWidget(pyexec_label)
pyexec_layout.addWidget(self.def_exec_radio)
pyexec_layout.addWidget(self.cus_exec_radio)
self.validate_custom_interpreters_list()
self.cus_exec_combo = self.create_file_combobox(
_('Recent custom interpreters'),
self.get_option('custom_interpreters_list'),
'custom_interpreter',
filters=filters,
default_line_edit=True,
adjust_to_contents=True,
validate_callback=programs.is_python_interpreter,
)
self.def_exec_radio.toggled.connect(self.cus_exec_combo.setDisabled)
self.cus_exec_radio.toggled.connect(self.cus_exec_combo.setEnabled)
pyexec_layout.addWidget(self.cus_exec_combo)
pyexec_group.setLayout(pyexec_layout)
self.pyexec_edit = self.cus_exec_combo.combobox.lineEdit()
# UMR Group
umr_group = QGroupBox(_("User Module Reloader (UMR)"))
umr_label = QLabel(_("UMR forces Python to reload modules which were "
"imported when executing a file in a Python or "
"IPython console with the runfile "
"function."))
umr_label.setWordWrap(True)
umr_enabled_box = newcb(
_("Enable UMR"),
'umr/enabled',
msg_if_enabled=True,
msg_warning=_(
"This option will enable the User Module Reloader (UMR) "
"in Python/IPython consoles. UMR forces Python to "
"reload deeply modules during import when running a "
"Python script using the Spyder's builtin function "
"runfile."
"
1. UMR may require to restart the "
"console in which it will be called "
"(otherwise only newly imported modules will be "
"reloaded when executing files)."
"
2. If errors occur when re-running a "
"PyQt-based program, please check that the Qt objects "
"are properly destroyed (e.g. you may have to use the "
"attribute Qt.WA_DeleteOnClose on your main "
"window, using the setAttribute method)"
),
)
umr_verbose_box = newcb(
_("Show reloaded modules list"),
'umr/verbose',
msg_info=_("Please note that these changes will "
"be applied only to new consoles"),
)
umr_namelist_btn = QPushButton(
_("Set UMR excluded (not reloaded) modules"))
umr_namelist_btn.clicked.connect(self.set_umr_namelist)
umr_layout = QVBoxLayout()
umr_layout.addWidget(umr_label)
umr_layout.addWidget(umr_enabled_box)
umr_layout.addWidget(umr_verbose_box)
umr_layout.addWidget(umr_namelist_btn)
umr_group.setLayout(umr_layout)
vlayout = QVBoxLayout()
vlayout.addWidget(pyexec_group)
vlayout.addWidget(umr_group)
vlayout.addStretch(1)
self.setLayout(vlayout)
def warn_python_compatibility(self, pyexec):
if not osp.isfile(pyexec):
return
spyder_version = sys.version_info[0]
try:
args = ["-c", "import sys; print(sys.version_info[0])"]
proc = programs.run_program(pyexec, args, env={})
console_version = int(proc.communicate()[0])
except IOError:
console_version = spyder_version
except ValueError:
return False
if spyder_version != console_version:
QMessageBox.warning(
self,
_('Warning'),
_("You selected a Python %d interpreter for the console "
"but Spyder is running on Python %d!.
"
"Although this is possible, we recommend you to install and "
"run Spyder directly with your selected interpreter, to avoid "
"seeing false warnings and errors due to the incompatible "
"syntax between these two Python versions."
) % (console_version, spyder_version),
QMessageBox.Ok,
)
return True
def set_umr_namelist(self):
"""Set UMR excluded modules name list"""
arguments, valid = QInputDialog.getText(
self,
_('UMR'),
_("Set the list of excluded modules as this: "
"numpy, scipy"),
QLineEdit.Normal,
", ".join(self.get_option('umr/namelist')),
)
if valid:
arguments = to_text_string(arguments)
if arguments:
namelist = arguments.replace(' ', '').split(',')
fixed_namelist = []
non_ascii_namelist = []
for module_name in namelist:
if PY2:
if all(ord(c) < 128 for c in module_name):
if programs.is_module_installed(module_name):
fixed_namelist.append(module_name)
else:
QMessageBox.warning(
self,
_('Warning'),
_("You are working with Python 2, this means "
"that you can not import a module that "
"contains non-ascii characters."),
QMessageBox.Ok,
)
non_ascii_namelist.append(module_name)
elif programs.is_module_installed(module_name):
fixed_namelist.append(module_name)
invalid = ", ".join(set(namelist)-set(fixed_namelist)-
set(non_ascii_namelist))
if invalid:
QMessageBox.warning(
self,
_('UMR'),
_("The following modules are not "
"installed on your machine:\n%s") % invalid,
QMessageBox.Ok,
)
QMessageBox.information(
self,
_('UMR'),
_("Please note that these changes will "
"be applied only to new IPython consoles"),
QMessageBox.Ok,
)
else:
fixed_namelist = []
self.set_option('umr/namelist', fixed_namelist)
def validate_custom_interpreters_list(self):
"""Check that the used custom interpreters are still valid."""
custom_list = self.get_option('custom_interpreters_list')
valid_custom_list = []
for value in custom_list:
if osp.isfile(value):
valid_custom_list.append(value)
self.set_option('custom_interpreters_list', valid_custom_list)
def perform_adjustments(self):
"""Perform some adjustments to the page after applying preferences."""
if not self.def_exec_radio.isChecked():
# Get current executable
executable = self.pyexec_edit.text()
executable = osp.normpath(executable)
if executable.endswith('pythonw.exe'):
executable = executable.replace("pythonw.exe", "python.exe")
# Update combobox items.
custom_list = self.cus_exec_combo.combobox.choices
if executable not in custom_list:
custom_list = custom_list + [executable]
self.cus_exec_combo.combobox.clear()
self.cus_exec_combo.combobox.addItems(custom_list)
self.pyexec_edit.setText(executable)
# Show warning compatibility message.
self.warn_python_compatibility(executable)