# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """Spyder path manager.""" # Standard library imports from __future__ import print_function from collections import OrderedDict import os import os.path as osp import re import sys # Third party imports from qtpy.compat import getexistingdirectory from qtpy.QtCore import Qt, Signal, Slot from qtpy.QtWidgets import (QDialog, QDialogButtonBox, QHBoxLayout, QListWidget, QListWidgetItem, QMessageBox, QVBoxLayout) # Local imports from spyder.config.base import _ from spyder.py3compat import PY2 from spyder.utils.icon_manager import ima from spyder.utils.misc import getcwd_or_home from spyder.utils.qthelpers import create_toolbutton class PathManager(QDialog): """Path manager dialog.""" redirect_stdio = Signal(bool) sig_path_changed = Signal(object) def __init__(self, parent, path=None, read_only_path=None, not_active_path=None, sync=True): """Path manager dialog.""" super(PathManager, self).__init__(parent) assert isinstance(path, (tuple, None)) self.path = path or () self.read_only_path = read_only_path or () self.not_active_path = not_active_path or () self.last_path = getcwd_or_home() self.original_path_dict = None # Widgets self.add_button = None self.remove_button = None self.movetop_button = None self.moveup_button = None self.movedown_button = None self.movebottom_button = None self.sync_button = None self.selection_widgets = [] self.top_toolbar_widgets = self._setup_top_toolbar() self.bottom_toolbar_widgets = self._setup_bottom_toolbar() self.listwidget = QListWidget(self) self.bbox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_ok = self.bbox.button(QDialogButtonBox.Ok) # Widget setup # Destroying the C++ object right after closing the dialog box, # otherwise it may be garbage-collected in another QThread # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) self.setWindowTitle(_("PYTHONPATH manager")) self.setWindowIcon(ima.icon('pythonpath')) self.resize(500, 300) self.sync_button.setVisible(os.name == 'nt' and sync) # Layouts top_layout = QHBoxLayout() self._add_widgets_to_layout(self.top_toolbar_widgets, top_layout) bottom_layout = QHBoxLayout() self._add_widgets_to_layout(self.bottom_toolbar_widgets, bottom_layout) bottom_layout.addWidget(self.bbox) layout = QVBoxLayout() layout.addLayout(top_layout) layout.addWidget(self.listwidget) layout.addLayout(bottom_layout) self.setLayout(layout) # Signals self.listwidget.currentRowChanged.connect(lambda x: self.refresh()) self.listwidget.itemChanged.connect(lambda x: self.refresh()) self.bbox.accepted.connect(self.accept) self.bbox.rejected.connect(self.reject) # Setup self.setup() def _add_widgets_to_layout(self, widgets, layout): """Helper to add toolbar widgets to top and bottom layout.""" layout.setAlignment(Qt.AlignLeft) for widget in widgets: if widget is None: layout.addStretch(1) else: layout.addWidget(widget) def _setup_top_toolbar(self): """Create top toolbar and actions.""" self.movetop_button = create_toolbutton( self, text=_("Move to top"), icon=ima.icon('2uparrow'), triggered=lambda: self.move_to(absolute=0), text_beside_icon=True) self.moveup_button = create_toolbutton( self, text=_("Move up"), icon=ima.icon('1uparrow'), triggered=lambda: self.move_to(relative=-1), text_beside_icon=True) self.movedown_button = create_toolbutton( self, text=_("Move down"), icon=ima.icon('1downarrow'), triggered=lambda: self.move_to(relative=1), text_beside_icon=True) self.movebottom_button = create_toolbutton( self, text=_("Move to bottom"), icon=ima.icon('2downarrow'), triggered=lambda: self.move_to(absolute=1), text_beside_icon=True) toolbar = [self.movetop_button, self.moveup_button, self.movedown_button, self.movebottom_button] self.selection_widgets.extend(toolbar) return toolbar def _setup_bottom_toolbar(self): """Create bottom toolbar and actions.""" self.add_button = create_toolbutton( self, text=_('Add path'), icon=ima.icon('edit_add'), triggered=lambda x: self.add_path(), text_beside_icon=True) self.remove_button = create_toolbutton( self, text=_('Remove path'), icon=ima.icon('edit_remove'), triggered=lambda x: self.remove_path(), text_beside_icon=True) self.sync_button = create_toolbutton( self, text=_("Synchronize..."), icon=ima.icon('fileimport'), triggered=self.synchronize, tip=_("Synchronize Spyder's path list with PYTHONPATH " "environment variable"), text_beside_icon=True) self.selection_widgets.append(self.remove_button) return [self.add_button, self.remove_button, None, self.sync_button] def _create_item(self, path): """Helper to create a new list item.""" item = QListWidgetItem(path) item.setIcon(ima.icon('DirClosedIcon')) if path in self.read_only_path: item.setFlags(Qt.NoItemFlags | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) elif path in self.not_active_path: item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Unchecked) else: item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) return item @property def editable_bottom_row(self): """Maximum bottom row count that is editable.""" read_only_count = len(self.read_only_path) if read_only_count == 0: max_row = self.listwidget.count() - 1 else: max_row = self.listwidget.count() - read_only_count - 1 return max_row def setup(self): """Populate list widget.""" self.listwidget.clear() for path in self.path + self.read_only_path: item = self._create_item(path) self.listwidget.addItem(item) self.listwidget.setCurrentRow(0) self.original_path_dict = self.get_path_dict() self.refresh() @Slot() def synchronize(self): """ Synchronize Spyder's path list with PYTHONPATH environment variable Only apply to: current user, on Windows platforms. """ answer = QMessageBox.question( self, _("Synchronize"), _("This will synchronize Spyder's path list with " "PYTHONPATH environment variable for the current user, " "allowing you to run your Python modules outside Spyder " "without having to configure sys.path. " "
" "Do you want to clear contents of PYTHONPATH before " "adding Spyder's path list?"), QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) if answer == QMessageBox.Cancel: return elif answer == QMessageBox.Yes: remove = True else: remove = False from spyder.utils.environ import (get_user_env, listdict2envdict, set_user_env) env = get_user_env() # Includes read only paths active_path = tuple(k for k, v in self.get_path_dict(True).items() if v) if remove: ppath = active_path else: ppath = env.get('PYTHONPATH', []) if not isinstance(ppath, list): ppath = [ppath] ppath = tuple(p for p in ppath if p not in active_path) ppath = ppath + active_path env['PYTHONPATH'] = list(ppath) set_user_env(listdict2envdict(env), parent=self) def get_path_dict(self, read_only=False): """ Return an ordered dict with the path entries as keys and the active state as the value. If `read_only` is True, the read_only entries are also included. `read_only` entry refers to the project path entry. """ odict = OrderedDict() for row in range(self.listwidget.count()): item = self.listwidget.item(row) path = item.text() if path in self.read_only_path and not read_only: continue odict[path] = item.checkState() == Qt.Checked return odict def refresh(self): """Refresh toolbar widgets.""" enabled = self.listwidget.currentItem() is not None for widget in self.selection_widgets: widget.setEnabled(enabled) # Disable buttons based on row row = self.listwidget.currentRow() disable_widgets = [] # Move up/top disabled for top item if row == 0: disable_widgets.extend([self.movetop_button, self.moveup_button]) # Move down/bottom disabled for bottom item if row == self.editable_bottom_row: disable_widgets.extend([self.movebottom_button, self.movedown_button]) for widget in disable_widgets: widget.setEnabled(False) self.sync_button.setEnabled(self.listwidget.count() > 0) # Ok button only enabled if actual changes occur self.button_ok.setEnabled( self.original_path_dict != self.get_path_dict()) def check_path(self, path): """Check that the path is not a [site|dist]-packages folder.""" if os.name == 'nt': pat = re.compile(r'.*lib/(?:site|dist)-packages.*') else: pat = re.compile(r'.*lib/python.../(?:site|dist)-packages.*') path_norm = path.replace('\\', '/') return pat.match(path_norm) is None @Slot() def add_path(self, directory=None): """ Add path to list widget. If `directory` is provided, the folder dialog is overridden. """ if directory is None: self.redirect_stdio.emit(False) directory = getexistingdirectory(self, _("Select directory"), self.last_path) self.redirect_stdio.emit(True) if PY2: is_unicode = False try: directory.decode('ascii') except (UnicodeEncodeError, UnicodeDecodeError): is_unicode = True if is_unicode: QMessageBox.warning( self, _("Add path"), _("You are using Python 2 and the selected path has " "Unicode characters." "
" "Therefore, this path will not be added."), QMessageBox.Ok) return directory = osp.abspath(directory) self.last_path = directory if directory in self.get_path_dict(): item = self.listwidget.findItems(directory, Qt.MatchExactly)[0] item.setCheckState(Qt.Checked) answer = QMessageBox.question( self, _("Add path"), _("This directory is already included in the list." "
" "Do you want to move it to the top of it?"), QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: item = self.listwidget.takeItem(self.listwidget.row(item)) self.listwidget.insertItem(0, item) self.listwidget.setCurrentRow(0) else: if self.check_path(directory): item = self._create_item(directory) self.listwidget.insertItem(0, item) self.listwidget.setCurrentRow(0) else: answer = QMessageBox.warning( self, _("Add path"), _("This directory cannot be added to the path!" "

