Пример #1
0
class ProgressWorker(QtCore.QObject):

    progressChanged = Signal(int)       # set value of OverlayProgressView
    finished = Signal()

    def __init__(self, name):
        """Worker object that will be passed to the thread.

        Args:
            name (str): name shown in progress ui.

        """
        super(ProgressWorker, self).__init__()
        self.name = name

    @Slot()
    def doWork(self):
        """start the thread"""
        self.run()
        # emit the result of the operation?
        self.finished.emit()

    def run(self):
        """Implement your job here. This is what the thread will do.

        """
        raise NotImplementedError
Пример #2
0
class ComplexDropWidget(QtGui.QLineEdit):

    dropRecieved = Signal(QtCore.QMimeData)

    def __init__(self, parent=None):
        super(ComplexDropWidget, self).__init__(parent)
        self.setAcceptDrops(True)

    def dragEnterEvent(self, event):
        """recieve a drag event and check if we want to accept or reject

        Args:
            event (QDragEnterEvent)
        """
        if event.mimeData().hasFormat(PandasCellMimeType):
            if event.mimeData().data().isValid():
                event.accept()
            else:
                event.ignore()
        else:
            event.ignore()

    def dropEvent(self, event):
        """process the dragged data

        Args:
            event (QDragEnterEvent)
        """
        self.dropRecieved.emit(event.mimeData())
Пример #3
0
class RemoveAttributesDialog(QtGui.QDialog):

    accepted = Signal(list)

    def __init__(self, columns, parent=None):
        super(RemoveAttributesDialog, self).__init__(parent)
        self.columns = columns
        self.initUi()

    def initUi(self):
        self.setWindowTitle(self.tr('Remove Attributes'))
        self.setModal(True)
        self.resize(366, 274)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed,
                                       QtGui.QSizePolicy.Expanding)
        self.setSizePolicy(sizePolicy)

        self.gridLayout = QtGui.QGridLayout(self)

        self.dialogHeading = QtGui.QLabel(
            self.tr('Select the attribute column(s) which shall be removed'),
            self)

        self.listView = QtGui.QListView(self)

        model = QtGui.QStandardItemModel()
        for column in self.columns:
            item = QtGui.QStandardItem(column)
            model.appendRow(item)

        self.listView.setModel(model)
        self.listView.setSelectionMode(QtGui.QListView.MultiSelection)

        self.buttonBox = QtGui.QDialogButtonBox(self)
        self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
        self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel
                                          | QtGui.QDialogButtonBox.Ok)

        self.gridLayout.addWidget(self.dialogHeading, 0, 0, 1, 1)
        self.gridLayout.addWidget(self.listView, 1, 0, 1, 1)
        self.gridLayout.addWidget(self.buttonBox, 2, 0, 1, 1)

        self.buttonBox.accepted.connect(self.accept)
        self.buttonBox.rejected.connect(self.reject)

    def accept(self):
        selection = self.listView.selectedIndexes()
        names = []
        for index in selection:
            position = index.row()
            names.append((position, index.data(QtCore.Qt.DisplayRole)))

        super(RemoveAttributesDialog, self).accept()
        self.accepted.emit(names)
Пример #4
0
class AddAttributesDialog(QtGui.QDialog):

    accepted = Signal(str, object, object)

    def __init__(self, parent=None):
        super(AddAttributesDialog, self).__init__(parent)

        self.initUi()

    def initUi(self):
        self.setModal(True)
        self.resize(303, 168)
        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,
                                       QtGui.QSizePolicy.Fixed)
        self.setSizePolicy(sizePolicy)

        self.verticalLayout = QtGui.QVBoxLayout(self)

        self.dialogHeading = QtGui.QLabel(
            self.tr('Add a new attribute column'), self)

        self.gridLayout = QtGui.QGridLayout()

        self.columnNameLineEdit = QtGui.QLineEdit(self)
        self.columnNameLabel = QtGui.QLabel(self.tr('Name'), self)
        self.dataTypeComboBox = QtGui.QComboBox(self)

        self.dataTypeComboBox.addItems(SupportedDtypes.names())

        self.columnTypeLabel = QtGui.QLabel(self.tr('Type'), self)
        self.defaultValueLineEdit = QtGui.QLineEdit(self)
        self.lineEditValidator = DefaultValueValidator(self)
        self.defaultValueLineEdit.setValidator(self.lineEditValidator)
        self.defaultValueLabel = QtGui.QLabel(self.tr('Inital Value(s)'), self)

        self.gridLayout.addWidget(self.columnNameLabel, 0, 0, 1, 1)
        self.gridLayout.addWidget(self.columnNameLineEdit, 0, 1, 1, 1)

        self.gridLayout.addWidget(self.columnTypeLabel, 1, 0, 1, 1)
        self.gridLayout.addWidget(self.dataTypeComboBox, 1, 1, 1, 1)

        self.gridLayout.addWidget(self.defaultValueLabel, 2, 0, 1, 1)
        self.gridLayout.addWidget(self.defaultValueLineEdit, 2, 1, 1, 1)

        self.buttonBox = QtGui.QDialogButtonBox(self)
        self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
        self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel
                                          | QtGui.QDialogButtonBox.Ok)

        self.verticalLayout.addWidget(self.dialogHeading)
        self.verticalLayout.addLayout(self.gridLayout)
        self.verticalLayout.addWidget(self.buttonBox)

        self.buttonBox.accepted.connect(self.accept)
        self.buttonBox.rejected.connect(self.reject)

        self.dataTypeComboBox.currentIndexChanged.connect(
            self.updateValidatorDtype)
        self.updateValidatorDtype(self.dataTypeComboBox.currentIndex())

    def accept(self):
        super(AddAttributesDialog, self).accept()

        newColumn = self.columnNameLineEdit.text()
        dtype = SupportedDtypes.dtype(self.dataTypeComboBox.currentText())

        defaultValue = self.defaultValueLineEdit.text()
        try:
            if dtype in SupportedDtypes.intTypes() + SupportedDtypes.uintTypes(
            ):
                defaultValue = int(defaultValue)
            elif dtype in SupportedDtypes.floatTypes():
                defaultValue = float(defaultValue)
            elif dtype in SupportedDtypes.boolTypes():
                defaultValue = defaultValue.lower() in ['t', '1']
            elif dtype in SupportedDtypes.datetimeTypes():
                defaultValue = Timestamp(defaultValue)
                if isinstance(defaultValue, NaTType):
                    defaultValue = Timestamp('')
            else:
                defaultValue = dtype.type()
        except ValueError as e:
            defaultValue = dtype.type()

        self.accepted.emit(newColumn, dtype, defaultValue)

    @Slot(int)
    def updateValidatorDtype(self, index):
        (dtype, name) = SupportedDtypes.tupleAt(index)
        self.defaultValueLineEdit.clear()
        self.lineEditValidator.validateType(dtype)
