# -*- coding: utf-8 -*- # # Copyright © Spyder Project Contributors # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) """ Namespace browser widget. This is the main widget used in the Variable Explorer plugin """ # Standard library imports import os import os.path as osp # Third library imports (qtpy) from qtpy.compat import getopenfilenames, getsavefilename from qtpy.QtCore import Qt, Signal, Slot from qtpy.QtGui import QCursor from qtpy.QtWidgets import (QApplication, QHBoxLayout, QInputDialog, QMessageBox, QVBoxLayout, QWidget) from spyder_kernels.utils.iofuncs import iofunctions from spyder_kernels.utils.misc import fix_reference_name from spyder_kernels.utils.nsview import REMOTE_SETTINGS # Local imports from spyder.api.translations import get_translation from spyder.api.widgets.mixins import SpyderWidgetMixin from spyder.widgets.collectionseditor import RemoteCollectionsEditorTableView from spyder.plugins.variableexplorer.widgets.importwizard import ImportWizard from spyder.utils import encoding from spyder.utils.misc import getcwd_or_home, remove_backslashes from spyder.widgets.helperwidgets import FinderLineEdit # Localization _ = get_translation('spyder') # Constants VALID_VARIABLE_CHARS = r"[^\w+*=¡!¿?'\"#$%&()/<>\-\[\]{}^`´;,|¬]*\w" class NamespaceBrowser(QWidget, SpyderWidgetMixin): """ Namespace browser (global variables explorer widget). """ # This is necessary to test the widget separately from its plugin CONF_SECTION = 'variable_explorer' # Signals sig_free_memory_requested = Signal() sig_start_spinner_requested = Signal() sig_stop_spinner_requested = Signal() sig_hide_finder_requested = Signal() def __init__(self, parent): super().__init__(parent=parent, class_parent=parent) # Attributes self.filename = None self.text_finder = None self.last_find = '' self.finder_is_visible = False # Widgets self.editor = None self.shellwidget = None def setup(self): """ Setup the namespace browser with provided options. """ assert self.shellwidget is not None if self.editor is not None: self.shellwidget.set_namespace_view_settings() self.refresh_table() else: # Widgets self.editor = RemoteCollectionsEditorTableView( self, data=None, shellwidget=self.shellwidget, create_menu=False, ) # Signals self.editor.sig_files_dropped.connect(self.import_data) self.editor.sig_free_memory_requested.connect( self.sig_free_memory_requested) self.editor.sig_editor_creation_started.connect( self.sig_start_spinner_requested) self.editor.sig_editor_shown.connect( self.sig_stop_spinner_requested) # Layout layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.editor) self.setLayout(layout) def get_view_settings(self): """Return dict editor view settings""" settings = {} for name in REMOTE_SETTINGS: settings[name] = self.get_conf(name) return settings def set_shellwidget(self, shellwidget): """Bind shellwidget instance to namespace browser""" self.shellwidget = shellwidget shellwidget.set_namespacebrowser(self) def set_text_finder(self, text_finder): """Bind NamespaceBrowsersFinder to namespace browser.""" self.text_finder = text_finder if self.finder_is_visible: self.text_finder.setText(self.last_find) self.editor.finder = text_finder return self.finder_is_visible def save_finder_state(self, last_find, finder_visibility): """Save last finder/search text input and finder visibility.""" if last_find and finder_visibility: self.last_find = last_find self.finder_is_visible = finder_visibility def refresh_table(self): """Refresh variable table.""" self.shellwidget.refresh_namespacebrowser() try: self.editor.resizeRowToContents() except TypeError: pass def process_remote_view(self, remote_view): """Process remote view""" # To load all variables when a new filtering search is # started. self.text_finder.load_all = False if remote_view is not None: self.set_data(remote_view) def set_var_properties(self, properties): """Set properties of variables""" if properties is not None: self.editor.var_properties = properties def set_data(self, data): """Set data.""" if data != self.editor.source_model.get_data(): self.editor.set_data(data) self.editor.adjust_columns() @Slot(list) def import_data(self, filenames=None): """Import data from text file.""" title = _("Import data") if filenames is None: if self.filename is None: basedir = getcwd_or_home() else: basedir = osp.dirname(self.filename) filenames, _selfilter = getopenfilenames(self, title, basedir, iofunctions.load_filters) if not filenames: return elif isinstance(filenames, str): filenames = [filenames] for filename in filenames: self.filename = str(filename) if os.name == "nt": self.filename = remove_backslashes(self.filename) ext = osp.splitext(self.filename)[1].lower() if ext not in iofunctions.load_funcs: buttons = QMessageBox.Yes | QMessageBox.Cancel answer = QMessageBox.question(self, title, _("Unsupported file extension '%s'