" "If you want to set a different Python interpreter, " "please go to Preferences > Main interpreter" "."), QMessageBox.Ok) self.refresh() @Slot() def remove_path(self, force=False): """ Remove path from list widget. If `force` is True, the message box is overridden. """ if self.listwidget.currentItem(): if not force: answer = QMessageBox.warning( self, _("Remove path"), _("Do you really want to remove the selected path?"), QMessageBox.Yes | QMessageBox.No) if force or answer == QMessageBox.Yes: self.listwidget.takeItem(self.listwidget.currentRow()) self.refresh() def move_to(self, absolute=None, relative=None): """Move items of list widget.""" index = self.listwidget.currentRow() if absolute is not None: if absolute: new_index = self.listwidget.count() - 1 else: new_index = 0 else: new_index = index + relative new_index = max(0, min(self.editable_bottom_row, new_index)) item = self.listwidget.takeItem(index) self.listwidget.insertItem(new_index, item) self.listwidget.setCurrentRow(new_index) self.refresh() def current_row(self): """Returns the current row of the list.""" return self.listwidget.currentRow() def set_current_row(self, row): """Set the current row of the list.""" self.listwidget.setCurrentRow(row) def row_check_state(self, row): """Return the checked state for item in row.""" item = self.listwidget.item(row) return item.checkState() def set_row_check_state(self, row, value): """Set the current checked state for item in row.""" item = self.listwidget.item(row) item.setCheckState(value) def count(self): """Return the number of items.""" return self.listwidget.count() def accept(self): """Override Qt method.""" path_dict = self.get_path_dict() if self.original_path_dict != path_dict: self.sig_path_changed.emit(path_dict) super(PathManager, self).accept() def test(): """Run path manager test.""" from spyder.utils.qthelpers import qapplication _ = qapplication() dlg = PathManager( None, path=tuple(sys.path[:-2]), read_only_path=tuple(sys.path[-2:]), ) def callback(path_dict): sys.stdout.write(str(path_dict)) dlg.sig_path_changed.connect(callback) dlg.exec_() if __name__ == "__main__": test()