1# -*- coding: utf-8 -*- 2# 3# Copyright © Spyder Project Contributors 4# Licensed under the terms of the MIT License 5# (see spyder/__init__.py for details) 6 7""" 8Namespace browser widget 9 10This is the main widget used in the Variable Explorer plugin 11""" 12 13# Standard library imports 14import os.path as osp 15 16# Third library imports (qtpy) 17from qtpy.compat import getsavefilename, getopenfilenames 18from qtpy.QtCore import Qt, Signal, Slot 19from qtpy.QtGui import QCursor 20from qtpy.QtWidgets import (QApplication, QHBoxLayout, QInputDialog, QMenu, 21 QMessageBox, QToolButton, QVBoxLayout, QWidget) 22 23# Third party imports (others) 24import cloudpickle 25 26# Local imports 27from spyder.config.base import _, get_supported_types 28from spyder.config.main import CONF 29from spyder.py3compat import is_text_string, to_text_string 30from spyder.utils import encoding 31from spyder.utils import icon_manager as ima 32from spyder.utils.iofuncs import iofunctions 33from spyder.utils.misc import fix_reference_name, getcwd_or_home 34from spyder.utils.programs import is_module_installed 35from spyder.utils.qthelpers import (add_actions, create_action, 36 create_toolbutton, create_plugin_layout) 37from spyder.widgets.variableexplorer.collectionseditor import ( 38 RemoteCollectionsEditorTableView) 39from spyder.widgets.variableexplorer.importwizard import ImportWizard 40from spyder.widgets.variableexplorer.utils import REMOTE_SETTINGS 41 42 43SUPPORTED_TYPES = get_supported_types() 44 45# To be able to get and set variables between Python 2 and 3 46PICKLE_PROTOCOL = 2 47 48 49class NamespaceBrowser(QWidget): 50 """Namespace browser (global variables explorer widget)""" 51 sig_option_changed = Signal(str, object) 52 sig_collapse = Signal() 53 54 def __init__(self, parent): 55 QWidget.__init__(self, parent) 56 57 self.shellwidget = None 58 self.is_visible = True 59 self.setup_in_progress = None 60 61 # Remote dict editor settings 62 self.check_all = None 63 self.exclude_private = None 64 self.exclude_uppercase = None 65 self.exclude_capitalized = None 66 self.exclude_unsupported = None 67 self.excluded_names = None 68 self.minmax = None 69 70 # Other setting 71 self.dataframe_format = None 72 73 self.editor = None 74 self.exclude_private_action = None 75 self.exclude_uppercase_action = None 76 self.exclude_capitalized_action = None 77 self.exclude_unsupported_action = None 78 79 self.filename = None 80 81 self.var_properties = {} 82 83 def setup(self, check_all=None, exclude_private=None, 84 exclude_uppercase=None, exclude_capitalized=None, 85 exclude_unsupported=None, excluded_names=None, 86 minmax=None, dataframe_format=None): 87 """ 88 Setup the namespace browser with provided settings. 89 90 Args: 91 dataframe_format (string): default floating-point format for 92 DataFrame editor 93 """ 94 assert self.shellwidget is not None 95 96 self.check_all = check_all 97 self.exclude_private = exclude_private 98 self.exclude_uppercase = exclude_uppercase 99 self.exclude_capitalized = exclude_capitalized 100 self.exclude_unsupported = exclude_unsupported 101 self.excluded_names = excluded_names 102 self.minmax = minmax 103 self.dataframe_format = dataframe_format 104 105 if self.editor is not None: 106 self.editor.setup_menu(minmax) 107 self.editor.set_dataframe_format(dataframe_format) 108 self.exclude_private_action.setChecked(exclude_private) 109 self.exclude_uppercase_action.setChecked(exclude_uppercase) 110 self.exclude_capitalized_action.setChecked(exclude_capitalized) 111 self.exclude_unsupported_action.setChecked(exclude_unsupported) 112 self.refresh_table() 113 return 114 115 self.editor = RemoteCollectionsEditorTableView( 116 self, 117 None, 118 minmax=minmax, 119 dataframe_format=dataframe_format, 120 get_value_func=self.get_value, 121 set_value_func=self.set_value, 122 new_value_func=self.set_value, 123 remove_values_func=self.remove_values, 124 copy_value_func=self.copy_value, 125 is_list_func=self.is_list, 126 get_len_func=self.get_len, 127 is_array_func=self.is_array, 128 is_image_func=self.is_image, 129 is_dict_func=self.is_dict, 130 is_data_frame_func=self.is_data_frame, 131 is_series_func=self.is_series, 132 get_array_shape_func=self.get_array_shape, 133 get_array_ndim_func=self.get_array_ndim, 134 plot_func=self.plot, imshow_func=self.imshow, 135 show_image_func=self.show_image) 136 137 self.editor.sig_option_changed.connect(self.sig_option_changed.emit) 138 self.editor.sig_files_dropped.connect(self.import_data) 139 140 # Setup layout 141 blayout = QHBoxLayout() 142 toolbar = self.setup_toolbar(exclude_private, exclude_uppercase, 143 exclude_capitalized, exclude_unsupported) 144 for widget in toolbar: 145 blayout.addWidget(widget) 146 147 # Options menu 148 options_button = create_toolbutton(self, text=_('Options'), 149 icon=ima.icon('tooloptions')) 150 options_button.setPopupMode(QToolButton.InstantPopup) 151 menu = QMenu(self) 152 editor = self.editor 153 actions = [self.exclude_private_action, self.exclude_uppercase_action, 154 self.exclude_capitalized_action, 155 self.exclude_unsupported_action, None] 156 if is_module_installed('numpy'): 157 actions.append(editor.minmax_action) 158 add_actions(menu, actions) 159 options_button.setMenu(menu) 160 161 blayout.addStretch() 162 blayout.addWidget(options_button) 163 164 layout = create_plugin_layout(blayout, self.editor) 165 self.setLayout(layout) 166 167 self.sig_option_changed.connect(self.option_changed) 168 169 def set_shellwidget(self, shellwidget): 170 """Bind shellwidget instance to namespace browser""" 171 self.shellwidget = shellwidget 172 shellwidget.set_namespacebrowser(self) 173 174 def setup_toolbar(self, exclude_private, exclude_uppercase, 175 exclude_capitalized, exclude_unsupported): 176 """Setup toolbar""" 177 self.setup_in_progress = True 178 179 toolbar = [] 180 181 load_button = create_toolbutton(self, text=_('Import data'), 182 icon=ima.icon('fileimport'), 183 triggered=lambda: self.import_data()) 184 self.save_button = create_toolbutton(self, text=_("Save data"), 185 icon=ima.icon('filesave'), 186 triggered=lambda: self.save_data(self.filename)) 187 self.save_button.setEnabled(False) 188 save_as_button = create_toolbutton(self, 189 text=_("Save data as..."), 190 icon=ima.icon('filesaveas'), 191 triggered=self.save_data) 192 reset_namespace_button = create_toolbutton( 193 self, text=_("Remove all variables"), 194 icon=ima.icon('editdelete'), triggered=self.reset_namespace) 195 196 toolbar += [load_button, self.save_button, save_as_button, 197 reset_namespace_button] 198 199 self.exclude_private_action = create_action(self, 200 _("Exclude private references"), 201 tip=_("Exclude references which name starts" 202 " with an underscore"), 203 toggled=lambda state: 204 self.sig_option_changed.emit('exclude_private', state)) 205 self.exclude_private_action.setChecked(exclude_private) 206 207 self.exclude_uppercase_action = create_action(self, 208 _("Exclude all-uppercase references"), 209 tip=_("Exclude references which name is uppercase"), 210 toggled=lambda state: 211 self.sig_option_changed.emit('exclude_uppercase', state)) 212 self.exclude_uppercase_action.setChecked(exclude_uppercase) 213 214 self.exclude_capitalized_action = create_action(self, 215 _("Exclude capitalized references"), 216 tip=_("Exclude references which name starts with an " 217 "uppercase character"), 218 toggled=lambda state: 219 self.sig_option_changed.emit('exclude_capitalized', state)) 220 self.exclude_capitalized_action.setChecked(exclude_capitalized) 221 222 self.exclude_unsupported_action = create_action(self, 223 _("Exclude unsupported data types"), 224 tip=_("Exclude references to unsupported data types" 225 " (i.e. which won't be handled/saved correctly)"), 226 toggled=lambda state: 227 self.sig_option_changed.emit('exclude_unsupported', state)) 228 self.exclude_unsupported_action.setChecked(exclude_unsupported) 229 230 self.setup_in_progress = False 231 232 return toolbar 233 234 def option_changed(self, option, value): 235 """Option has changed""" 236 setattr(self, to_text_string(option), value) 237 self.shellwidget.set_namespace_view_settings() 238 self.refresh_table() 239 240 def visibility_changed(self, enable): 241 """Notify the widget whether its container (the namespace browser 242 plugin is visible or not""" 243 # This is slowing down Spyder a lot if too much data is present in 244 # the Variable Explorer, and users give focus to it after being hidden. 245 # This also happens when the Variable Explorer is visible and users 246 # give focus to Spyder after using another application (like Chrome 247 # or Firefox). 248 # That's why we've decided to remove this feature 249 # Fixes Issue 2593 250 # 251 # self.is_visible = enable 252 # if enable: 253 # self.refresh_table() 254 pass 255 256 def get_view_settings(self): 257 """Return dict editor view settings""" 258 settings = {} 259 for name in REMOTE_SETTINGS: 260 settings[name] = getattr(self, name) 261 return settings 262 263 def refresh_table(self): 264 """Refresh variable table""" 265 if self.is_visible and self.isVisible(): 266 self.shellwidget.refresh_namespacebrowser() 267 try: 268 self.editor.resizeRowToContents() 269 except TypeError: 270 pass 271 272 def process_remote_view(self, remote_view): 273 """Process remote view""" 274 if remote_view is not None: 275 self.set_data(remote_view) 276 277 def set_var_properties(self, properties): 278 """Set properties of variables""" 279 if properties is not None: 280 self.var_properties = properties 281 282 #------ Remote commands ------------------------------------ 283 def get_value(self, name): 284 value = self.shellwidget.get_value(name) 285 286 # Reset temporal variable where value is saved to 287 # save memory 288 self.shellwidget._kernel_value = None 289 return value 290 291 def set_value(self, name, value): 292 """Set value for a variable.""" 293 try: 294 # We need to enclose values in a list to be able to send 295 # them to the kernel in Python 2 296 svalue = [cloudpickle.dumps(value, protocol=PICKLE_PROTOCOL)] 297 self.shellwidget.set_value(name, svalue) 298 except TypeError as e: 299 QMessageBox.critical(self, _("Error"), 300 "TypeError: %s" % to_text_string(e)) 301 self.refresh_table() 302 303 def remove_values(self, names): 304 for name in names: 305 self.shellwidget.remove_value(name) 306 self.refresh_table() 307 308 def copy_value(self, orig_name, new_name): 309 self.shellwidget.copy_value(orig_name, new_name) 310 self.refresh_table() 311 312 def is_list(self, name): 313 """Return True if variable is a list or a tuple""" 314 return self.var_properties[name]['is_list'] 315 316 def is_dict(self, name): 317 """Return True if variable is a dictionary""" 318 return self.var_properties[name]['is_dict'] 319 320 def get_len(self, name): 321 """Return sequence length""" 322 return self.var_properties[name]['len'] 323 324 def is_array(self, name): 325 """Return True if variable is a NumPy array""" 326 return self.var_properties[name]['is_array'] 327 328 def is_image(self, name): 329 """Return True if variable is a PIL.Image image""" 330 return self.var_properties[name]['is_image'] 331 332 def is_data_frame(self, name): 333 """Return True if variable is a DataFrame""" 334 return self.var_properties[name]['is_data_frame'] 335 336 def is_series(self, name): 337 """Return True if variable is a Series""" 338 return self.var_properties[name]['is_series'] 339 340 def get_array_shape(self, name): 341 """Return array's shape""" 342 return self.var_properties[name]['array_shape'] 343 344 def get_array_ndim(self, name): 345 """Return array's ndim""" 346 return self.var_properties[name]['array_ndim'] 347 348 def plot(self, name, funcname): 349 sw = self.shellwidget 350 if sw._reading: 351 sw.dbg_exec_magic('varexp', '--%s %s' % (funcname, name)) 352 else: 353 sw.execute("%%varexp --%s %s" % (funcname, name)) 354 355 def imshow(self, name): 356 sw = self.shellwidget 357 if sw._reading: 358 sw.dbg_exec_magic('varexp', '--imshow %s' % name) 359 else: 360 sw.execute("%%varexp --imshow %s" % name) 361 362 def show_image(self, name): 363 command = "%s.show()" % name 364 sw = self.shellwidget 365 if sw._reading: 366 sw.kernel_client.input(command) 367 else: 368 sw.execute(command) 369 370 # ------ Set, load and save data ------------------------------------------ 371 def set_data(self, data): 372 """Set data.""" 373 if data != self.editor.model.get_data(): 374 self.editor.set_data(data) 375 self.editor.adjust_columns() 376 377 def collapse(self): 378 """Collapse.""" 379 self.sig_collapse.emit() 380 381 @Slot(bool) 382 @Slot(list) 383 def import_data(self, filenames=None): 384 """Import data from text file.""" 385 title = _("Import data") 386 if filenames is None: 387 if self.filename is None: 388 basedir = getcwd_or_home() 389 else: 390 basedir = osp.dirname(self.filename) 391 filenames, _selfilter = getopenfilenames(self, title, basedir, 392 iofunctions.load_filters) 393 if not filenames: 394 return 395 elif is_text_string(filenames): 396 filenames = [filenames] 397 398 for filename in filenames: 399 self.filename = to_text_string(filename) 400 ext = osp.splitext(self.filename)[1].lower() 401 402 if ext not in iofunctions.load_funcs: 403 buttons = QMessageBox.Yes | QMessageBox.Cancel 404 answer = QMessageBox.question(self, title, 405 _("<b>Unsupported file extension '%s'</b><br><br>" 406 "Would you like to import it anyway " 407 "(by selecting a known file format)?" 408 ) % ext, buttons) 409 if answer == QMessageBox.Cancel: 410 return 411 formats = list(iofunctions.load_extensions.keys()) 412 item, ok = QInputDialog.getItem(self, title, 413 _('Open file as:'), 414 formats, 0, False) 415 if ok: 416 ext = iofunctions.load_extensions[to_text_string(item)] 417 else: 418 return 419 420 load_func = iofunctions.load_funcs[ext] 421 422 # 'import_wizard' (self.setup_io) 423 if is_text_string(load_func): 424 # Import data with import wizard 425 error_message = None 426 try: 427 text, _encoding = encoding.read(self.filename) 428 base_name = osp.basename(self.filename) 429 editor = ImportWizard(self, text, title=base_name, 430 varname=fix_reference_name(base_name)) 431 if editor.exec_(): 432 var_name, clip_data = editor.get_data() 433 self.set_value(var_name, clip_data) 434 except Exception as error: 435 error_message = str(error) 436 else: 437 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) 438 QApplication.processEvents() 439 error_message = self.shellwidget.load_data(self.filename, 440 ext) 441 self.shellwidget._kernel_reply = None 442 QApplication.restoreOverrideCursor() 443 QApplication.processEvents() 444 445 if error_message is not None: 446 QMessageBox.critical(self, title, 447 _("<b>Unable to load '%s'</b>" 448 "<br><br>Error message:<br>%s" 449 ) % (self.filename, error_message)) 450 self.refresh_table() 451 452 @Slot() 453 def reset_namespace(self): 454 warning = CONF.get('ipython_console', 'show_reset_namespace_warning') 455 self.shellwidget.reset_namespace(warning=warning, silent=True, 456 message=True) 457 458 @Slot() 459 def save_data(self, filename=None): 460 """Save data""" 461 if filename is None: 462 filename = self.filename 463 if filename is None: 464 filename = getcwd_or_home() 465 filename, _selfilter = getsavefilename(self, _("Save data"), 466 filename, 467 iofunctions.save_filters) 468 if filename: 469 self.filename = filename 470 else: 471 return False 472 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) 473 QApplication.processEvents() 474 error_message = self.shellwidget.save_namespace(self.filename) 475 self.shellwidget._kernel_reply = None 476 QApplication.restoreOverrideCursor() 477 QApplication.processEvents() 478 if error_message is not None: 479 QMessageBox.critical(self, _("Save data"), 480 _("<b>Unable to save current workspace</b>" 481 "<br><br>Error message:<br>%s") % error_message) 482 self.save_button.setEnabled(self.filename is not None) 483