class AutoResizeableFile(QtCore.QObject): """ Class representing the auto-resizeable files """ def __init__(self, filePath = None): """ Default constructor. Opens specified file for reading and writing. 'filePath' - path to the file. If file doesn't exist, new file created. If 'filePath' has None value, a temporary file created, which would be removed after it's closing. """ QtCore.QObject.__init__(self) self._isTemporary = False; if filePath == None: filePath = os.tempnam() self._isTemporary = True self._file = QtCore.QFile(filePath) if not self._file.open(QtCore.QIODevice.ReadWrite): raise IOError("File can't be opened!", {"raisingObject": self, "path": filePath, "error": self._file.error()}) self._validRanges = RangesList(0, self._file.size()) self._packingListeners = [] def __del__(self): """ Default finalizing method (destructor). Packs, saves all changes and closes the file """ self.close() def close(self): """ Packs, saves all changes and closes the file. Returns None. """ if self._file.isOpen(): self.flush() self._file.close() if self._isTemporary: self._file.remove() def path(self): """ Returns file path """ return self._file.fileName() def position(self): """ Returns current file position """ return self._file.pos() def size(self): """ Returns real physical size of the file. """ return self._file.size() def validRanges(self): """ Returns a list (each item - a 2-integer tuple) of valid file sections. Valid section is a one, which wasn't removed previously """ return self._validRanges.ranges() def isOpened(self): """ Returns True if file is opened, False otherwise """ return self._file.isOpen() def endOfFile(self): """ Returns True if current file position points to the end of physical file. """ return self.position() >= self.size() def setPosition(self, position): """ Sets new file position. Returns real value of position. 'position' - new file position (integer) """ position = self._validateInteger(position) if not self._file.seek(position): raise IOError("Seeking position failed", {"raiseObject": self, "position": position, "error": self._file.error()}) return self.position() def erase(self): """ Erases all file content. Returns None """ self._file.resize(0) self.setPosition(0) self._validRanges.clear() def addPackingListener(self, listener): """ Adds a new file packing event listener. This event happens when all previously removed file sections are removed physically from the disk. Returns None 'listener' - a FilePackingListener object reference """ if not isinstance(listener, FilePackingListener): raise TypeError("Listener must has a 'FilePackingListener' type!", {"raisingObject": self}) self._packingListeners.append(listener) def removePackingListener(self, listener): """ Removes listener added via 'addPackingListener' method. 'listener' - a FilePackingListener object reference """ if not isinstance(listener, FilePackingListener): raise TypeError("Listener must has a 'FilePackingListener' type!", {"raisingObject": self}) if not (listener in self._packingListeners): return del self._packingListeners[self._packingListeners.index(listener)] def realPositionFor(self, validPosition): """ Translates any file position to real position in the physical file. This values may be different if any sections were removed previously 'validPosition' - position to translate (integer) """ ranges = self._validRanges.ranges() lastRangeIndex, currentSum = 0, 0 rangeFound = False for lastRangeIndex in xrange(0, len(ranges)): currentRange = ranges[lastRangeIndex] if currentSum <= validPosition < currentSum + (currentRange[1] - currentRange[0]): rangeFound = True break else: currentSum += (currentRange[1] - currentRange[0]) if not rangeFound: if currentSum == validPosition: return validPosition else: raise IndexError("Position is too large", {"raisingObject": self, "value": validPosition}) lastEnumeratedRange = ranges[lastRangeIndex] neededDelta = validPosition - currentSum return lastEnumeratedRange[0] + neededDelta def readLine(self): """ Reads and returns a line. """ data = str(self._file.readLine()) return data.strip() def read(self, size = -1): """ Reads and returns a string with specified length 'size' - needed length of string. If 'size' has -1 value, the whole file up to the end would be read """ size = self._validateInteger(size) if size == -1: size = self._file.size() - self.position() data = str(self._file.read(size)) return data def readAll(self): """ Reads and returns the whole file content, saves old filed position """ oldPosition = self.position() self.setPosition(0) data = self.read() self.setPosition(oldPosition) return data def readInteger(self): """ Reads next 4 bytes from the file and converts them to integer value, that would be returned """ oldPosition = self.position() string = self.read(4) if len(string) != 4: self._file.seek(oldPosition) raise EOFError("Can't read needed amount of bytes!", {"raiseObject": self, "position": oldPosition, "fileSize": self._file.size()}) return struct.unpack("i", string)[0] def write(self, string, eraseFirstSymbols = 0): """ Writes data represented by 'string', previously removed 'eraseFirstSymbols' bytes. Returns really written bytes amount """ eraseFirstSymbols = self._validateInteger(eraseFirstSymbols) string = str(string) stringLength = len(string) eraseFirstSymbols = min(eraseFirstSymbols, self._file.size() - self.position()) additionalSpace = stringLength - eraseFirstSymbols start= self.position() + eraseFirstSymbols newStart = start + additionalSpace oldPosition = self.position() self._shift(start, -1, newStart) written = self._file.writeData(string) self.setPosition(oldPosition + written) rangeToAdd = (oldPosition, oldPosition + stringLength) shiftingValue = newStart - start self._shiftValidRanges(start, shiftingValue) self._validRanges.add(rangeToAdd[0], rangeToAdd[1]) return written def writeLine(self, string, eraseFirstSymbols = 0): """ Writes a line represented by 'string', previously removed 'eraseFirstSymbols' bytes. Returns really written bytes amount """ string = str(string) + '\n' return self.write(string, eraseFirstSymbols) def writeInteger(self, value, eraseOld = False): """ Writes an integer represented by 'value' with erasing previous value if 'eraseOld' is True. Returns really written bytes amount """ value = self._validateInteger(value) eraseFirstSymbols = 0 if eraseOld: eraseFirstSymbols = 4 return self.write(struct.pack("i", value), eraseFirstSymbols) def flush(self): """ Flushes file to the disk with previously packing. Returns None. """ self.pack() self._file.flush() def pack(self): """ Performs packing operation, that physically removes all removed previously file sections via 'removeRange' method. Returns None. """ if self._validRanges.size() == 0: self.setPosition(0) self._file.resize(0) return shiftingInfos = [] currentValidSize = 0 totalShifting = 0 for validRange in self._validRanges.ranges(): shiftValue = currentValidSize - validRange[0] totalShifting += shiftValue if (validRange[0] != validRange[1]) and (totalShifting != 0): shiftingInfos.append(RangeShiftingInfo(validRange, totalShifting)) self._shift(validRange[0], validRange[1], currentValidSize) currentValidSize += validRange[1] - validRange[0] self._file.resize(currentValidSize) if len(shiftingInfos) != 0: for listener in self._packingListeners: listener.filePackedEvent(shiftingInfos) self._validRanges.clear() self._validRanges.add(0, currentValidSize) def removeRange(self, startPosition, endPosition): """ Removes file section, specified by 'startPosition' and 'endPosition' constrains ('endPosition' - excludly).Returns None """ self._validRanges.remove(startPosition, endPosition) invalidSpacePercentage = (1 - (float(self._validRanges.size()) / float(self._file.size()))) * 100.0 if invalidSpacePercentage > 25.0 or self._validRanges.size() >= 64: self.pack() def _shift(self, start, end, newStart): if newStart == start: return if end == -1: end = self._file.size(); start, end = min(start, end), max(start, end) oldPosition = self.position() self.setPosition(start) data = self.read(end - start) if newStart > self._file.size(): self._file.resize(newStart) self.setPosition(newStart) self._file.writeData(data) self.setPosition(oldPosition) def _shiftValidRanges(self, fileStartPosition, shiftingValue): ranges = self._validRanges.ranges() firstShiftingRangeIndex = -1 for i in xrange(0, len(ranges)): if ranges[i][0] <= fileStartPosition <= ranges[i][1]: firstShiftingRangeIndex = i break if firstShiftingRangeIndex == -1: return else: firstShiftingRange = ranges[firstShiftingRangeIndex] splitedRanges = [(firstShiftingRange[0], fileStartPosition), (fileStartPosition, firstShiftingRange[1])] self._validRanges.remove(splitedRanges[1][0], ranges[len(ranges) - 1][1]) shiftingRanges = [(splitedRanges[1][0], splitedRanges[1][1])] for i in xrange(firstShiftingRangeIndex + 1, len(ranges)): if ranges[i][0] != ranges[i][1]: shiftingRanges.append(ranges[i]) shiftingInfos = [] for item in shiftingRanges: self._validRanges.add(item[0] + shiftingValue, item[1] + shiftingValue) if (item[0] != item[1]) and (shiftingValue != 0): shiftingInfos.append(RangeShiftingInfo(item, shiftingValue)) if len(shiftingInfos) != 0: for listener in self._packingListeners: listener.filePackedEvent(shiftingInfos) def _validateInteger(self, value): try: return int(value) except (TypeError, AttributeError, ValueError): raise TypeError("Value mast be an integer", {"raisingObject": self, "value": value})
class RangesList_test(unittest.TestCase): def setUp(self): self._list = RangesList(0, 0) def tearDown(self): self._list.clear() def testUnion(self): self._list.add(1, 4) self._list.add(2, 5) self._list.add(0, 2) self._list.add(5, 7) self.assertEqual(self._list.size(), 1, ".size() error") self.assertEqual(self._list.ranges(), [(0, 7)], "Ranges union result error") self._list.add(8, 12) self._list.add(14, 16) self._list.add(15, 17) self._list.add(19, 24) self.assertEqual(self._list.size(), 4, ".size() error") self.assertEqual(self._list.ranges(), [(0, 7), (8, 12), (14, 17), (19, 24)], "Ranges union result error") def testSpliting(self): self._list.add(0, 7) self._list.add(8, 12) self._list.add(14, 17) self._list.add(19, 24) self._list.remove(2, 5) self._list.remove(6, 13) self._list.remove(14, 24) self.assertEqual(self._list.size(), 2, ".size() error") self.assertEquals(self._list.ranges(), [(0, 2), (5, 6)], "Ranges splitting result error")
class MainWindow(QtGui.QMainWindow): """ Main window class """ def __init__(self, controller): """ Default constructor. 'controller' - an AbstractTableController object """ if not isinstance(controller, AbstractTableController): raise TypeError("Controller must be an 'AbstractTableController' type", {"raisingObject": self, "controller": controller}) QtGui.QMainWindow.__init__(self) self.rowsPerPage = 100 self._controller = controller self._ui = Ui_MainWindow() self._ui.setupUi(self) self._visibleRowsRanges = RangesList(0, 0) self._searchDialog = SearchDialog(self) self._searchResultsDialog = SearchResultsDialog(self) self._modifyingFieldDialog = ModifyingFieldDialog(self) self._setFileAndTableOperationsEnabled(False) QtCore.QObject.connect(self._ui.actionOpen, QtCore.SIGNAL("triggered()"), self.openFileAction) QtCore.QObject.connect(self._ui.actionSave, QtCore.SIGNAL("triggered()"), self.saveFileAction) QtCore.QObject.connect(self._ui.actionClose, QtCore.SIGNAL("triggered()"), self.closeFileAction) QtCore.QObject.connect(self._ui.pagesTabs, QtCore.SIGNAL("currentChanged(int)"), self.activeTabChanged) QtCore.QObject.connect(self._ui.actionInsert, QtCore.SIGNAL("triggered()"), self.insertNewRowAction) QtCore.QObject.connect(self._ui.actionRemove, QtCore.SIGNAL("triggered()"), self.removeRowAction) QtCore.QObject.connect(self._ui.actionSearch, QtCore.SIGNAL("triggered()"), self.searchRowAction) QtCore.QObject.connect(self._ui.tableView, QtCore.SIGNAL("doubleClicked(const QModelIndex &)"), self.tableViewDbClickEvent) def openFileAction(self): """ Slot for File->Open item triggered() signal Returns None """ filePath = QtGui.QFileDialog.getOpenFileName(parent = self, caption = QtCore.QString("Open table..."), filter = QtCore.QString("CSV files (*.csv)")) if filePath == None or len(filePath) == 0: return result = self._controller.openTable(filePath) if not result.isOk(): QtGui.QMessageBox.about(self, "Error...", result.message()) return model = result.data() self._ui.tableView.setModel(model) self._visibleRowsRanges.clear() self._visibleRowsRanges.add(0, model.rowCount()) self._distributeToPages() self._selectPage(0) self._setFileAndTableOperationsEnabled(True) def saveFileAction(self): """ Slot for File->Save item triggered() signal Returns None """ self._controller.save() def closeFileAction(self): """ Slot for File->Close item triggered() signal Returns None """ self._controller.save() self._controller.close() self._ui.pagesTabs.clear() self._ui.tableView.setModel(None) def activeTabChanged(self, index): """ Slot for File->Open item triggered() signal Returns None """ self._selectPage(index) def insertNewRowAction(self): """ Slot for pages QTabView object currentChanged(int) signal Returns None """ result = self._controller.insertRow(self._ui.tableView.currentIndex().row()) if not result.isOk(): QtGui.QMessageBox.about(self, "Error...", result.message()) return index = self._visibleRowsRanges.ranges()[0][1] self._visibleRowsRanges.add(index, index + 1) self._distributeToPages() self._selectPage(self._ui.pagesTabs.currentIndex()) def removeRowAction(self): """ Slot for Table->Remove row item triggered() signal Returns None """ result = self._controller.removeRow(self._ui.tableView.currentIndex().row()) if not result.isOk(): QtGui.QMessageBox.about(self, "Error...", result.message()) return self._distributeToPages() self._selectPage(self._ui.pagesTabs.currentIndex()) def tableViewDbClickEvent(self, ARG): """ Slot for QTableView object doubleClicked(const QModelIndex &) signal Returns None """ self._modifyingFieldDialog.show() self._modifyingFieldDialog.focusToEdit() def searchRowAction(self): """ Slot for Table->Search for... item triggered() signal Returns None """ self._searchDialog.show() def searchFor(self, columnKey, key): """ Called by SearchDialog object for searching data. Returns None 'keyColumn' - zero-based column index 'key' - string-convertable object to search """ result = self._controller.searchRows(columnKey, key) if not result.isOk(): QtGui.QMessageBox.about(self, "Error...", result.message()) return self._searchResultsDialog.showResults(result.data()) def modifyCurrentField(self, value): """ Called by ModifyingFieldDialog object for changing currently selected field. Returns None 'value' - new field value """ index = self._ui.tableView.currentIndex() result = self._controller.modifyField(index.row(), index.column(), value) if not result.isOk(): QtGui.QMessageBox.about(self, "Error...", result.message()) return newRow = index.row() newColumn = (index.column() + 1) % self.currentModel().columnCount() if newColumn == 0: newRow = (index.row() + 1) % self.currentModel().rowCount() if newRow == 0: newRow = self.currentModel().rowCount() - 1 self._ui.tableView.setCurrentIndex(self.currentModel().createIndex(newRow, newColumn)) def currentModel(self): """ Returns currently associated with controller model """ if self._controller == None: return None return self._controller.model() def _distributeToPages(self): model = self._controller.model() pages = (model.rowCount() + self.rowsPerPage - 1) / self.rowsPerPage for i in xrange(pages, self._ui.pagesTabs.count()): self._ui.pagesTabs.removeTab(i) for i in xrange(self._ui.pagesTabs.count(), pages): tab = QtGui.QWidget() tab.setObjectName(QtCore.QString("Tab_" + str(i + 1))) self._ui.pagesTabs.addTab(tab, QtCore.QString(str(i + 1))) def _selectPage(self, page): model = self._controller.model() if model == None: return rangeToShow = (page * self.rowsPerPage, min((page + 1) * self.rowsPerPage, model.rowCount())) for item in self._visibleRowsRanges.ranges(): for i in xrange(item[0], item[1]): if i < rangeToShow[0] or i >= rangeToShow[1]: self._ui.tableView.hideRow(i) self._visibleRowsRanges.clear() for i in xrange(rangeToShow[0], rangeToShow[1]): self._ui.tableView.showRow(i) self._visibleRowsRanges.add(rangeToShow[0], rangeToShow[1]) self._ui.tableView.setCurrentIndex(self._controller.model().createIndex(rangeToShow[0], 0)) def _setFileAndTableOperationsEnabled(self, enabled): self._ui.actionClose.setEnabled(enabled) self._ui.actionSave.setEnabled(enabled) self._ui.actionRemove.setEnabled(enabled) self._ui.actionSearch.setEnabled(enabled) self._ui.actionInsert.setEnabled(enabled) if not enabled: self._searchDialog.hide() self._searchResultsDialog.hide() self._modifyingFieldDialog.hide()