" "Would you like to import it anyway " "(by selecting a known file format)?" ) % ext, buttons) if answer == QMessageBox.Cancel: return formats = list(iofunctions.load_extensions.keys()) item, ok = QInputDialog.getItem(self, title, _('Open file as:'), formats, 0, False) if ok: ext = iofunctions.load_extensions[str(item)] else: return load_func = iofunctions.load_funcs[ext] # 'import_wizard' (self.setup_io) if isinstance(load_func, str): # Import data with import wizard error_message = None try: text, _encoding = encoding.read(self.filename) base_name = osp.basename(self.filename) editor = ImportWizard(self, text, title=base_name, varname=fix_reference_name(base_name)) if editor.exec_(): var_name, clip_data = editor.get_data() self.editor.new_value(var_name, clip_data) except Exception as error: error_message = str(error) else: QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QApplication.processEvents() error_message = self.shellwidget.load_data(self.filename, ext) QApplication.restoreOverrideCursor() QApplication.processEvents() if error_message is not None: QMessageBox.critical(self, title, _("Unable to load '%s'" "

" "The error message was:
%s" ) % (self.filename, error_message)) self.refresh_table() def reset_namespace(self): warning = self.get_conf( section='ipython_console', option='show_reset_namespace_warning' ) self.shellwidget.reset_namespace(warning=warning, message=True) self.editor.automatic_column_width = True def save_data(self, filename=None): """Save data""" if filename is None: filename = self.filename if filename is None: filename = getcwd_or_home() filename, _selfilter = getsavefilename(self, _("Save data"), filename, iofunctions.save_filters) if filename: self.filename = filename else: return False QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QApplication.processEvents() error_message = self.shellwidget.save_namespace(self.filename) QApplication.restoreOverrideCursor() QApplication.processEvents() if error_message is not None: if 'Some objects could not be saved:' in error_message: save_data_message = ( _("Some objects could not be saved:") + "

{obj_list}".format( obj_list=error_message.split(': ')[1])) else: save_data_message = _( "Unable to save current workspace" "

" "The error message was:
") + error_message QMessageBox.critical(self, _("Save data"), save_data_message) class NamespacesBrowserFinder(FinderLineEdit): """Textbox for filtering listed variables in the table.""" # To load all variables when filtering. load_all = False def update_parent(self, parent, callback=None, main=None): self._parent = parent self.main = main try: self.textChanged.disconnect() except TypeError: pass if callback: self.textChanged.connect(callback) def load_all_variables(self): """Load all variables to correctly filter them.""" if not self.load_all: self._parent.parent().editor.source_model.load_all() self.load_all = True def keyPressEvent(self, event): """Qt and FilterLineEdit Override.""" key = event.key() if key in [Qt.Key_Up]: self.load_all_variables() self._parent.previous_row() elif key in [Qt.Key_Down]: self.load_all_variables() self._parent.next_row() elif key in [Qt.Key_Escape]: self.main.sig_hide_finder_requested.emit() elif key in [Qt.Key_Enter, Qt.Key_Return]: # TODO: Check if an editor needs to be shown pass else: self.load_all_variables() super(NamespacesBrowserFinder, self).keyPressEvent(event)