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""" 8Collections (i.e. dictionary, list and tuple) editor widget and dialog 9""" 10 11#TODO: Multiple selection: open as many editors (array/dict/...) as necessary, 12# at the same time 13 14# pylint: disable=C0103 15# pylint: disable=R0903 16# pylint: disable=R0911 17# pylint: disable=R0201 18 19# Standard library imports 20from __future__ import print_function 21import datetime 22import gc 23import sys 24import warnings 25 26# Third party imports 27from qtpy.compat import getsavefilename, to_qvariant 28from qtpy.QtCore import (QAbstractTableModel, QDateTime, QModelIndex, Qt, 29 Signal, Slot) 30from qtpy.QtGui import QColor, QKeySequence 31from qtpy.QtWidgets import (QAbstractItemDelegate, QApplication, QDateEdit, 32 QDateTimeEdit, QDialog, QDialogButtonBox, 33 QInputDialog, QItemDelegate, QLineEdit, QMenu, 34 QMessageBox, QTableView, QVBoxLayout, QWidget) 35 36# Local import 37from spyder.config.base import _ 38from spyder.config.fonts import DEFAULT_SMALL_DELTA 39from spyder.config.gui import get_font 40from spyder.py3compat import (io, is_binary_string, is_text_string, 41 PY3, to_text_string) 42from spyder.utils import icon_manager as ima 43from spyder.utils.misc import fix_reference_name, getcwd_or_home 44from spyder.utils.qthelpers import (add_actions, create_action, 45 mimedata2url) 46from spyder.widgets.variableexplorer.importwizard import ImportWizard 47from spyder.widgets.variableexplorer.texteditor import TextEditor 48from spyder.widgets.variableexplorer.utils import ( 49 array, DataFrame, DatetimeIndex, display_to_value, FakeObject, 50 get_color_name, get_human_readable_type, get_size, Image, is_editable_type, 51 is_known_type, MaskedArray, ndarray, np_savetxt, Series, sort_against, 52 try_to_eval, unsorted_unique, value_to_display, get_object_attrs, 53 get_type_string) 54 55if ndarray is not FakeObject: 56 from spyder.widgets.variableexplorer.arrayeditor import ArrayEditor 57 58if DataFrame is not FakeObject: 59 from spyder.widgets.variableexplorer.dataframeeditor import DataFrameEditor 60 61 62LARGE_NROWS = 100 63ROWS_TO_LOAD = 50 64 65class ProxyObject(object): 66 """Dictionary proxy to an unknown object.""" 67 68 def __init__(self, obj): 69 """Constructor.""" 70 self.__obj__ = obj 71 72 def __len__(self): 73 """Get len according to detected attributes.""" 74 return len(get_object_attrs(self.__obj__)) 75 76 def __getitem__(self, key): 77 """Get attribute corresponding to key.""" 78 return getattr(self.__obj__, key) 79 80 def __setitem__(self, key, value): 81 """Set attribute corresponding to key with value.""" 82 try: 83 setattr(self.__obj__, key, value) 84 except TypeError: 85 pass 86 87 88class ReadOnlyCollectionsModel(QAbstractTableModel): 89 """CollectionsEditor Read-Only Table Model""" 90 91 def __init__(self, parent, data, title="", names=False, 92 minmax=False, dataframe_format=None, remote=False): 93 QAbstractTableModel.__init__(self, parent) 94 if data is None: 95 data = {} 96 self.names = names 97 self.minmax = minmax 98 self.dataframe_format = dataframe_format 99 self.remote = remote 100 self.header0 = None 101 self._data = None 102 self.total_rows = None 103 self.showndata = None 104 self.keys = None 105 self.title = to_text_string(title) # in case title is not a string 106 if self.title: 107 self.title = self.title + ' - ' 108 self.sizes = [] 109 self.types = [] 110 self.set_data(data) 111 112 def get_data(self): 113 """Return model data""" 114 return self._data 115 116 def set_data(self, data, coll_filter=None): 117 """Set model data""" 118 self._data = data 119 data_type = get_type_string(data) 120 121 if coll_filter is not None and not self.remote and \ 122 isinstance(data, (tuple, list, dict)): 123 data = coll_filter(data) 124 self.showndata = data 125 126 self.header0 = _("Index") 127 if self.names: 128 self.header0 = _("Name") 129 if isinstance(data, tuple): 130 self.keys = list(range(len(data))) 131 self.title += _("Tuple") 132 elif isinstance(data, list): 133 self.keys = list(range(len(data))) 134 self.title += _("List") 135 elif isinstance(data, dict): 136 self.keys = list(data.keys()) 137 self.title += _("Dictionary") 138 if not self.names: 139 self.header0 = _("Key") 140 else: 141 self.keys = get_object_attrs(data) 142 self._data = data = self.showndata = ProxyObject(data) 143 if not self.names: 144 self.header0 = _("Attribute") 145 146 if not isinstance(self._data, ProxyObject): 147 self.title += (' (' + str(len(self.keys)) + ' ' + 148 _("elements") + ')') 149 else: 150 self.title += data_type 151 152 self.total_rows = len(self.keys) 153 if self.total_rows > LARGE_NROWS: 154 self.rows_loaded = ROWS_TO_LOAD 155 else: 156 self.rows_loaded = self.total_rows 157 158 self.set_size_and_type() 159 self.reset() 160 161 def set_size_and_type(self, start=None, stop=None): 162 data = self._data 163 164 if start is None and stop is None: 165 start = 0 166 stop = self.rows_loaded 167 fetch_more = False 168 else: 169 fetch_more = True 170 171 if self.remote: 172 sizes = [ data[self.keys[index]]['size'] 173 for index in range(start, stop) ] 174 types = [ data[self.keys[index]]['type'] 175 for index in range(start, stop) ] 176 else: 177 sizes = [ get_size(data[self.keys[index]]) 178 for index in range(start, stop) ] 179 types = [ get_human_readable_type(data[self.keys[index]]) 180 for index in range(start, stop) ] 181 182 if fetch_more: 183 self.sizes = self.sizes + sizes 184 self.types = self.types + types 185 else: 186 self.sizes = sizes 187 self.types = types 188 189 def sort(self, column, order=Qt.AscendingOrder): 190 """Overriding sort method""" 191 reverse = (order==Qt.DescendingOrder) 192 if column == 0: 193 self.sizes = sort_against(self.sizes, self.keys, reverse) 194 self.types = sort_against(self.types, self.keys, reverse) 195 try: 196 self.keys.sort(reverse=reverse) 197 except: 198 pass 199 elif column == 1: 200 self.keys[:self.rows_loaded] = sort_against(self.keys, self.types, 201 reverse) 202 self.sizes = sort_against(self.sizes, self.types, reverse) 203 try: 204 self.types.sort(reverse=reverse) 205 except: 206 pass 207 elif column == 2: 208 self.keys[:self.rows_loaded] = sort_against(self.keys, self.sizes, 209 reverse) 210 self.types = sort_against(self.types, self.sizes, reverse) 211 try: 212 self.sizes.sort(reverse=reverse) 213 except: 214 pass 215 elif column == 3: 216 values = [self._data[key] for key in self.keys] 217 self.keys = sort_against(self.keys, values, reverse) 218 self.sizes = sort_against(self.sizes, values, reverse) 219 self.types = sort_against(self.types, values, reverse) 220 self.beginResetModel() 221 self.endResetModel() 222 223 def columnCount(self, qindex=QModelIndex()): 224 """Array column number""" 225 return 4 226 227 def rowCount(self, index=QModelIndex()): 228 """Array row number""" 229 if self.total_rows <= self.rows_loaded: 230 return self.total_rows 231 else: 232 return self.rows_loaded 233 234 def canFetchMore(self, index=QModelIndex()): 235 if self.total_rows > self.rows_loaded: 236 return True 237 else: 238 return False 239 240 def fetchMore(self, index=QModelIndex()): 241 reminder = self.total_rows - self.rows_loaded 242 items_to_fetch = min(reminder, ROWS_TO_LOAD) 243 self.set_size_and_type(self.rows_loaded, 244 self.rows_loaded + items_to_fetch) 245 self.beginInsertRows(QModelIndex(), self.rows_loaded, 246 self.rows_loaded + items_to_fetch - 1) 247 self.rows_loaded += items_to_fetch 248 self.endInsertRows() 249 250 def get_index_from_key(self, key): 251 try: 252 return self.createIndex(self.keys.index(key), 0) 253 except (RuntimeError, ValueError): 254 return QModelIndex() 255 256 def get_key(self, index): 257 """Return current key""" 258 return self.keys[index.row()] 259 260 def get_value(self, index): 261 """Return current value""" 262 if index.column() == 0: 263 return self.keys[ index.row() ] 264 elif index.column() == 1: 265 return self.types[ index.row() ] 266 elif index.column() == 2: 267 return self.sizes[ index.row() ] 268 else: 269 return self._data[ self.keys[index.row()] ] 270 271 def get_bgcolor(self, index): 272 """Background color depending on value""" 273 if index.column() == 0: 274 color = QColor(Qt.lightGray) 275 color.setAlphaF(.05) 276 elif index.column() < 3: 277 color = QColor(Qt.lightGray) 278 color.setAlphaF(.2) 279 else: 280 color = QColor(Qt.lightGray) 281 color.setAlphaF(.3) 282 return color 283 284 def data(self, index, role=Qt.DisplayRole): 285 """Cell content""" 286 if not index.isValid(): 287 return to_qvariant() 288 value = self.get_value(index) 289 if index.column() == 3 and self.remote: 290 value = value['view'] 291 if index.column() == 3: 292 display = value_to_display(value, minmax=self.minmax) 293 else: 294 display = to_text_string(value) 295 if role == Qt.DisplayRole: 296 return to_qvariant(display) 297 elif role == Qt.EditRole: 298 return to_qvariant(value_to_display(value)) 299 elif role == Qt.TextAlignmentRole: 300 if index.column() == 3: 301 if len(display.splitlines()) < 3: 302 return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter)) 303 else: 304 return to_qvariant(int(Qt.AlignLeft|Qt.AlignTop)) 305 else: 306 return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter)) 307 elif role == Qt.BackgroundColorRole: 308 return to_qvariant( self.get_bgcolor(index) ) 309 elif role == Qt.FontRole: 310 return to_qvariant(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) 311 return to_qvariant() 312 313 def headerData(self, section, orientation, role=Qt.DisplayRole): 314 """Overriding method headerData""" 315 if role != Qt.DisplayRole: 316 return to_qvariant() 317 i_column = int(section) 318 if orientation == Qt.Horizontal: 319 headers = (self.header0, _("Type"), _("Size"), _("Value")) 320 return to_qvariant( headers[i_column] ) 321 else: 322 return to_qvariant() 323 324 def flags(self, index): 325 """Overriding method flags""" 326 # This method was implemented in CollectionsModel only, but to enable 327 # tuple exploration (even without editing), this method was moved here 328 if not index.isValid(): 329 return Qt.ItemIsEnabled 330 return Qt.ItemFlags(QAbstractTableModel.flags(self, index)| 331 Qt.ItemIsEditable) 332 def reset(self): 333 self.beginResetModel() 334 self.endResetModel() 335 336 337class CollectionsModel(ReadOnlyCollectionsModel): 338 """Collections Table Model""" 339 340 def set_value(self, index, value): 341 """Set value""" 342 self._data[ self.keys[index.row()] ] = value 343 self.showndata[ self.keys[index.row()] ] = value 344 self.sizes[index.row()] = get_size(value) 345 self.types[index.row()] = get_human_readable_type(value) 346 347 def get_bgcolor(self, index): 348 """Background color depending on value""" 349 value = self.get_value(index) 350 if index.column() < 3: 351 color = ReadOnlyCollectionsModel.get_bgcolor(self, index) 352 else: 353 if self.remote: 354 color_name = value['color'] 355 else: 356 color_name = get_color_name(value) 357 color = QColor(color_name) 358 color.setAlphaF(.2) 359 return color 360 361 def setData(self, index, value, role=Qt.EditRole): 362 """Cell content change""" 363 if not index.isValid(): 364 return False 365 if index.column() < 3: 366 return False 367 value = display_to_value(value, self.get_value(index), 368 ignore_errors=True) 369 self.set_value(index, value) 370 self.dataChanged.emit(index, index) 371 return True 372 373 374class CollectionsDelegate(QItemDelegate): 375 """CollectionsEditor Item Delegate""" 376 377 def __init__(self, parent=None): 378 QItemDelegate.__init__(self, parent) 379 self._editors = {} # keep references on opened editors 380 381 def get_value(self, index): 382 if index.isValid(): 383 return index.model().get_value(index) 384 385 def set_value(self, index, value): 386 if index.isValid(): 387 index.model().set_value(index, value) 388 389 def show_warning(self, index): 390 """ 391 Decide if showing a warning when the user is trying to view 392 a big variable associated to a Tablemodel index 393 394 This avoids getting the variables' value to know its 395 size and type, using instead those already computed by 396 the TableModel. 397 398 The problem is when a variable is too big, it can take a 399 lot of time just to get its value 400 """ 401 try: 402 val_size = index.model().sizes[index.row()] 403 val_type = index.model().types[index.row()] 404 except: 405 return False 406 if val_type in ['list', 'tuple', 'dict'] and int(val_size) > 1e5: 407 return True 408 else: 409 return False 410 411 def createEditor(self, parent, option, index): 412 """Overriding method createEditor""" 413 if index.column() < 3: 414 return None 415 if self.show_warning(index): 416 answer = QMessageBox.warning(self.parent(), _("Warning"), 417 _("Opening this variable can be slow\n\n" 418 "Do you want to continue anyway?"), 419 QMessageBox.Yes | QMessageBox.No) 420 if answer == QMessageBox.No: 421 return None 422 try: 423 value = self.get_value(index) 424 if value is None: 425 return None 426 except Exception as msg: 427 QMessageBox.critical(self.parent(), _("Error"), 428 _("Spyder was unable to retrieve the value of " 429 "this variable from the console.<br><br>" 430 "The error mesage was:<br>" 431 "<i>%s</i>" 432 ) % to_text_string(msg)) 433 return 434 key = index.model().get_key(index) 435 readonly = isinstance(value, tuple) or self.parent().readonly \ 436 or not is_known_type(value) 437 #---editor = CollectionsEditor 438 if isinstance(value, (list, tuple, dict)): 439 editor = CollectionsEditor() 440 editor.setup(value, key, icon=self.parent().windowIcon(), 441 readonly=readonly) 442 self.create_dialog(editor, dict(model=index.model(), editor=editor, 443 key=key, readonly=readonly)) 444 return None 445 #---editor = ArrayEditor 446 elif isinstance(value, (ndarray, MaskedArray)) \ 447 and ndarray is not FakeObject: 448 editor = ArrayEditor(parent) 449 if not editor.setup_and_check(value, title=key, readonly=readonly): 450 return 451 self.create_dialog(editor, dict(model=index.model(), editor=editor, 452 key=key, readonly=readonly)) 453 return None 454 #---showing image 455 elif isinstance(value, Image) and ndarray is not FakeObject \ 456 and Image is not FakeObject: 457 arr = array(value) 458 editor = ArrayEditor(parent) 459 if not editor.setup_and_check(arr, title=key, readonly=readonly): 460 return 461 conv_func = lambda arr: Image.fromarray(arr, mode=value.mode) 462 self.create_dialog(editor, dict(model=index.model(), editor=editor, 463 key=key, readonly=readonly, 464 conv=conv_func)) 465 return None 466 #--editor = DataFrameEditor 467 elif isinstance(value, (DataFrame, DatetimeIndex, Series)) \ 468 and DataFrame is not FakeObject: 469 editor = DataFrameEditor() 470 if not editor.setup_and_check(value, title=key): 471 return 472 editor.dataModel.set_format(index.model().dataframe_format) 473 editor.sig_option_changed.connect(self.change_option) 474 self.create_dialog(editor, dict(model=index.model(), editor=editor, 475 key=key, readonly=readonly)) 476 return None 477 #---editor = QDateEdit or QDateTimeEdit 478 elif isinstance(value, datetime.date): 479 if readonly: 480 return None 481 else: 482 if isinstance(value, datetime.datetime): 483 editor = QDateTimeEdit(value, parent) 484 else: 485 editor = QDateEdit(value, parent) 486 editor.setCalendarPopup(True) 487 editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) 488 return editor 489 #---editor = TextEditor 490 elif is_text_string(value) and len(value) > 40: 491 te = TextEditor(None) 492 if te.setup_and_check(value): 493 editor = TextEditor(value, key, readonly=readonly) 494 self.create_dialog(editor, dict(model=index.model(), 495 editor=editor, key=key, 496 readonly=readonly)) 497 return None 498 #---editor = QLineEdit 499 elif is_editable_type(value): 500 if readonly: 501 return None 502 else: 503 editor = QLineEdit(parent) 504 editor.setFont(get_font(font_size_delta=DEFAULT_SMALL_DELTA)) 505 editor.setAlignment(Qt.AlignLeft) 506 # This is making Spyder crash because the QLineEdit that it's 507 # been modified is removed and a new one is created after 508 # evaluation. So the object on which this method is trying to 509 # act doesn't exist anymore. 510 # editor.returnPressed.connect(self.commitAndCloseEditor) 511 return editor 512 #---editor = CollectionsEditor for an arbitrary object 513 else: 514 editor = CollectionsEditor() 515 editor.setup(value, key, icon=self.parent().windowIcon(), 516 readonly=readonly) 517 self.create_dialog(editor, dict(model=index.model(), editor=editor, 518 key=key, readonly=readonly)) 519 return None 520 521 def create_dialog(self, editor, data): 522 self._editors[id(editor)] = data 523 editor.accepted.connect( 524 lambda eid=id(editor): self.editor_accepted(eid)) 525 editor.rejected.connect( 526 lambda eid=id(editor): self.editor_rejected(eid)) 527 editor.show() 528 529 @Slot(str, object) 530 def change_option(self, option_name, new_value): 531 """ 532 Change configuration option. 533 534 This function is called when a `sig_option_changed` signal is received. 535 At the moment, this signal can only come from a DataFrameEditor. 536 """ 537 if option_name == 'dataframe_format': 538 self.parent().set_dataframe_format(new_value) 539 540 def editor_accepted(self, editor_id): 541 data = self._editors[editor_id] 542 if not data['readonly']: 543 index = data['model'].get_index_from_key(data['key']) 544 value = data['editor'].get_value() 545 conv_func = data.get('conv', lambda v: v) 546 self.set_value(index, conv_func(value)) 547 self._editors.pop(editor_id) 548 self.free_memory() 549 550 def editor_rejected(self, editor_id): 551 self._editors.pop(editor_id) 552 self.free_memory() 553 554 def free_memory(self): 555 """Free memory after closing an editor.""" 556 gc.collect() 557 558 def commitAndCloseEditor(self): 559 """Overriding method commitAndCloseEditor""" 560 editor = self.sender() 561 # Avoid a segfault with PyQt5. Variable value won't be changed 562 # but at least Spyder won't crash. It seems generated by a 563 # bug in sip. See 564 # http://comments.gmane.org/gmane.comp.python.pyqt-pykde/26544 565 try: 566 self.commitData.emit(editor) 567 except AttributeError: 568 pass 569 self.closeEditor.emit(editor, QAbstractItemDelegate.NoHint) 570 571 def setEditorData(self, editor, index): 572 """ 573 Overriding method setEditorData 574 Model --> Editor 575 """ 576 value = self.get_value(index) 577 if isinstance(editor, QLineEdit): 578 if is_binary_string(value): 579 try: 580 value = to_text_string(value, 'utf8') 581 except: 582 pass 583 if not is_text_string(value): 584 value = repr(value) 585 editor.setText(value) 586 elif isinstance(editor, QDateEdit): 587 editor.setDate(value) 588 elif isinstance(editor, QDateTimeEdit): 589 editor.setDateTime(QDateTime(value.date(), value.time())) 590 591 def setModelData(self, editor, model, index): 592 """ 593 Overriding method setModelData 594 Editor --> Model 595 """ 596 if not hasattr(model, "set_value"): 597 # Read-only mode 598 return 599 600 if isinstance(editor, QLineEdit): 601 value = editor.text() 602 try: 603 value = display_to_value(to_qvariant(value), 604 self.get_value(index), 605 ignore_errors=False) 606 except Exception as msg: 607 raise 608 QMessageBox.critical(editor, _("Edit item"), 609 _("<b>Unable to assign data to item.</b>" 610 "<br><br>Error message:<br>%s" 611 ) % str(msg)) 612 return 613 elif isinstance(editor, QDateEdit): 614 qdate = editor.date() 615 value = datetime.date( qdate.year(), qdate.month(), qdate.day() ) 616 elif isinstance(editor, QDateTimeEdit): 617 qdatetime = editor.dateTime() 618 qdate = qdatetime.date() 619 qtime = qdatetime.time() 620 value = datetime.datetime( qdate.year(), qdate.month(), 621 qdate.day(), qtime.hour(), 622 qtime.minute(), qtime.second() ) 623 else: 624 # Should not happen... 625 raise RuntimeError("Unsupported editor widget") 626 self.set_value(index, value) 627 628 629class BaseTableView(QTableView): 630 """Base collection editor table view""" 631 sig_option_changed = Signal(str, object) 632 sig_files_dropped = Signal(list) 633 redirect_stdio = Signal(bool) 634 635 def __init__(self, parent): 636 QTableView.__init__(self, parent) 637 self.array_filename = None 638 self.menu = None 639 self.empty_ws_menu = None 640 self.paste_action = None 641 self.copy_action = None 642 self.edit_action = None 643 self.plot_action = None 644 self.hist_action = None 645 self.imshow_action = None 646 self.save_array_action = None 647 self.insert_action = None 648 self.remove_action = None 649 self.minmax_action = None 650 self.rename_action = None 651 self.duplicate_action = None 652 self.delegate = None 653 self.setAcceptDrops(True) 654 655 def setup_table(self): 656 """Setup table""" 657 self.horizontalHeader().setStretchLastSection(True) 658 self.adjust_columns() 659 # Sorting columns 660 self.setSortingEnabled(True) 661 self.sortByColumn(0, Qt.AscendingOrder) 662 663 def setup_menu(self, minmax): 664 """Setup context menu""" 665 if self.minmax_action is not None: 666 self.minmax_action.setChecked(minmax) 667 return 668 669 resize_action = create_action(self, _("Resize rows to contents"), 670 triggered=self.resizeRowsToContents) 671 self.paste_action = create_action(self, _("Paste"), 672 icon=ima.icon('editpaste'), 673 triggered=self.paste) 674 self.copy_action = create_action(self, _("Copy"), 675 icon=ima.icon('editcopy'), 676 triggered=self.copy) 677 self.edit_action = create_action(self, _("Edit"), 678 icon=ima.icon('edit'), 679 triggered=self.edit_item) 680 self.plot_action = create_action(self, _("Plot"), 681 icon=ima.icon('plot'), 682 triggered=lambda: self.plot_item('plot')) 683 self.plot_action.setVisible(False) 684 self.hist_action = create_action(self, _("Histogram"), 685 icon=ima.icon('hist'), 686 triggered=lambda: self.plot_item('hist')) 687 self.hist_action.setVisible(False) 688 self.imshow_action = create_action(self, _("Show image"), 689 icon=ima.icon('imshow'), 690 triggered=self.imshow_item) 691 self.imshow_action.setVisible(False) 692 self.save_array_action = create_action(self, _("Save array"), 693 icon=ima.icon('filesave'), 694 triggered=self.save_array) 695 self.save_array_action.setVisible(False) 696 self.insert_action = create_action(self, _("Insert"), 697 icon=ima.icon('insert'), 698 triggered=self.insert_item) 699 self.remove_action = create_action(self, _("Remove"), 700 icon=ima.icon('editdelete'), 701 triggered=self.remove_item) 702 self.minmax_action = create_action(self, _("Show arrays min/max"), 703 toggled=self.toggle_minmax) 704 self.minmax_action.setChecked(minmax) 705 self.toggle_minmax(minmax) 706 self.rename_action = create_action(self, _("Rename"), 707 icon=ima.icon('rename'), 708 triggered=self.rename_item) 709 self.duplicate_action = create_action(self, _("Duplicate"), 710 icon=ima.icon('edit_add'), 711 triggered=self.duplicate_item) 712 menu = QMenu(self) 713 menu_actions = [self.edit_action, self.plot_action, self.hist_action, 714 self.imshow_action, self.save_array_action, 715 self.insert_action, self.remove_action, 716 self.copy_action, self.paste_action, 717 None, self.rename_action, self.duplicate_action, 718 None, resize_action] 719 if ndarray is not FakeObject: 720 menu_actions.append(self.minmax_action) 721 add_actions(menu, menu_actions) 722 self.empty_ws_menu = QMenu(self) 723 add_actions(self.empty_ws_menu, 724 [self.insert_action, self.paste_action, 725 None, resize_action]) 726 return menu 727 728 #------ Remote/local API --------------------------------------------------- 729 def remove_values(self, keys): 730 """Remove values from data""" 731 raise NotImplementedError 732 733 def copy_value(self, orig_key, new_key): 734 """Copy value""" 735 raise NotImplementedError 736 737 def new_value(self, key, value): 738 """Create new value in data""" 739 raise NotImplementedError 740 741 def is_list(self, key): 742 """Return True if variable is a list or a tuple""" 743 raise NotImplementedError 744 745 def get_len(self, key): 746 """Return sequence length""" 747 raise NotImplementedError 748 749 def is_array(self, key): 750 """Return True if variable is a numpy array""" 751 raise NotImplementedError 752 753 def is_image(self, key): 754 """Return True if variable is a PIL.Image image""" 755 raise NotImplementedError 756 757 def is_dict(self, key): 758 """Return True if variable is a dictionary""" 759 raise NotImplementedError 760 761 def get_array_shape(self, key): 762 """Return array's shape""" 763 raise NotImplementedError 764 765 def get_array_ndim(self, key): 766 """Return array's ndim""" 767 raise NotImplementedError 768 769 def oedit(self, key): 770 """Edit item""" 771 raise NotImplementedError 772 773 def plot(self, key, funcname): 774 """Plot item""" 775 raise NotImplementedError 776 777 def imshow(self, key): 778 """Show item's image""" 779 raise NotImplementedError 780 781 def show_image(self, key): 782 """Show image (item is a PIL image)""" 783 raise NotImplementedError 784 #--------------------------------------------------------------------------- 785 786 def refresh_menu(self): 787 """Refresh context menu""" 788 index = self.currentIndex() 789 condition = index.isValid() 790 self.edit_action.setEnabled( condition ) 791 self.remove_action.setEnabled( condition ) 792 self.refresh_plot_entries(index) 793 794 def refresh_plot_entries(self, index): 795 if index.isValid(): 796 key = self.model.get_key(index) 797 is_list = self.is_list(key) 798 is_array = self.is_array(key) and self.get_len(key) != 0 799 condition_plot = (is_array and len(self.get_array_shape(key)) <= 2) 800 condition_hist = (is_array and self.get_array_ndim(key) == 1) 801 condition_imshow = condition_plot and self.get_array_ndim(key) == 2 802 condition_imshow = condition_imshow or self.is_image(key) 803 else: 804 is_array = condition_plot = condition_imshow = is_list \ 805 = condition_hist = False 806 self.plot_action.setVisible(condition_plot or is_list) 807 self.hist_action.setVisible(condition_hist or is_list) 808 self.imshow_action.setVisible(condition_imshow) 809 self.save_array_action.setVisible(is_array) 810 811 def adjust_columns(self): 812 """Resize two first columns to contents""" 813 for col in range(3): 814 self.resizeColumnToContents(col) 815 816 def set_data(self, data): 817 """Set table data""" 818 if data is not None: 819 self.model.set_data(data, self.dictfilter) 820 self.sortByColumn(0, Qt.AscendingOrder) 821 822 def mousePressEvent(self, event): 823 """Reimplement Qt method""" 824 if event.button() != Qt.LeftButton: 825 QTableView.mousePressEvent(self, event) 826 return 827 index_clicked = self.indexAt(event.pos()) 828 if index_clicked.isValid(): 829 if index_clicked == self.currentIndex() \ 830 and index_clicked in self.selectedIndexes(): 831 self.clearSelection() 832 else: 833 QTableView.mousePressEvent(self, event) 834 else: 835 self.clearSelection() 836 event.accept() 837 838 def mouseDoubleClickEvent(self, event): 839 """Reimplement Qt method""" 840 index_clicked = self.indexAt(event.pos()) 841 if index_clicked.isValid(): 842 row = index_clicked.row() 843 # TODO: Remove hard coded "Value" column number (3 here) 844 index_clicked = index_clicked.child(row, 3) 845 self.edit(index_clicked) 846 else: 847 event.accept() 848 849 def keyPressEvent(self, event): 850 """Reimplement Qt methods""" 851 if event.key() == Qt.Key_Delete: 852 self.remove_item() 853 elif event.key() == Qt.Key_F2: 854 self.rename_item() 855 elif event == QKeySequence.Copy: 856 self.copy() 857 elif event == QKeySequence.Paste: 858 self.paste() 859 else: 860 QTableView.keyPressEvent(self, event) 861 862 def contextMenuEvent(self, event): 863 """Reimplement Qt method""" 864 if self.model.showndata: 865 self.refresh_menu() 866 self.menu.popup(event.globalPos()) 867 event.accept() 868 else: 869 self.empty_ws_menu.popup(event.globalPos()) 870 event.accept() 871 872 def dragEnterEvent(self, event): 873 """Allow user to drag files""" 874 if mimedata2url(event.mimeData()): 875 event.accept() 876 else: 877 event.ignore() 878 879 def dragMoveEvent(self, event): 880 """Allow user to move files""" 881 if mimedata2url(event.mimeData()): 882 event.setDropAction(Qt.CopyAction) 883 event.accept() 884 else: 885 event.ignore() 886 887 def dropEvent(self, event): 888 """Allow user to drop supported files""" 889 urls = mimedata2url(event.mimeData()) 890 if urls: 891 event.setDropAction(Qt.CopyAction) 892 event.accept() 893 self.sig_files_dropped.emit(urls) 894 else: 895 event.ignore() 896 897 @Slot(bool) 898 def toggle_minmax(self, state): 899 """Toggle min/max display for numpy arrays""" 900 self.sig_option_changed.emit('minmax', state) 901 self.model.minmax = state 902 903 @Slot(str) 904 def set_dataframe_format(self, new_format): 905 """ 906 Set format to use in DataframeEditor. 907 908 Args: 909 new_format (string): e.g. "%.3f" 910 """ 911 self.sig_option_changed.emit('dataframe_format', new_format) 912 self.model.dataframe_format = new_format 913 914 @Slot() 915 def edit_item(self): 916 """Edit item""" 917 index = self.currentIndex() 918 if not index.isValid(): 919 return 920 # TODO: Remove hard coded "Value" column number (3 here) 921 self.edit(index.child(index.row(), 3)) 922 923 @Slot() 924 def remove_item(self): 925 """Remove item""" 926 indexes = self.selectedIndexes() 927 if not indexes: 928 return 929 for index in indexes: 930 if not index.isValid(): 931 return 932 one = _("Do you want to remove the selected item?") 933 more = _("Do you want to remove all selected items?") 934 answer = QMessageBox.question(self, _( "Remove"), 935 one if len(indexes) == 1 else more, 936 QMessageBox.Yes | QMessageBox.No) 937 if answer == QMessageBox.Yes: 938 idx_rows = unsorted_unique([idx.row() for idx in indexes]) 939 keys = [ self.model.keys[idx_row] for idx_row in idx_rows ] 940 self.remove_values(keys) 941 942 def copy_item(self, erase_original=False): 943 """Copy item""" 944 indexes = self.selectedIndexes() 945 if not indexes: 946 return 947 idx_rows = unsorted_unique([idx.row() for idx in indexes]) 948 if len(idx_rows) > 1 or not indexes[0].isValid(): 949 return 950 orig_key = self.model.keys[idx_rows[0]] 951 if erase_original: 952 title = _('Rename') 953 field_text = _('New variable name:') 954 else: 955 title = _('Duplicate') 956 field_text = _('Variable name:') 957 data = self.model.get_data() 958 if isinstance(data, (list, set)): 959 new_key, valid = len(data), True 960 else: 961 new_key, valid = QInputDialog.getText(self, title, field_text, 962 QLineEdit.Normal, orig_key) 963 if valid and to_text_string(new_key): 964 new_key = try_to_eval(to_text_string(new_key)) 965 if new_key == orig_key: 966 return 967 self.copy_value(orig_key, new_key) 968 if erase_original: 969 self.remove_values([orig_key]) 970 971 @Slot() 972 def duplicate_item(self): 973 """Duplicate item""" 974 self.copy_item() 975 976 @Slot() 977 def rename_item(self): 978 """Rename item""" 979 self.copy_item(True) 980 981 @Slot() 982 def insert_item(self): 983 """Insert item""" 984 index = self.currentIndex() 985 if not index.isValid(): 986 row = self.model.rowCount() 987 else: 988 row = index.row() 989 data = self.model.get_data() 990 if isinstance(data, list): 991 key = row 992 data.insert(row, '') 993 elif isinstance(data, dict): 994 key, valid = QInputDialog.getText(self, _( 'Insert'), _( 'Key:'), 995 QLineEdit.Normal) 996 if valid and to_text_string(key): 997 key = try_to_eval(to_text_string(key)) 998 else: 999 return 1000 else: 1001 return 1002 value, valid = QInputDialog.getText(self, _('Insert'), _('Value:'), 1003 QLineEdit.Normal) 1004 if valid and to_text_string(value): 1005 self.new_value(key, try_to_eval(to_text_string(value))) 1006 1007 def __prepare_plot(self): 1008 try: 1009 import guiqwt.pyplot #analysis:ignore 1010 return True 1011 except: 1012 try: 1013 if 'matplotlib' not in sys.modules: 1014 import matplotlib 1015 matplotlib.use("Qt4Agg") 1016 return True 1017 except: 1018 QMessageBox.warning(self, _("Import error"), 1019 _("Please install <b>matplotlib</b>" 1020 " or <b>guiqwt</b>.")) 1021 1022 def plot_item(self, funcname): 1023 """Plot item""" 1024 index = self.currentIndex() 1025 if self.__prepare_plot(): 1026 key = self.model.get_key(index) 1027 try: 1028 self.plot(key, funcname) 1029 except (ValueError, TypeError) as error: 1030 QMessageBox.critical(self, _( "Plot"), 1031 _("<b>Unable to plot data.</b>" 1032 "<br><br>Error message:<br>%s" 1033 ) % str(error)) 1034 1035 @Slot() 1036 def imshow_item(self): 1037 """Imshow item""" 1038 index = self.currentIndex() 1039 if self.__prepare_plot(): 1040 key = self.model.get_key(index) 1041 try: 1042 if self.is_image(key): 1043 self.show_image(key) 1044 else: 1045 self.imshow(key) 1046 except (ValueError, TypeError) as error: 1047 QMessageBox.critical(self, _( "Plot"), 1048 _("<b>Unable to show image.</b>" 1049 "<br><br>Error message:<br>%s" 1050 ) % str(error)) 1051 1052 @Slot() 1053 def save_array(self): 1054 """Save array""" 1055 title = _( "Save array") 1056 if self.array_filename is None: 1057 self.array_filename = getcwd_or_home() 1058 self.redirect_stdio.emit(False) 1059 filename, _selfilter = getsavefilename(self, title, 1060 self.array_filename, 1061 _("NumPy arrays")+" (*.npy)") 1062 self.redirect_stdio.emit(True) 1063 if filename: 1064 self.array_filename = filename 1065 data = self.delegate.get_value( self.currentIndex() ) 1066 try: 1067 import numpy as np 1068 np.save(self.array_filename, data) 1069 except Exception as error: 1070 QMessageBox.critical(self, title, 1071 _("<b>Unable to save array</b>" 1072 "<br><br>Error message:<br>%s" 1073 ) % str(error)) 1074 1075 @Slot() 1076 def copy(self): 1077 """Copy text to clipboard""" 1078 clipboard = QApplication.clipboard() 1079 clipl = [] 1080 for idx in self.selectedIndexes(): 1081 if not idx.isValid(): 1082 continue 1083 obj = self.delegate.get_value(idx) 1084 # Check if we are trying to copy a numpy array, and if so make sure 1085 # to copy the whole thing in a tab separated format 1086 if isinstance(obj, (ndarray, MaskedArray)) \ 1087 and ndarray is not FakeObject: 1088 if PY3: 1089 output = io.BytesIO() 1090 else: 1091 output = io.StringIO() 1092 try: 1093 np_savetxt(output, obj, delimiter='\t') 1094 except: 1095 QMessageBox.warning(self, _("Warning"), 1096 _("It was not possible to copy " 1097 "this array")) 1098 return 1099 obj = output.getvalue().decode('utf-8') 1100 output.close() 1101 elif isinstance(obj, (DataFrame, Series)) \ 1102 and DataFrame is not FakeObject: 1103 output = io.StringIO() 1104 obj.to_csv(output, sep='\t', index=True, header=True) 1105 if PY3: 1106 obj = output.getvalue() 1107 else: 1108 obj = output.getvalue().decode('utf-8') 1109 output.close() 1110 elif is_binary_string(obj): 1111 obj = to_text_string(obj, 'utf8') 1112 else: 1113 obj = to_text_string(obj) 1114 clipl.append(obj) 1115 clipboard.setText('\n'.join(clipl)) 1116 1117 def import_from_string(self, text, title=None): 1118 """Import data from string""" 1119 data = self.model.get_data() 1120 editor = ImportWizard(self, text, title=title, 1121 contents_title=_("Clipboard contents"), 1122 varname=fix_reference_name("data", 1123 blacklist=list(data.keys()))) 1124 if editor.exec_(): 1125 var_name, clip_data = editor.get_data() 1126 self.new_value(var_name, clip_data) 1127 1128 @Slot() 1129 def paste(self): 1130 """Import text/data/code from clipboard""" 1131 clipboard = QApplication.clipboard() 1132 cliptext = '' 1133 if clipboard.mimeData().hasText(): 1134 cliptext = to_text_string(clipboard.text()) 1135 if cliptext.strip(): 1136 self.import_from_string(cliptext, title=_("Import from clipboard")) 1137 else: 1138 QMessageBox.warning(self, _( "Empty clipboard"), 1139 _("Nothing to be imported from clipboard.")) 1140 1141 1142class CollectionsEditorTableView(BaseTableView): 1143 """CollectionsEditor table view""" 1144 def __init__(self, parent, data, readonly=False, title="", 1145 names=False, minmax=False): 1146 BaseTableView.__init__(self, parent) 1147 self.dictfilter = None 1148 self.readonly = readonly or isinstance(data, tuple) 1149 CollectionsModelClass = ReadOnlyCollectionsModel if self.readonly \ 1150 else CollectionsModel 1151 self.model = CollectionsModelClass(self, data, title, names=names, 1152 minmax=minmax) 1153 self.setModel(self.model) 1154 self.delegate = CollectionsDelegate(self) 1155 self.setItemDelegate(self.delegate) 1156 1157 self.setup_table() 1158 self.menu = self.setup_menu(minmax) 1159 1160 #------ Remote/local API --------------------------------------------------- 1161 def remove_values(self, keys): 1162 """Remove values from data""" 1163 data = self.model.get_data() 1164 for key in sorted(keys, reverse=True): 1165 data.pop(key) 1166 self.set_data(data) 1167 1168 def copy_value(self, orig_key, new_key): 1169 """Copy value""" 1170 data = self.model.get_data() 1171 if isinstance(data, list): 1172 data.append(data[orig_key]) 1173 if isinstance(data, set): 1174 data.add(data[orig_key]) 1175 else: 1176 data[new_key] = data[orig_key] 1177 self.set_data(data) 1178 1179 def new_value(self, key, value): 1180 """Create new value in data""" 1181 data = self.model.get_data() 1182 data[key] = value 1183 self.set_data(data) 1184 1185 def is_list(self, key): 1186 """Return True if variable is a list or a tuple""" 1187 data = self.model.get_data() 1188 return isinstance(data[key], (tuple, list)) 1189 1190 def get_len(self, key): 1191 """Return sequence length""" 1192 data = self.model.get_data() 1193 return len(data[key]) 1194 1195 def is_array(self, key): 1196 """Return True if variable is a numpy array""" 1197 data = self.model.get_data() 1198 return isinstance(data[key], (ndarray, MaskedArray)) 1199 1200 def is_image(self, key): 1201 """Return True if variable is a PIL.Image image""" 1202 data = self.model.get_data() 1203 return isinstance(data[key], Image) 1204 1205 def is_dict(self, key): 1206 """Return True if variable is a dictionary""" 1207 data = self.model.get_data() 1208 return isinstance(data[key], dict) 1209 1210 def get_array_shape(self, key): 1211 """Return array's shape""" 1212 data = self.model.get_data() 1213 return data[key].shape 1214 1215 def get_array_ndim(self, key): 1216 """Return array's ndim""" 1217 data = self.model.get_data() 1218 return data[key].ndim 1219 1220 def oedit(self, key): 1221 """Edit item""" 1222 data = self.model.get_data() 1223 from spyder.widgets.variableexplorer.objecteditor import oedit 1224 oedit(data[key]) 1225 1226 def plot(self, key, funcname): 1227 """Plot item""" 1228 data = self.model.get_data() 1229 import spyder.pyplot as plt 1230 plt.figure() 1231 getattr(plt, funcname)(data[key]) 1232 plt.show() 1233 1234 def imshow(self, key): 1235 """Show item's image""" 1236 data = self.model.get_data() 1237 import spyder.pyplot as plt 1238 plt.figure() 1239 plt.imshow(data[key]) 1240 plt.show() 1241 1242 def show_image(self, key): 1243 """Show image (item is a PIL image)""" 1244 data = self.model.get_data() 1245 data[key].show() 1246 #--------------------------------------------------------------------------- 1247 1248 def refresh_menu(self): 1249 """Refresh context menu""" 1250 data = self.model.get_data() 1251 index = self.currentIndex() 1252 condition = (not isinstance(data, tuple)) and index.isValid() \ 1253 and not self.readonly 1254 self.edit_action.setEnabled( condition ) 1255 self.remove_action.setEnabled( condition ) 1256 self.insert_action.setEnabled( not self.readonly ) 1257 self.duplicate_action.setEnabled(condition) 1258 condition_rename = not isinstance(data, (tuple, list, set)) 1259 self.rename_action.setEnabled(condition_rename) 1260 self.refresh_plot_entries(index) 1261 1262 def set_filter(self, dictfilter=None): 1263 """Set table dict filter""" 1264 self.dictfilter = dictfilter 1265 1266 1267class CollectionsEditorWidget(QWidget): 1268 """Dictionary Editor Widget""" 1269 def __init__(self, parent, data, readonly=False, title="", remote=False): 1270 QWidget.__init__(self, parent) 1271 if remote: 1272 self.editor = RemoteCollectionsEditorTableView(self, data, readonly) 1273 else: 1274 self.editor = CollectionsEditorTableView(self, data, readonly, 1275 title) 1276 layout = QVBoxLayout() 1277 layout.addWidget(self.editor) 1278 self.setLayout(layout) 1279 1280 def set_data(self, data): 1281 """Set DictEditor data""" 1282 self.editor.set_data(data) 1283 1284 def get_title(self): 1285 """Get model title""" 1286 return self.editor.model.title 1287 1288 1289class CollectionsEditor(QDialog): 1290 """Collections Editor Dialog""" 1291 def __init__(self, parent=None): 1292 QDialog.__init__(self, parent) 1293 1294 # Destroying the C++ object right after closing the dialog box, 1295 # otherwise it may be garbage-collected in another QThread 1296 # (e.g. the editor's analysis thread in Spyder), thus leading to 1297 # a segmentation fault on UNIX or an application crash on Windows 1298 self.setAttribute(Qt.WA_DeleteOnClose) 1299 1300 self.data_copy = None 1301 self.widget = None 1302 1303 def setup(self, data, title='', readonly=False, width=650, remote=False, 1304 icon=None, parent=None): 1305 """Setup editor.""" 1306 if isinstance(data, dict): 1307 # dictionnary 1308 self.data_copy = data.copy() 1309 datalen = len(data) 1310 elif isinstance(data, (tuple, list)): 1311 # list, tuple 1312 self.data_copy = data[:] 1313 datalen = len(data) 1314 else: 1315 # unknown object 1316 import copy 1317 try: 1318 self.data_copy = copy.deepcopy(data) 1319 except NotImplementedError: 1320 self.data_copy = copy.copy(data) 1321 except (TypeError, AttributeError): 1322 readonly = True 1323 self.data_copy = data 1324 datalen = len(get_object_attrs(data)) 1325 self.widget = CollectionsEditorWidget(self, self.data_copy, 1326 title=title, readonly=readonly, 1327 remote=remote) 1328 1329 layout = QVBoxLayout() 1330 layout.addWidget(self.widget) 1331 self.setLayout(layout) 1332 1333 # Buttons configuration 1334 buttons = QDialogButtonBox.Ok 1335 if not readonly: 1336 buttons = buttons | QDialogButtonBox.Cancel 1337 self.bbox = QDialogButtonBox(buttons) 1338 self.bbox.accepted.connect(self.accept) 1339 if not readonly: 1340 self.bbox.rejected.connect(self.reject) 1341 layout.addWidget(self.bbox) 1342 1343 constant = 121 1344 row_height = 30 1345 error_margin = 10 1346 height = constant + row_height * min([10, datalen]) + error_margin 1347 self.resize(width, height) 1348 1349 self.setWindowTitle(self.widget.get_title()) 1350 if icon is None: 1351 self.setWindowIcon(ima.icon('dictedit')) 1352 # Make the dialog act as a window 1353 self.setWindowFlags(Qt.Window) 1354 1355 def get_value(self): 1356 """Return modified copy of dictionary or list""" 1357 # It is import to avoid accessing Qt C++ object as it has probably 1358 # already been destroyed, due to the Qt.WA_DeleteOnClose attribute 1359 return self.data_copy 1360 1361 1362class DictEditor(CollectionsEditor): 1363 def __init__(self, parent=None): 1364 warnings.warn("`DictEditor` has been renamed to `CollectionsEditor` in " 1365 "Spyder 3. Please use `CollectionsEditor` instead", 1366 RuntimeWarning) 1367 CollectionsEditor.__init__(self, parent) 1368 1369 1370#----Remote versions of CollectionsDelegate and CollectionsEditorTableView 1371class RemoteCollectionsDelegate(CollectionsDelegate): 1372 """CollectionsEditor Item Delegate""" 1373 def __init__(self, parent=None, get_value_func=None, set_value_func=None): 1374 CollectionsDelegate.__init__(self, parent) 1375 self.get_value_func = get_value_func 1376 self.set_value_func = set_value_func 1377 1378 def get_value(self, index): 1379 if index.isValid(): 1380 name = index.model().keys[index.row()] 1381 return self.get_value_func(name) 1382 1383 def set_value(self, index, value): 1384 if index.isValid(): 1385 name = index.model().keys[index.row()] 1386 self.set_value_func(name, value) 1387 1388 1389class RemoteCollectionsEditorTableView(BaseTableView): 1390 """DictEditor table view""" 1391 def __init__(self, parent, data, minmax=False, 1392 dataframe_format=None, 1393 get_value_func=None, set_value_func=None, 1394 new_value_func=None, remove_values_func=None, 1395 copy_value_func=None, is_list_func=None, get_len_func=None, 1396 is_array_func=None, is_image_func=None, is_dict_func=None, 1397 get_array_shape_func=None, get_array_ndim_func=None, 1398 plot_func=None, imshow_func=None, 1399 is_data_frame_func=None, is_series_func=None, 1400 show_image_func=None): 1401 BaseTableView.__init__(self, parent) 1402 1403 self.remove_values = remove_values_func 1404 self.copy_value = copy_value_func 1405 self.new_value = new_value_func 1406 1407 self.is_data_frame = is_data_frame_func 1408 self.is_series = is_series_func 1409 self.is_list = is_list_func 1410 self.get_len = get_len_func 1411 self.is_array = is_array_func 1412 self.is_image = is_image_func 1413 self.is_dict = is_dict_func 1414 self.get_array_shape = get_array_shape_func 1415 self.get_array_ndim = get_array_ndim_func 1416 self.plot = plot_func 1417 self.imshow = imshow_func 1418 self.show_image = show_image_func 1419 1420 self.dictfilter = None 1421 self.model = None 1422 self.delegate = None 1423 self.readonly = False 1424 self.model = CollectionsModel(self, data, names=True, 1425 minmax=minmax, 1426 dataframe_format=dataframe_format, 1427 remote=True) 1428 self.setModel(self.model) 1429 self.delegate = RemoteCollectionsDelegate(self, get_value_func, 1430 set_value_func) 1431 self.setItemDelegate(self.delegate) 1432 1433 self.setup_table() 1434 self.menu = self.setup_menu(minmax) 1435 1436 def setup_menu(self, minmax): 1437 """Setup context menu""" 1438 menu = BaseTableView.setup_menu(self, minmax) 1439 return menu 1440 1441 1442# ============================================================================= 1443# Tests 1444# ============================================================================= 1445def get_test_data(): 1446 """Create test data.""" 1447 import numpy as np 1448 from spyder.pil_patch import Image 1449 image = Image.fromarray(np.random.random_integers(255, size=(100, 100)), 1450 mode='P') 1451 testdict = {'d': 1, 'a': np.random.rand(10, 10), 'b': [1, 2]} 1452 testdate = datetime.date(1945, 5, 8) 1453 1454 class Foobar(object): 1455 1456 def __init__(self): 1457 self.text = "toto" 1458 self.testdict = testdict 1459 self.testdate = testdate 1460 1461 foobar = Foobar() 1462 return {'object': foobar, 1463 'str': 'kjkj kj k j j kj k jkj', 1464 'unicode': to_text_string('éù', 'utf-8'), 1465 'list': [1, 3, [sorted, 5, 6], 'kjkj', None], 1466 'tuple': ([1, testdate, testdict], 'kjkj', None), 1467 'dict': testdict, 1468 'float': 1.2233, 1469 'int': 223, 1470 'bool': True, 1471 'array': np.random.rand(10, 10), 1472 'masked_array': np.ma.array([[1, 0], [1, 0]], 1473 mask=[[True, False], [False, False]]), 1474 '1D-array': np.linspace(-10, 10), 1475 'empty_array': np.array([]), 1476 'image': image, 1477 'date': testdate, 1478 'datetime': datetime.datetime(1945, 5, 8), 1479 'complex': 2+1j, 1480 'complex64': np.complex64(2+1j), 1481 'int8_scalar': np.int8(8), 1482 'int16_scalar': np.int16(16), 1483 'int32_scalar': np.int32(32), 1484 'bool_scalar': np.bool(8), 1485 'unsupported1': np.arccos, 1486 'unsupported2': np.cast, 1487 # Test for Issue #3518 1488 'big_struct_array': np.zeros(1000, dtype=[('ID', 'f8'), 1489 ('param1', 'f8', 5000)]), 1490 } 1491 1492 1493def editor_test(): 1494 """Collections editor test""" 1495 from spyder.utils.qthelpers import qapplication 1496 1497 app = qapplication() #analysis:ignore 1498 dialog = CollectionsEditor() 1499 dialog.setup(get_test_data()) 1500 dialog.show() 1501 app.exec_() 1502 1503 1504def remote_editor_test(): 1505 """Remote collections editor test""" 1506 from spyder.utils.qthelpers import qapplication 1507 app = qapplication() 1508 1509 from spyder.plugins.variableexplorer import VariableExplorer 1510 from spyder.widgets.variableexplorer.utils import make_remote_view 1511 1512 remote = make_remote_view(get_test_data(), 1513 VariableExplorer(None).get_settings()) 1514 dialog = CollectionsEditor() 1515 dialog.setup(remote, remote=True) 1516 dialog.show() 1517 app.exec_() 1518 1519 1520if __name__ == "__main__": 1521 editor_test() 1522 remote_editor_test() 1523