def startDrag(self, index): """start a drag operation with a PandasCellPayload on defined index. Args: index (QModelIndex): model index you want to start the drag operation. """ if not index.isValid(): return dataFrame = self.model().dataFrame() # get all infos from dataFrame dfindex = dataFrame.iloc[[index.row()]].index columnName = dataFrame.columns[index.column()] dtype = dataFrame[columnName].dtype value = dataFrame[columnName][dfindex] # create the mime data mimePayload = PandasCellPayload(dfindex, columnName, value, dtype, hex(id(self.model()))) mimeData = MimeData() mimeData.setData(mimePayload) # create the drag icon and start drag operation drag = QtGui.QDrag(self) drag.setMimeData(mimeData) pixmap = QtGui.QPixmap(":/icons/insert-table.png") drag.setHotSpot(QtCore.QPoint(pixmap.width() / 3, pixmap.height() / 3)) drag.setPixmap(pixmap)
def removeDataFrameColumns(self, columns): """ Removes columns from the dataframe. :param columns: [(int, str)] :return: (bool) True on success, False on failure. """ if not self.editable: return False if columns: deleted = 0 errored = False for (position, name) in columns: position = position - deleted if position < 0: position = 0 self.beginRemoveColumns(QtCore.QModelIndex(), position, position) try: self._dataFrame.drop(name, axis=1, inplace=True) except ValueError as e: errored = True continue self.endRemoveColumns() deleted += 1 self.dataChanged.emit() if errored: return False else: return True return False
def removeDataFrameRows(self, rows): """ Removes rows from the dataframe. :param rows: (list) of row indexes to removes. :return: (bool) True on success, False on failure. """ if not self.editable: return False if rows: position = min(rows) count = len(rows) self.beginRemoveRows(QtCore.QModelIndex(), position, position + count - 1) removedAny = False for idx, line in self._dataFrame.iterrows(): if idx in rows: removedAny = True self._dataFrame.drop(idx, inplace=True) if not removedAny: return False self._dataFrame.reset_index(inplace=True, drop=True) self.endRemoveRows() return True return False
def test_setDelegates(self, qtbot, tableView, index, value, singleStep): dlg = createDelegate(numpy.dtype(value), 0, tableView) assert dlg is not None data = pandas.DataFrame([value], columns=['A']) data['A'] = data['A'].astype(value.dtype) model = tableView.model() model.setDataFrame(data) for i, delegate in enumerate([dlg]): assert tableView.itemDelegateForColumn(i) == delegate option = QtGui.QStyleOptionViewItem() option.rect = QtCore.QRect(0, 0, 100, 100) editor = delegate.createEditor(tableView, option, index) delegate.setEditorData(editor, index) assert editor.value() == index.data() delegate.setModelData(editor, model, index) delegate.updateEditorGeometry(editor, option, index) dtype = value.dtype if dtype in DataFrameModel._intDtypes: info = numpy.iinfo(dtype) assert isinstance(delegate, BigIntSpinboxDelegate) elif dtype in DataFrameModel._floatDtypes: info = numpy.finfo(dtype) assert isinstance(delegate, CustomDoubleSpinboxDelegate) assert delegate.decimals == DataFrameModel._float_precisions[ str(value.dtype)] assert delegate.maximum == info.max assert editor.maximum() == info.max assert delegate.minimum == info.min assert editor.minimum() == info.min assert delegate.singleStep == singleStep assert editor.singleStep() == singleStep def clickEvent(index): assert index.isValid() tableView.clicked.connect(clickEvent) with qtbot.waitSignal(tableView.clicked) as blocker: qtbot.mouseClick(tableView.viewport(), QtCore.Qt.LeftButton, pos=QtCore.QPoint(10, 10)) assert blocker.signal_triggered
def columnCount(self, index=QtCore.QModelIndex()): """returns number of columns Args: index (QtCore.QModelIndex, optional): Index to define column and row. defaults to empty QModelIndex Returns: number of columns """ return len(self.headers)
def rowCount(self, index=QtCore.QModelIndex()): """returns number of rows Args: index (QtCore.QModelIndex, optional): Index to define column and row. defaults to empty QModelIndex Returns: number of rows """ return len(self._dataFrame.columns)
def __init__(self, parent=None): """Constructs the object with the given parent. Args: parent (QObject, optional): Causes the objected to be owned by `parent` instead of Qt. Defaults to `None`. """ super(DelimiterValidator, self).__init__(parent) re = QtCore.QRegExp('\S{1}') self.setRegExp(re)
def __init__(self, parent=None, iconSize=QtCore.QSize(36, 36)): """Constructs the object with the given parent. Args: parent (QObject, optional): Causes the objected to be owned by `parent` instead of Qt. Defaults to `None`. iconSize (QSize, optional): Size of edit buttons. Defaults to QSize(36, 36). """ super(DataTableWidget, self).__init__(parent) self._iconSize = iconSize self.initUi()
def columnCount(self, index=QtCore.QModelIndex()): """returns number of columns Args: index (QtCore.QModelIndex, optional): Index to define column and row. defaults to empty QModelIndex Returns: number of columns """ # speed comparison: # In [23]: %timeit len(df.columns) # 10000000 loops, best of 3: 108 ns per loop # In [24]: %timeit df.shape[1] # 1000000 loops, best of 3: 440 ns per loop return len(self._dataFrame.columns)
def addDataFrameRows(self, count=1): """ Adds rows to the dataframe. :param count: (int) The number of rows to add to the dataframe. :return: (bool) True on success, False on failure. """ # don't allow any gaps in the data rows. # and always append at the end if not self.editable: return False position = self.rowCount() if count < 1: return False if len(self.dataFrame().columns) == 0: # log an error message or warning return False # Note: This function emits the rowsAboutToBeInserted() signal which # connected views (or proxies) must handle before the data is # inserted. Otherwise, the views may end up in an invalid state. self.beginInsertRows(QtCore.QModelIndex(), position, position + count - 1) defaultValues = [] for dtype in self._dataFrame.dtypes: if dtype.type == numpy.dtype('<M8[ns]'): val = pandas.Timestamp('') elif dtype.type == numpy.dtype(object): val = '' else: val = dtype.type() defaultValues.append(val) for i in range(count): self._dataFrame.loc[position + i] = defaultValues self._dataFrame.reset_index() self.endInsertRows() return True
def rowCount(self, index=QtCore.QModelIndex()): """returns number of rows Args: index (QtCore.QModelIndex, optional): Index to define column and row. defaults to empty QModelIndex Returns: number of rows """ # len(df.index) is faster, so use it: # In [12]: %timeit df.shape[0] # 1000000 loops, best of 3: 437 ns per loop # In [13]: %timeit len(df.index) # 10000000 loops, best of 3: 110 ns per loop # %timeit df.__len__() # 1000000 loops, best of 3: 215 ns per loop return len(self._dataFrame.index)
def createThread(parent, worker, deleteWorkerLater=False): """Create a new thread for given worker. Args: parent (QObject): parent of thread and worker. worker (ProgressWorker): worker to use in thread. deleteWorkerLater (bool, optional): delete the worker if thread finishes. Returns: QThread """ thread = QtCore.QThread(parent) thread.started.connect(worker.doWork) worker.finished.connect(thread.quit) if deleteWorkerLater: thread.finished.connect(worker.deleteLater) worker.moveToThread(thread) worker.setParent(parent) return thread
def addDataFrameColumn(self, columnName, dtype=str, defaultValue=None): """ Adds a column to the dataframe as long as the model's editable property is set to True and the dtype is supported. :param columnName: str name of the column. :param dtype: qtpandas.models.SupportedDtypes option :param defaultValue: (object) to default the column's value to, should be the same as the dtype or None :return: (bool) True on success, False otherwise. """ if not self.editable or dtype not in SupportedDtypes.allTypes(): return False elements = self.rowCount() columnPosition = self.columnCount() newColumn = pandas.Series([defaultValue] * elements, index=self._dataFrame.index, dtype=dtype) self.beginInsertColumns(QtCore.QModelIndex(), columnPosition - 1, columnPosition - 1) try: self._dataFrame.insert(columnPosition, columnName, newColumn, allow_duplicates=False) except ValueError as e: # columnName does already exist return False self.endInsertColumns() self.propagateDtypeChanges(columnPosition, newColumn.dtype) return True
def __init__(self, parent=None): """the __init__ method. Args: parent (QObject): defaults to None. If parent is 0, the new widget becomes a window. If parent is another widget, this widget becomes a child window inside parent. The new widget is deleted when its parent is deleted. """ super(BigIntSpinbox, self).__init__(parent) self._singleStep = 1 self._minimum = -18446744073709551616 self._maximum = 18446744073709551615 rx = QtCore.QRegExp("[0-9]\\d{0,20}") validator = QtGui.QRegExpValidator(rx, self) self._lineEdit = QtGui.QLineEdit(self) self._lineEdit.setText('0') self._lineEdit.setValidator(validator) self.setLineEdit(self._lineEdit)
def test_data(self, dataframe): model = ColumnDtypeModel(dataFrame=dataframe) index = model.index(0, 0) # get data for display role ret = index.data() assert ret == 'Foo' # edit role does the same as display role ret = index.data(Qt.EditRole) assert ret == 'Foo' # datatype only defined for column 1 ret = index.data(DTYPE_ROLE) assert ret == None # datatype column index = index.sibling(0, 1) ret = index.data(DTYPE_ROLE) assert ret == numpy.dtype(numpy.int64) # check translation / display text assert index.data( ) == 'integer (64 bit)' == SupportedDtypes.description(ret) # column not defined index = index.sibling(0, 2) assert index.data(DTYPE_ROLE) == None # invalid index index = QtCore.QModelIndex() assert model.data(index) == None index = model.index(2, 0) # get data for display role ret = index.data() assert ret == 'Spam'
def test_invalidIndex(self, model): assert model.setData(QtCore.QModelIndex(), None) == False
def test_invalidIndex(self, model): assert model.data(QtCore.QModelIndex()) is None
class DataFrameModelManager(QtCore.QObject): """ A central storage unit for managing DataFrameModels. """ signalNewModelRead = QtCore.Signal(str) signalModelDestroyed = QtCore.Signal(str) def __init__(self): QtCore.QObject.__init__(self) self._models = {} self._updates = defaultdict(list) self._paths_read = [] self._paths_updated = [] @property def file_paths(self): """Returns a list of the currently stored file paths""" return list(self._models.keys()) @property def models(self): """Returns a list of all currently stored DataFrameModels""" return list(self._models.values()) @property def last_path_read(self): """Returns the last path read (via the DataFrameModelManager.read_file method)""" if self._paths_read: return self._paths_read[-1] else: return None @property def last_path_updated(self): """Returns the last path to register an update. (or None)""" if self._paths_updated: return self._paths_updated[-1] else: return None def save_file(self, filepath, save_as=None, keep_orig=False, **kwargs): """ Saves a DataFrameModel to a file. :param filepath: (str) The filepath of the DataFrameModel to save. :param save_as: (str, default None) The new filepath to save as. :param keep_orig: (bool, default False) True keeps the original filepath/DataFrameModel if save_as is specified. :param kwargs: pandas.DataFrame.to_excel(**kwargs) if .xlsx pandas.DataFrame.to_csv(**kwargs) otherwise. :return: None """ df = self._models[filepath].dataFrame() kwargs['index'] = kwargs.get('index', False) if save_as is not None: to_path = save_as else: to_path = filepath ext = os.path.splitext(to_path)[1].lower() if ext == ".xlsx": kwargs.pop('sep', None) df.to_excel(to_path, **kwargs) elif ext in ['.csv', '.txt']: df.to_csv(to_path, **kwargs) else: raise NotImplementedError( "Cannot save file of type {}".format(ext)) if save_as is not None: if keep_orig is False: # Re-purpose the original model # Todo - capture the DataFrameModelManager._updates too model = self._models.pop(filepath) model._filePath = to_path else: # Create a new model. model = DataFrameModel() model.setDataFrame(df, copyDataFrame=True, filePath=to_path) self._models[to_path] = model def set_model(self, df_model, file_path): """ Sets a DataFrameModel and registers it to the given file_path. :param df_model: (DataFrameModel) The DataFrameModel to register. :param file_path: The file path to associate with the DataFrameModel. *Overrides the current filePath on the DataFrameModel (if any) :return: None """ assert isinstance( df_model, DataFrameModel), "df_model argument must be a DataFrameModel!" df_model._filePath = file_path try: self._models[file_path] except KeyError: self.signalNewModelRead.emit(file_path) self._models[file_path] = df_model def get_model(self, filepath): """ Returns the DataFrameModel registered to filepath """ return self._models[filepath] def get_frame(self, filepath): """Returns the DataFrameModel.dataFrame() registered to filepath """ return self._models[filepath].dataFrame() def update_file(self, filepath, df, notes=None): """ Sets a new DataFrame for the DataFrameModel registered to filepath. :param filepath (str) The filepath to the DataFrameModel to be updated :param df (pandas.DataFrame) The new DataFrame to register to the model. :param notes (str, default None) Optional notes to register along with the update. """ assert isinstance( df, pd.DataFrame), "Cannot update file with type '{}'".format(type(df)) self._models[filepath].setDataFrame(df, copyDataFrame=False) if notes: update = dict(date=pd.Timestamp(datetime.datetime.now()), notes=notes) self._updates[filepath].append(update) self._paths_updated.append(filepath) def remove_file(self, filepath): """ Removes the DataFrameModel from being registered. :param filepath: (str) The filepath to delete from the DataFrameModelManager. :return: None """ self._models.pop(filepath) self._updates.pop(filepath, default=None) self.signalModelDestroyed.emit(filepath) def read_file(self, filepath, **kwargs): """ Reads a filepath into a DataFrameModel and registers it. Example use: dfmm = DataFrameModelManger() dfmm.read_file(path_to_file) dfm = dfmm.get_model(path_to_file) df = dfm.get_frame(path_to_file) :param filepath: (str) The filepath to read :param kwargs: .xlsx files: pandas.read_excel(**kwargs) .csv files: pandas.read_csv(**kwargs) :return: DataFrameModel """ try: model = self._models[filepath] except KeyError: model = read_file(filepath, **kwargs) self._models[filepath] = model self.signalNewModelRead.emit(filepath) finally: self._paths_read.append(filepath) return self._models[filepath]
class DataFrameExportDialog(CSVExportDialog): """ Extends the CSVExportDialog with support for exporting to .txt and .xlsx """ signalExportFilenames = QtCore.Signal(str, str) signalModelChanged = QtCore.Signal(DataFrameModel) def __init__(self, model=None, parent=None): CSVExportDialog.__init__(self, model=None, parent=None) if model is not None: self._filename = model.filePath else: self._filename = None self._windowTitle = "Export Data" self.setWindowTitle(self._windowTitle) @Slot(DataFrameModel) def swapModel(self, model): good = self.setExportModel(model) if good: self.signalModelChanged.emit(model) @Slot() def accepted(self): """Successfully close the widget and emit an export signal. This method is also a `SLOT`. The dialog will be closed, when the `Export Data` button is pressed. If errors occur during the export, the status bar will show the error message and the dialog will not be closed. """ #return super(DataFrameExportDialog, self).accepted try: self._saveModel() except Exception as err: self._statusBar.showMessage(str(err)) raise else: self._resetWidgets() self.exported.emit(True) self.accept() @Slot() def rejected(self): """Close the widget and reset its inital state. This method is also a `SLOT`. The dialog will be closed and all changes reverted, when the `cancel` button is pressed. """ self._resetWidgets() self.exported.emit(False) self.reject() def _saveModel(self): """ Reimplements _saveModel to utilize all of the Pandas export options based on file extension. :return: None """ delimiter = self._delimiterBox.currentSelected() header = self._headerCheckBox.isChecked() # column labels if self._filename is None: filename = self._filenameLineEdit.text() else: filename = self._filename ext = os.path.splitext(filename)[1].lower() index = False # row labels encodingIndex = self._encodingComboBox.currentIndex() encoding = self._encodingComboBox.itemText(encodingIndex) encoding = _calculateEncodingKey(encoding.lower()) try: dataFrame = self._model.dataFrame() except AttributeError as err: raise AttributeError('No data loaded to export.') else: print("Identifying export type for {}".format(filename)) try: if ext in ['.txt', '.csv']: dataFrame.to_csv(filename, encoding=encoding, header=header, index=index, sep=delimiter) elif ext == '.tsv': sep = '\t' dataFrame.to_csv(filename, encoding=encoding, header=header, index=index, sep=delimiter) elif ext in ['.xlsx', '.xls']: dataFrame.to_excel(filename, encoding=encoding, header=header, index=index, sep=delimiter) except IOError as err: raise IOError('No filename given') except UnicodeError as err: raise UnicodeError( 'Could not encode all data. Choose a different encoding') except Exception: raise self.signalExportFilenames.emit(self._model._filePath, filename)