Пример #5
0
class CSVExportDialog(QtGui.QDialog):
    """An widget to serialize a `DataFrameModel` to a `CSV-File`.

    """
    exported = Signal(bool)

    def __init__(self, model=None, parent=None):
        super(CSVExportDialog, self).__init__(parent)
        self._model = model
        self._modal = True
        self._windowTitle = 'Export to CSV'
        self._idx = -1
        self._initUI()

    def _initUI(self):
        """Initiates the user interface with a grid layout and several widgets.

        """
        self.setModal(self._modal)
        self.setWindowTitle(self._windowTitle)

        layout = QtGui.QGridLayout()

        self._filenameLabel = QtGui.QLabel('Output File', self)
        self._filenameLineEdit = QtGui.QLineEdit(self)
        chooseFileButtonIcon = QtGui.QIcon(
            QtGui.QPixmap(':/icons/document-save-as.png'))
        self._chooseFileAction = QtGui.QAction(self)
        self._chooseFileAction.setIcon(chooseFileButtonIcon)
        self._chooseFileAction.triggered.connect(self._createFile)

        self._chooseFileButton = QtGui.QToolButton(self)
        self._chooseFileButton.setDefaultAction(self._chooseFileAction)

        layout.addWidget(self._filenameLabel, 0, 0)
        layout.addWidget(self._filenameLineEdit, 0, 1, 1, 2)
        layout.addWidget(self._chooseFileButton, 0, 3)

        self._encodingLabel = QtGui.QLabel('File Encoding', self)

        encoding_names = list(
            map(lambda x: x.upper(), sorted(list(set(_encodings.values())))))

        self._encodingComboBox = QtGui.QComboBox(self)
        self._encodingComboBox.addItems(encoding_names)
        self._idx = encoding_names.index('UTF_8')
        self._encodingComboBox.setCurrentIndex(self._idx)
        #self._encodingComboBox.activated.connect(self._updateEncoding)

        layout.addWidget(self._encodingLabel, 1, 0)
        layout.addWidget(self._encodingComboBox, 1, 1, 1, 1)

        self._hasHeaderLabel = QtGui.QLabel('Header Available?', self)
        self._headerCheckBox = QtGui.QCheckBox(self)
        #self._headerCheckBox.toggled.connect(self._updateHeader)

        layout.addWidget(self._hasHeaderLabel, 2, 0)
        layout.addWidget(self._headerCheckBox, 2, 1)

        self._delimiterLabel = QtGui.QLabel('Column Delimiter', self)
        self._delimiterBox = DelimiterSelectionWidget(self)

        layout.addWidget(self._delimiterLabel, 3, 0)
        layout.addWidget(self._delimiterBox, 3, 1, 1, 3)

        self._exportButton = QtGui.QPushButton('Export Data', self)
        self._cancelButton = QtGui.QPushButton('Cancel', self)

        self._buttonBox = QtGui.QDialogButtonBox(self)
        self._buttonBox.addButton(self._exportButton,
                                  QtGui.QDialogButtonBox.AcceptRole)
        self._buttonBox.addButton(self._cancelButton,
                                  QtGui.QDialogButtonBox.RejectRole)

        self._buttonBox.accepted.connect(self.accepted)
        self._buttonBox.rejected.connect(self.rejected)

        layout.addWidget(self._buttonBox, 5, 2, 1, 2)
        self._exportButton.setDefault(False)
        self._filenameLineEdit.setFocus()

        self._statusBar = QtGui.QStatusBar(self)
        self._statusBar.setSizeGripEnabled(False)
        layout.addWidget(self._statusBar, 4, 0, 1, 4)
        self.setLayout(layout)

    def setExportModel(self, model):
        if not isinstance(model, DataFrameModel):
            return False

        self._model = model
        return True

    @Slot()
    def _createFile(self):
        ret = QtGui.QFileDialog.getSaveFileName(
            self, 'Save File', filter='Comma Separated Value (*.csv)')
        if isinstance(ret, tuple):
            ret = ret[0]
        self._filenameLineEdit.setText(ret)

    def _saveModel(self):
        delimiter = self._delimiterBox.currentSelected()
        header = self._headerCheckBox.isChecked()  # column labels
        filename = self._filenameLineEdit.text()
        index = False  # row labels

        encodingIndex = self._encodingComboBox.currentIndex()
        encoding = self._encodingComboBox.itemText(encodingIndex)
        encoding = _calculateEncodingKey(encoding.lower())

        try:
            dataFrame = self._model.dataFrame()
        except AttributeError:
            raise AttributeError('No data loaded to export.')
        else:
            try:
                dataFrame.to_csv(filename,
                                 encoding=encoding,
                                 header=header,
                                 index=index,
                                 sep=delimiter)
            except IOError:
                raise IOError('No filename given')
            except UnicodeError:
                raise UnicodeError(
                    'Could not encode all data. Choose a different encoding')
            except Exception:
                raise

    def _resetWidgets(self):
        """Resets all widgets of this dialog to its inital state.

        """
        self._filenameLineEdit.setText('')
        self._encodingComboBox.setCurrentIndex(self._idx)
        self._delimiterBox.reset()
        self._headerCheckBox.setChecked(False)
        self._statusBar.showMessage('')

    @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.

        """
        try:
            self._saveModel()
        except Exception as err:
            self._statusBar.showMessage(str(err))
        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()
Пример #6
0
class DelimiterSelectionWidget(QtGui.QGroupBox):
    """A custom widget with different text delimiter signs.

    A user can choose between 3 predefined and one user defined
    text delimiter characters. Default delimiters include `semicolon`,
    `colon` and `tabulator`. The user defined delimiter may only have
    a length of 1 and may not include any whitespace character.

    Attributes:
        delimiter (QtCore.pyqtSignal): This signal is emitted, whenever a
            delimiter character is selected by the user.
        semicolonRadioButton (QtGui.QRadioButton): A radio button to
            select the `semicolon` character as delimiter.
        commaRadioButton (QtGui.QRadioButton): A radio button to select
            the `comma` character as delimiter.
        tabRadioButton (QtGui.QRadioButton): A radio button to select
            the `tabulator` character as delimiter.
        otherRadioButton (QtGui.QRadioButton): A radio button to select
            the given input text as delimiter.
        otherSeparatorLineEdit (QtGui.QLineEdit): An input line to let the
            user enter one character only, which may be used as delimiter.

    """

    delimiter = Signal('QString')

    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(DelimiterSelectionWidget, self).__init__(parent)
        self.semicolonRadioButton = None
        self.commaRadioButton = None
        self.tabRadioButton = None
        self.otherRadioButton = None
        self.otherSeparatorLineEdit = None
        self._initUI()

    def _initUI(self):
        """Creates the inital layout with all subwidgets.

        The layout is a `QHBoxLayout`. Each time a radio button is
        selected or unselected, a slot
        `DelimiterSelectionWidget._delimiter` is called.
        Furthermore the `QLineEdit` widget has a custom regex validator
        `DelimiterValidator` enabled.

        """
        #layout = QtGui.QHBoxLayout(self)

        self.semicolonRadioButton = QtGui.QRadioButton('Semicolon')
        self.commaRadioButton = QtGui.QRadioButton('Comma')
        self.tabRadioButton = QtGui.QRadioButton('Tab')
        self.otherRadioButton = QtGui.QRadioButton('Other')
        self.commaRadioButton.setChecked(True)

        self.otherSeparatorLineEdit = QtGui.QLineEdit(self)
        #TODO: Enable this or add BAR radio and option.
        self.otherSeparatorLineEdit.setEnabled(False)

        self.semicolonRadioButton.toggled.connect(self._delimiter)
        self.commaRadioButton.toggled.connect(self._delimiter)
        self.tabRadioButton.toggled.connect(self._delimiter)

        self.otherRadioButton.toggled.connect(self._enableLine)
        self.otherSeparatorLineEdit.textChanged.connect(
            lambda: self._delimiter(True))
        self.otherSeparatorLineEdit.setValidator(DelimiterValidator(self))

        currentLayout = self.layout()
        # unset and delete the current layout in order to set a new one
        if currentLayout is not None:
            del currentLayout

        layout = QtGui.QHBoxLayout()
        layout.addWidget(self.semicolonRadioButton)
        layout.addWidget(self.commaRadioButton)
        layout.addWidget(self.tabRadioButton)
        layout.addWidget(self.otherRadioButton)
        layout.addWidget(self.otherSeparatorLineEdit)
        self.setLayout(layout)

    @Slot(bool)
    def _enableLine(self, toggled):
        self.otherSeparatorLineEdit.setEnabled(toggled)

    def currentSelected(self):
        """Returns the currently selected delimiter character.

        Returns:
            str: One of `,`, `;`, `\t`, `*other*`.

        """
        if self.commaRadioButton.isChecked():
            return ','
        elif self.semicolonRadioButton.isChecked():
            return ';'
        elif self.tabRadioButton.isChecked():
            return '\t'
        elif self.otherRadioButton.isChecked():
            return self.otherSeparatorLineEdit.text()
        return

    @Slot(bool)
    def _delimiter(self, checked):
        if checked:
            if self.commaRadioButton.isChecked():
                self.delimiter.emit(',')
            elif self.semicolonRadioButton.isChecked():
                self.delimiter.emit(';')
            elif self.tabRadioButton.isChecked():
                self.delimiter.emit('\t')
            elif self.otherRadioButton.isChecked():
                ret = self.otherSeparatorLineEdit.text()
                if len(ret) > 0:
                    self.delimiter.emit(ret)

    def reset(self):
        """Resets this widget to its initial state.

        """
        self.semicolonRadioButton.setChecked(True)
        self.otherSeparatorLineEdit.setText('')
Пример #7
0
class CSVImportDialog(QtGui.QDialog):
    """A dialog to import any csv file into a pandas data frame.

    This modal dialog enables the user to enter any path to a csv
    file and parse this file with or without a header and with special
    delimiter characters.

    On a successful load, the data can be previewed and the column data
    types may be edited by the user.

    After all configuration is done, the dataframe and the underlying model
    may be used by the main application.

    Attributes:
        load (QtCore.pyqtSignal): This signal is emitted, whenever the
            dialog is successfully closed, e.g. when the ok button is
            pressed. Returns DataFrameModel and path of chosen csv file.
    """

    load = Signal('QAbstractItemModel', str)

    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(CSVImportDialog, self).__init__(parent)
        self._modal = True
        self._windowTitle = 'Import CSV'
        self._encodingKey = None
        self._filename = None
        self._delimiter = None
        self._header = None
        # self._detector = Detector()
        self._initUI()

    def _initUI(self):
        """Initiates the user interface with a grid layout and several widgets.

        """
        self.setModal(self._modal)
        self.setWindowTitle(self._windowTitle)

        layout = QtGui.QGridLayout()

        self._filenameLabel = QtGui.QLabel('Choose File', self)
        self._filenameLineEdit = QtGui.QLineEdit(self)
        self._filenameLineEdit.textEdited.connect(self._updateFilename)
        chooseFileButtonIcon = QtGui.QIcon(
            QtGui.QPixmap(':/icons/document-open.png'))
        self._chooseFileAction = QtGui.QAction(self)
        self._chooseFileAction.setIcon(chooseFileButtonIcon)
        self._chooseFileAction.triggered.connect(self._openFile)

        self._chooseFileButton = QtGui.QToolButton(self)
        self._chooseFileButton.setDefaultAction(self._chooseFileAction)

        layout.addWidget(self._filenameLabel, 0, 0)
        layout.addWidget(self._filenameLineEdit, 0, 1, 1, 2)
        layout.addWidget(self._chooseFileButton, 0, 3)

        self._encodingLabel = QtGui.QLabel('File Encoding', self)

        encoding_names = list(
            [x.upper() for x in sorted(list(set(_encodings.values())))])
        self._encodingComboBox = QtGui.QComboBox(self)
        self._encodingComboBox.addItems(encoding_names)
        self._encodingComboBox.activated.connect(self._updateEncoding)

        layout.addWidget(self._encodingLabel, 1, 0)
        layout.addWidget(self._encodingComboBox, 1, 1, 1, 1)

        self._hasHeaderLabel = QtGui.QLabel('Header Available?', self)
        self._headerCheckBox = QtGui.QCheckBox(self)
        self._headerCheckBox.toggled.connect(self._updateHeader)

        layout.addWidget(self._hasHeaderLabel, 2, 0)
        layout.addWidget(self._headerCheckBox, 2, 1)

        self._delimiterLabel = QtGui.QLabel('Column Delimiter', self)
        self._delimiterBox = DelimiterSelectionWidget(self)
        self._delimiter = self._delimiterBox.currentSelected()
        self._delimiterBox.delimiter.connect(self._updateDelimiter)

        layout.addWidget(self._delimiterLabel, 3, 0)
        layout.addWidget(self._delimiterBox, 3, 1, 1, 3)

        self._tabWidget = QtGui.QTabWidget(self)
        self._previewTableView = QtGui.QTableView(self)
        self._datatypeTableView = QtGui.QTableView(self)
        self._tabWidget.addTab(self._previewTableView, 'Preview')
        self._tabWidget.addTab(self._datatypeTableView, 'Change Column Types')
        layout.addWidget(self._tabWidget, 4, 0, 3, 4)

        self._datatypeTableView.horizontalHeader().setDefaultSectionSize(200)
        self._datatypeTableView.setItemDelegateForColumn(
            1, DtypeComboDelegate(self._datatypeTableView))

        self._loadButton = QtGui.QPushButton('Load Data', self)
        #self.loadButton.setAutoDefault(False)

        self._cancelButton = QtGui.QPushButton('Cancel', self)
        # self.cancelButton.setDefault(False)
        # self.cancelButton.setAutoDefault(True)

        self._buttonBox = QtGui.QDialogButtonBox(self)
        self._buttonBox.addButton(self._loadButton,
                                  QtGui.QDialogButtonBox.AcceptRole)
        self._buttonBox.addButton(self._cancelButton,
                                  QtGui.QDialogButtonBox.RejectRole)
        self._buttonBox.accepted.connect(self.accepted)
        self._buttonBox.rejected.connect(self.rejected)
        layout.addWidget(self._buttonBox, 9, 2, 1, 2)
        self._loadButton.setDefault(False)
        self._filenameLineEdit.setFocus()

        self._statusBar = QtGui.QStatusBar(self)
        self._statusBar.setSizeGripEnabled(False)
        self._headerCheckBox.setChecked(True)
        layout.addWidget(self._statusBar, 8, 0, 1, 4)
        self.setLayout(layout)

    @Slot('QString')
    def updateStatusBar(self, message):
        """Updates the status bar widget of this dialog with the given message.

        This method is also a `SLOT()`.
        The message will be shown for only 5 seconds.

        Args:
            message (QString): The new message which will be displayed.

        """
        self._statusBar.showMessage(message, 5000)

    @Slot()
    def _openFile(self):
        """Opens a file dialog and sets a value for the QLineEdit widget.

        This method is also a `SLOT`.

        """

        file_types = "Comma Separated Values (*.csv);;Text files (*.txt);;All Files (*)"
        ret = QtGui.QFileDialog.getOpenFileName(self,
                                                self.tr('open file'),
                                                filter=file_types)

        if isinstance(ret, tuple):
            ret = ret[0]  #PySide compatibility maybe?

        if ret:
            self._filenameLineEdit.setText(ret)
            self._updateFilename()

    @Slot(bool)
    def _updateHeader(self, toggled):
        """Changes the internal flag, whether the csv file contains a header or not.

        This method is also a `SLOT`.

        In addition, after toggling the corresponding checkbox, the
        `_previewFile` method will be called.

        Args:
            toggled (boolean): A flag indicating the status of the checkbox.
                The flag will be used to update an internal variable.

        """
        self._header = 0 if toggled else None
        self._previewFile()

    @Slot()
    def _updateFilename(self):
        """Calls several methods after the filename changed.

        This method is also a `SLOT`.
        It checks the encoding of the changed filename and generates a
        preview of the data.

        """
        self._filename = self._filenameLineEdit.text()
        self._guessEncoding(self._filename)
        self._previewFile()

    def _guessEncoding(self, path):
        """Opens a file from the given `path` and checks the file encoding.

        The file must exists on the file system and end with the extension
        `.csv`. The file is read line by line until the encoding could be
        guessed.
        On a successfull identification, the widgets of this dialog will be
        updated.

        Args:
            path (string): Path to a csv file on the file system.

        """
        if os.path.exists(path) and path.lower().endswith('csv'):
            # encoding = self._detector.detect(path)
            encoding = None

            if encoding is not None:
                if encoding.startswith('utf'):
                    encoding = encoding.replace('-', '')
                encoding = encoding.replace('-', '_')

                viewValue = _encodings.get(encoding)

                self._encodingKey = encoding

                index = self._encodingComboBox.findText(viewValue.upper())
                self._encodingComboBox.setCurrentIndex(index)

    @Slot('int')
    def _updateEncoding(self, index):
        """Changes the value of the encoding combo box to the value of given index.

        This method is also a `SLOT`.
        After the encoding is changed, the file will be reloaded and previewed.

        Args:
            index (int): An valid index of the combo box.

        """
        encoding = self._encodingComboBox.itemText(index)
        encoding = encoding.lower()

        self._encodingKey = _calculateEncodingKey(encoding)
        self._previewFile()

    @Slot('QString')
    def _updateDelimiter(self, delimiter):
        """Changes the value of the delimiter for the csv file.

        This method is also a `SLOT`.

        Args:
            delimiter (string): The new delimiter.

        """
        self._delimiter = delimiter
        self._previewFile()

    def _previewFile(self):
        """Updates the preview widgets with new models for both tab panes.

        """
        dataFrame = self._loadCSVDataFrame()
        dataFrameModel = DataFrameModel(dataFrame, filePath=self._filename)
        dataFrameModel.enableEditing(True)
        self._previewTableView.setModel(dataFrameModel)
        columnModel = dataFrameModel.columnDtypeModel()
        columnModel.changeFailed.connect(self.updateStatusBar)
        self._datatypeTableView.setModel(columnModel)

    def _loadCSVDataFrame(self):
        """Loads the given csv file with pandas and generate a new dataframe.

        The file will be loaded with the configured encoding, delimiter
        and header.git
        If any execptions will occur, an empty Dataframe is generated
        and a message will appear in the status bar.

        Returns:
            pandas.DataFrame: A dataframe containing all the available
                information of the csv file.

        """
        if self._filename and os.path.exists(self._filename):
            # default fallback if no encoding was found/selected
            encoding = self._encodingKey or 'UTF_8'

            try:
                dataFrame = superReadFile(self._filename,
                                          sep=self._delimiter,
                                          first_codec=encoding,
                                          header=self._header)
                dataFrame = dataFrame.apply(fillNoneValues)
                dataFrame = dataFrame.apply(convertTimestamps)
            except Exception as err:
                self.updateStatusBar(str(err))
                print(err)
                return pandas.DataFrame()
            self.updateStatusBar('Preview generated.')
            return dataFrame
        self.updateStatusBar('File could not be read.')
        return pandas.DataFrame()

    def _resetWidgets(self):
        """Resets all widgets of this dialog to its inital state.

        """
        self._filenameLineEdit.setText('')
        self._encodingComboBox.setCurrentIndex(0)
        self._delimiterBox.reset()
        self._headerCheckBox.setChecked(False)
        self._statusBar.showMessage('')
        self._previewTableView.setModel(None)
        self._datatypeTableView.setModel(None)

    @Slot()
    def accepted(self):
        """Successfully close the widget and return the loaded model.

        This method is also a `SLOT`.
        The dialog will be closed, when the `ok` button is pressed. If
        a `DataFrame` was loaded, it will be emitted by the signal `load`.

        """
        model = self._previewTableView.model()
        if model is not None:
            df = model.dataFrame().copy()
            dfModel = DataFrameModel(df)
            self.load.emit(dfModel, self._filename)
            print(("Emitted model for {}".format(self._filename)))
        self._resetWidgets()
        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.reject()
Пример #8
0
class DataFrameModel(QtCore.QAbstractTableModel):
    """data model for use in QTableView, QListView, QComboBox, etc.

    Attributes:
        timestampFormat (unicode): formatting string for conversion of timestamps to QtCore.QDateTime.
            Used in data method.
        sortingAboutToStart (QtCore.pyqtSignal): emitted directly before sorting starts.
        sortingFinished (QtCore.pyqtSignal): emitted, when sorting finished.
        dtypeChanged (Signal(columnName)): passed from related ColumnDtypeModel
            if a columns dtype has changed.
        changingDtypeFailed (Signal(columnName, index, dtype)):
            passed from related ColumnDtypeModel.
            emitted after a column has changed it's data type.
        dataChanged (Signal):
            Emitted, if data has changed, e.x. finished loading, new columns added or removed.
            It's not the same as layoutChanged.
            Usefull to reset delegates in the view.
    """

    _float_precisions = {
        "float16": numpy.finfo(numpy.float16).precision - 2,
        "float32": numpy.finfo(numpy.float32).precision - 1,
        "float64": numpy.finfo(numpy.float64).precision - 1
    }
    """list of int datatypes for easy checking in data() and setData()"""
    _intDtypes = SupportedDtypes.intTypes() + SupportedDtypes.uintTypes()
    """list of float datatypes for easy checking in data() and setData()"""
    _floatDtypes = SupportedDtypes.floatTypes()
    """list of bool datatypes for easy checking in data() and setData()"""
    _boolDtypes = SupportedDtypes.boolTypes()
    """list of datetime datatypes for easy checking in data() and setData()"""
    _dateDtypes = SupportedDtypes.datetimeTypes()

    _timestampFormat = Qt.ISODate

    sortingAboutToStart = Signal()
    sortingFinished = Signal()
    dtypeChanged = Signal(int, object)
    changingDtypeFailed = Signal(object, QtCore.QModelIndex, object)
    dataChanged = Signal()
    dataFrameChanged = Signal()

    def __init__(self, dataFrame=None, copyDataFrame=False, filePath=None):
        """

        Args:
            dataFrame (pandas.core.frame.DataFrame, optional): initializes the model with given DataFrame.
                If none is given an empty DataFrame will be set. defaults to None.
            copyDataFrame (bool, optional): create a copy of dataFrame or use it as is. defaults to False.
                If you use it as is, you can change it from outside otherwise you have to reset the dataFrame
                after external changes.
            filePath (str, optional): stores the original path for tracking.

        """
        super(DataFrameModel, self).__init__()

        self._dataFrame = pandas.DataFrame()

        if dataFrame is not None:
            self.setDataFrame(dataFrame, copyDataFrame=copyDataFrame)

        self.dataChanged.emit()

        self._dataFrameOriginal = None
        self._search = DataSearch("nothing", "")
        self.editable = False
        self._filePath = filePath

    @property
    def filePath(self):
        """
        Access to the internal _filepath property (could be None)
        :return: qtpandas.models.DataFrameModel._filepath
        """
        return self._filePath

    def dataFrame(self):
        """
        getter function to _dataFrame. Holds all data.

        Note:
            It's not implemented with python properties to keep Qt conventions.
            Not sure why??
        """
        return self._dataFrame

    def setDataFrameFromFile(self, filepath, **kwargs):
        """
        Sets the model's dataFrame by reading a file.
        Accepted file formats:
            - .xlsx (sheet1 is read unless specified in kwargs)
            - .csv (comma separated unless specified in kwargs)
            - .txt (any separator)

        :param filepath: (str)
            The path to the file to be read.
        :param kwargs:
            pandas.read_csv(**kwargs) or pandas.read_excel(**kwargs)
        :return: None
        """
        df = superReadFile(filepath, **kwargs)
        self.setDataFrame(df, filePath=filepath)

    def setDataFrame(self, dataFrame, copyDataFrame=False, filePath=None):
        """
        Setter function to _dataFrame. Holds all data.

        Note:
            It's not implemented with python properties to keep Qt conventions.

        Raises:
            TypeError: if dataFrame is not of type pandas.core.frame.DataFrame.

        Args:
            dataFrame (pandas.core.frame.DataFrame): assign dataFrame to _dataFrame. Holds all the data displayed.
            copyDataFrame (bool, optional): create a copy of dataFrame or use it as is. defaults to False.
                If you use it as is, you can change it from outside otherwise you have to reset the dataFrame
                after external changes.

        """
        if not isinstance(dataFrame, pandas.core.frame.DataFrame):
            raise TypeError("not of type pandas.core.frame.DataFrame")

        self.layoutAboutToBeChanged.emit()
        if copyDataFrame:
            self._dataFrame = dataFrame.copy()
        else:
            self._dataFrame = dataFrame

        self._columnDtypeModel = ColumnDtypeModel(dataFrame)
        self._columnDtypeModel.dtypeChanged.connect(self.propagateDtypeChanges)
        self._columnDtypeModel.changeFailed.connect(
            lambda columnName, index, dtype: self.changingDtypeFailed.emit(
                columnName, index, dtype))
        if filePath is not None:
            self._filePath = filePath
        self.layoutChanged.emit()
        self.dataChanged.emit()
        self.dataFrameChanged.emit()

    @Slot(int, object)
    def propagateDtypeChanges(self, column, dtype):
        """
        Emits a dtypeChanged signal with the column and dtype.

        :param column: (str)
        :param dtype: ??
        :return: None
        """
        self.dtypeChanged.emit(column, dtype)

    @property
    def timestampFormat(self):
        """getter to _timestampFormat"""
        return self._timestampFormat

    @timestampFormat.setter
    def timestampFormat(self, timestampFormat):
        """
        Setter to _timestampFormat. Formatting string for conversion of timestamps to QtCore.QDateTime

        Raises:
            AssertionError: if timestampFormat is not of type unicode.

        Args:
            timestampFormat (unicode): assign timestampFormat to _timestampFormat.
                Formatting string for conversion of timestamps to QtCore.QDateTime. Used in data method.

        """
        if not isinstance(timestampFormat, str):
            raise TypeError('not of type unicode')
        #assert isinstance(timestampFormat, unicode) or timestampFormat.__class__.__name__ == "DateFormat", "not of type unicode"
        self._timestampFormat = timestampFormat

    def rename(self, index=None, columns=None, **kwargs):
        """
        Renames the dataframe inplace calling appropriate signals.
        Wraps pandas.DataFrame.rename(*args, **kwargs) - overrides
        the inplace kwarg setting it to True.

        Example use:
        renames = {'colname1':'COLNAME_1', 'colname2':'COL2'}
        DataFrameModel.rename(columns=renames)

        :param args:
            see pandas.DataFrame.rename
        :param kwargs:
            see pandas.DataFrame.rename
        :return:
            None
        """
        kwargs['inplace'] = True
        self.layoutAboutToBeChanged.emit()
        self._dataFrame.rename(index, columns, **kwargs)
        self.layoutChanged.emit()
        self.dataChanged.emit()
        self.dataFrameChanged.emit()

    def applyFunction(self, func):
        """
        Applies a function to the dataFrame with appropriate signals.
        The function must return a dataframe.
        :param func: A function (or partial function) that accepts a dataframe as the first argument.
        :return: None
        :raise:
            AssertionError if the func is not callable.
            AssertionError if the func does not return a DataFrame.
        """
        assert callable(func), "function {} is not callable".format(func)
        self.layoutAboutToBeChanged.emit()
        df = func(self._dataFrame)
        assert isinstance(df, pandas.DataFrame
                          ), "function {} did not return a DataFrame.".format(
                              func.__name__)
        self._dataFrame = df
        self.layoutChanged.emit()
        self.dataChanged.emit()
        self.dataFrameChanged.emit()

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        """
        Return the header depending on section, orientation and Qt::ItemDataRole

        Args:
            section (int): For horizontal headers, the section number corresponds to the column number.
                Similarly, for vertical headers, the section number corresponds to the row number.
            orientation (Qt::Orientations):
            role (Qt::ItemDataRole):

        Returns:
            None if not Qt.DisplayRole
            _dataFrame.columns.tolist()[section] if orientation == Qt.Horizontal
            section if orientation == Qt.Vertical
            None if horizontal orientation and section raises IndexError
        """
        if role != Qt.DisplayRole:
            return None

        if orientation == Qt.Horizontal:
            try:
                label = self._dataFrame.columns.tolist()[section]
                if label == section:
                    label = section
                return label
            except (IndexError, ):
                return None
        elif orientation == Qt.Vertical:
            return section

    def data(self, index, role=Qt.DisplayRole):
        """return data depending on index, Qt::ItemDataRole and data type of the column.

        Args:
            index (QtCore.QModelIndex): Index to define column and row you want to return
            role (Qt::ItemDataRole): Define which data you want to return.

        Returns:
            None if index is invalid
            None if role is none of: DisplayRole, EditRole, CheckStateRole, DATAFRAME_ROLE

            if role DisplayRole:
                unmodified _dataFrame value if column dtype is object (string or unicode).
                _dataFrame value as int or long if column dtype is in _intDtypes.
                _dataFrame value as float if column dtype is in _floatDtypes. Rounds to defined precision (look at: _float16_precision, _float32_precision).
                None if column dtype is in _boolDtypes.
                QDateTime if column dtype is numpy.timestamp64[ns]. Uses timestampFormat as conversion template.

            if role EditRole:
                unmodified _dataFrame value if column dtype is object (string or unicode).
                _dataFrame value as int or long if column dtype is in _intDtypes.
                _dataFrame value as float if column dtype is in _floatDtypes. Rounds to defined precision (look at: _float16_precision, _float32_precision).
                _dataFrame value as bool if column dtype is in _boolDtypes.
                QDateTime if column dtype is numpy.timestamp64[ns]. Uses timestampFormat as conversion template.

            if role CheckStateRole:
                Qt.Checked or Qt.Unchecked if dtype is numpy.bool_ otherwise None for all other dtypes.

            if role DATAFRAME_ROLE:
                unmodified _dataFrame value.

            raises TypeError if an unhandled dtype is found in column.
        """

        if not index.isValid():
            return None

        def convertValue(row, col, columnDtype):
            value = None
            if columnDtype == object:
                value = self._dataFrame.ix[row, col]
            elif columnDtype in self._floatDtypes:
                value = round(float(self._dataFrame.ix[row, col]),
                              self._float_precisions[str(columnDtype)])
            elif columnDtype in self._intDtypes:
                value = int(self._dataFrame.ix[row, col])
            elif columnDtype in self._boolDtypes:
                # TODO this will most likely always be true
                # See: http://stackoverflow.com/a/715455
                # well no: I am mistaken here, the data is already in the dataframe
                # so its already converted to a bool
                value = bool(self._dataFrame.ix[row, col])

            elif columnDtype in self._dateDtypes:
                #print numpy.datetime64(self._dataFrame.ix[row, col])
                value = pandas.Timestamp(self._dataFrame.ix[row, col])
                value = QtCore.QDateTime.fromString(str(value),
                                                    self.timestampFormat)
                #print value
            # else:
            #     raise TypeError, "returning unhandled data type"
            return value

        row = self._dataFrame.index[index.row()]
        col = self._dataFrame.columns[index.column()]
        columnDtype = self._dataFrame[col].dtype

        if role == Qt.DisplayRole:
            # return the value if you wanne show True/False as text
            if columnDtype == numpy.bool:
                result = self._dataFrame.ix[row, col]
            else:
                result = convertValue(row, col, columnDtype)
        elif role == Qt.EditRole:
            result = convertValue(row, col, columnDtype)
        elif role == Qt.CheckStateRole:
            if columnDtype == numpy.bool_:
                if convertValue(row, col, columnDtype):
                    result = Qt.Checked
                else:
                    result = Qt.Unchecked
            else:
                result = None
        elif role == DATAFRAME_ROLE:
            result = self._dataFrame.ix[row, col]
        else:
            result = None
        return result

    def flags(self, index):
        """Returns the item flags for the given index as ored value, e.x.: Qt.ItemIsUserCheckable | Qt.ItemIsEditable

        If a combobox for bool values should pop up ItemIsEditable have to set for bool columns too.

        Args:
            index (QtCore.QModelIndex): Index to define column and row

        Returns:
            if column dtype is not boolean Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
            if column dtype is boolean Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsUserCheckable
        """
        flags = super(DataFrameModel, self).flags(index)

        if not self.editable:
            return flags

        col = self._dataFrame.columns[index.column()]
        if self._dataFrame[col].dtype == numpy.bool:
            flags |= Qt.ItemIsUserCheckable
        else:
            # if you want to have a combobox for bool columns set this
            flags |= Qt.ItemIsEditable

        return flags

    def setData(self, index, value, role=Qt.DisplayRole):
        """Set the value to the index position depending on Qt::ItemDataRole and data type of the column

        Args:
            index (QtCore.QModelIndex): Index to define column and row.
            value (object): new value.
            role (Qt::ItemDataRole): Use this role to specify what you want to do.

        Raises:
            TypeError: If the value could not be converted to a known datatype.

        Returns:
            True if value is changed. Calls layoutChanged after update.
            False if value is not different from original value.

        """
        if not index.isValid() or not self.editable:
            return False

        if value != index.data(role):

            self.layoutAboutToBeChanged.emit()

            row = self._dataFrame.index[index.row()]
            col = self._dataFrame.columns[index.column()]
            #print 'before change: ', index.data().toUTC(), self._dataFrame.iloc[row][col]
            columnDtype = self._dataFrame[col].dtype

            if columnDtype == object:
                pass

            elif columnDtype in self._intDtypes:
                dtypeInfo = numpy.iinfo(columnDtype)
                if value < dtypeInfo.min:
                    value = dtypeInfo.min
                elif value > dtypeInfo.max:
                    value = dtypeInfo.max

            elif columnDtype in self._floatDtypes:
                value = numpy.float64(value).astype(columnDtype)

            elif columnDtype in self._boolDtypes:
                value = numpy.bool_(value)

            elif columnDtype in self._dateDtypes:
                # convert the given value to a compatible datetime object.
                # if the conversation could not be done, keep the original
                # value.
                if isinstance(value, QtCore.QDateTime):
                    value = value.toString(self.timestampFormat)
                try:
                    value = pandas.Timestamp(value)
                except Exception:
                    raise Exception(
                        "Can't convert '{0}' into a datetime".format(value))
                    # return False
            else:
                raise TypeError("try to set unhandled data type")

            self._dataFrame.set_value(row, col, value)

            #print 'after change: ', value, self._dataFrame.iloc[row][col]
            self.layoutChanged.emit()
            return True
        else:
            return False

    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 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 sort(self, columnId, order=Qt.AscendingOrder):
        """
        Sorts the model column

        After sorting the data in ascending or descending order, a signal
        `layoutChanged` is emitted.

        :param: columnId (int)
            the index of the column to sort on.
        :param: order (Qt::SortOrder, optional)
            descending(1) or ascending(0). defaults to Qt.AscendingOrder

        """
        self.layoutAboutToBeChanged.emit()
        self.sortingAboutToStart.emit()
        column = self._dataFrame.columns[columnId]
        self._dataFrame.sort_values(column,
                                    ascending=not bool(order),
                                    inplace=True)
        self.layoutChanged.emit()
        self.sortingFinished.emit()

    def setFilter(self, search):
        """
        Apply a filter and hide rows.

        The filter must be a `DataSearch` object, which evaluates a python
        expression.
        If there was an error while parsing the expression, the data will remain
        unfiltered.

        Args:
            search(qtpandas.DataSearch): data search object to use.

        Raises:
            TypeError: An error is raised, if the given parameter is not a
                `DataSearch` object.

        """
        if not isinstance(search, DataSearch):
            raise TypeError(
                'The given parameter must an `qtpandas.DataSearch` object')

        self._search = search

        self.layoutAboutToBeChanged.emit()

        if self._dataFrameOriginal is not None:
            self._dataFrame = self._dataFrameOriginal
        self._dataFrameOriginal = self._dataFrame.copy()

        self._search.setDataFrame(self._dataFrame)
        searchIndex, valid = self._search.search()

        if valid:
            self._dataFrame = self._dataFrame[searchIndex]
            self.layoutChanged.emit()
        else:
            self.clearFilter()
            self.layoutChanged.emit()

        self.dataFrameChanged.emit()

    def clearFilter(self):
        """
        Clear all filters.
        """
        if self._dataFrameOriginal is not None:
            self.layoutAboutToBeChanged.emit()
            self._dataFrame = self._dataFrameOriginal
            self._dataFrameOriginal = None
            self.layoutChanged.emit()

    def columnDtypeModel(self):
        """
        Getter for a ColumnDtypeModel.

        :return:
            qtpandas.models.ColumnDtypeModel
        """
        return self._columnDtypeModel

    def enableEditing(self, editable=True):
        """
        Sets the DataFrameModel and columnDtypeModel's
        editable properties.
        :param editable: bool
            defaults to True,
            False disables most editing methods.
        :return:
            None
        """
        self.editable = editable
        self._columnDtypeModel.setEditable(self.editable)

    def dataFrameColumns(self):
        """
        :return: list containing dataframe columns
        """
        return self._dataFrame.columns.tolist()

    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 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 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
Пример #9
0
class ColumnDtypeModel(QtCore.QAbstractTableModel):
    """Data model returning datatypes per column

    Attributes:
        dtypeChanged (Signal(columnName)): emitted after a column has changed it's data type.
        changeFailed (Signal('QString')): emitted if a column
            datatype could not be changed. An errormessage is provided.
    """
    dtypeChanged = Signal(int, object)
    changeFailed = Signal('QString', QtCore.QModelIndex, object)

    def __init__(self, dataFrame=None, editable=False):
        """the __init__ method.

        Args:
            dataFrame (pandas.core.frame.DataFrame, optional): initializes the model with given DataFrame.
                If none is given an empty DataFrame will be set. defaults to None.
            editable (bool, optional): apply changes while changing dtype. defaults to True.

        """
        super(ColumnDtypeModel, self).__init__()
        self.headers = ['column', 'data type']

        self._editable = editable

        self._dataFrame = pandas.DataFrame()
        if dataFrame is not None:
            self.setDataFrame(dataFrame)

    def dataFrame(self):
        """getter function to _dataFrame. Holds all data.

        Note:
            It's not implemented with python properties to keep Qt conventions.

        """
        return self._dataFrame

    def setDataFrame(self, dataFrame):
        """setter function to _dataFrame. Holds all data.

        Note:
            It's not implemented with python properties to keep Qt conventions.

        Raises:
            TypeError: if dataFrame is not of type pandas.core.frame.DataFrame.

        Args:
            dataFrame (pandas.core.frame.DataFrame): assign dataFrame to _dataFrame. Holds all the data displayed.

        """
        if not isinstance(dataFrame, pandas.core.frame.DataFrame):
            raise TypeError(
                'Argument is not of type pandas.core.frame.DataFrame')

        self.layoutAboutToBeChanged.emit()
        self._dataFrame = dataFrame
        self.layoutChanged.emit()

    def editable(self):
        """getter to _editable """
        return self._editable

    def setEditable(self, editable):
        """setter to _editable. apply changes while changing dtype.

        Raises:
            TypeError: if editable is not of type bool.

        Args:
            editable (bool): apply changes while changing dtype.

        """
        if not isinstance(editable, bool):
            raise TypeError('Argument is not of type bool')
        self._editable = editable

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        """defines which labels the view/user shall see.

        Args:
            section (int): the row or column number.
            orientation (Qt.Orienteation): Either horizontal or vertical.
            role (Qt.ItemDataRole, optional): Defaults to `Qt.DisplayRole`.

        Returns
            str if a header for the appropriate section is set and the requesting
                role is fitting, None if not.

        """
        if role != Qt.DisplayRole:
            return None

        if orientation == Qt.Horizontal:
            try:
                return self.headers[section]
            except (IndexError, ):
                return None

    def data(self, index, role=Qt.DisplayRole):
        """Retrieve the data stored in the model at the given `index`.

        Args:
            index (QtCore.QModelIndex): The model index, which points at a
                data object.
            role (Qt.ItemDataRole, optional): Defaults to `Qt.DisplayRole`. You
                have to use different roles to retrieve different data for an
                `index`. Accepted roles are `Qt.DisplayRole`, `Qt.EditRole` and
                `DTYPE_ROLE`.

        Returns:
            None if an invalid index is given, the role is not accepted by the
            model or the column is greater than `1`.
            The column name will be returned if the given column number equals `0`
            and the role is either `Qt.DisplayRole` or `Qt.EditRole`.
            The datatype will be returned, if the column number equals `1`. The
            `Qt.DisplayRole` or `Qt.EditRole` return a human readable, translated
            string, whereas the `DTYPE_ROLE` returns the raw data type.

        """

        # an index is invalid, if a row or column does not exist or extends
        # the bounds of self.columnCount() or self.rowCount()
        # therefor a check for col>1 is unnecessary.
        if not index.isValid():
            return None

        col = index.column()

        #row = self._dataFrame.columns[index.column()]
        columnName = self._dataFrame.columns[index.row()]
        columnDtype = self._dataFrame[columnName].dtype

        if role == Qt.DisplayRole or role == Qt.EditRole:
            if col == 0:
                if columnName == index.row():
                    return index.row()
                return columnName
            elif col == 1:
                return SupportedDtypes.description(columnDtype)
        elif role == DTYPE_ROLE:
            if col == 1:
                return columnDtype
            else:
                return None

    def setData(self, index, value, role=DTYPE_CHANGE_ROLE):
        """Updates the datatype of a column.

        The model must be initated with a dataframe already, since valid
        indexes are necessary. The `value` is a translated description of the
        data type. The translations can be found at
        `qtpandas.translation.DTypeTranslator`.

        If a datatype can not be converted, e.g. datetime to integer, a
        `NotImplementedError` will be raised.

        Args:
            index (QtCore.QModelIndex): The index of the column to be changed.
            value (str): The description of the new datatype, e.g.
                `positive kleine ganze Zahl (16 Bit)`.
            role (Qt.ItemDataRole, optional): The role, which accesses and
                changes data. Defaults to `DTYPE_CHANGE_ROLE`.

        Raises:
            NotImplementedError: If an error during conversion occured.

        Returns:
            bool: `True` if the datatype could be changed, `False` if not or if
                the new datatype equals the old one.

        """
        if role != DTYPE_CHANGE_ROLE or not index.isValid():
            return False

        if not self.editable():
            return False

        self.layoutAboutToBeChanged.emit()

        dtype = SupportedDtypes.dtype(value)
        currentDtype = np.dtype(index.data(role=DTYPE_ROLE))

        if dtype is not None:
            if dtype != currentDtype:
                # col = index.column()
                # row = self._dataFrame.columns[index.column()]
                columnName = self._dataFrame.columns[index.row()]

                try:
                    if dtype == np.dtype('<M8[ns]'):
                        if currentDtype in SupportedDtypes.boolTypes():
                            raise Exception(
                                "Can't convert a boolean value into a datetime value."
                            )
                        self._dataFrame[columnName] = self._dataFrame[
                            columnName].apply(pandas.to_datetime)
                    else:
                        self._dataFrame[columnName] = self._dataFrame[
                            columnName].astype(dtype)
                    self.dtypeChanged.emit(index.row(), dtype)
                    self.layoutChanged.emit()

                    return True
                except Exception:
                    message = 'Could not change datatype %s of column %s to datatype %s' % (
                        currentDtype, columnName, dtype)
                    self.changeFailed.emit(message, index, dtype)
                    raise
                    # self._dataFrame[columnName] = self._dataFrame[columnName].astype(currentDtype)
                    # self.layoutChanged.emit()
                    # self.dtypeChanged.emit(columnName)
                    #raise NotImplementedError, "dtype changing not fully working, original error:\n{}".format(e)
        return False

    def flags(self, index):
        """Returns the item flags for the given index as ored value, e.x.: Qt.ItemIsUserCheckable | Qt.ItemIsEditable

        Args:
            index (QtCore.QModelIndex): Index to define column and row

        Returns:
            for column 'column': Qt.ItemIsSelectable | Qt.ItemIsEnabled
            for column 'data type': Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable

        """
        if not index.isValid():
            return Qt.NoItemFlags

        col = index.column()

        flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable

        if col > 0 and self.editable():
            flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable

        return flags

    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 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)