Tue, 23 Jul 2019 19:43:14 +0200
MicroPython: continued implementing the file manager widget.
--- a/eric6.e4p Mon Jul 22 20:17:33 2019 +0200 +++ b/eric6.e4p Tue Jul 23 19:43:14 2019 +0200 @@ -459,6 +459,7 @@ <Source>eric6/MicroPython/MicroPythonDevices.py</Source> <Source>eric6/MicroPython/MicroPythonFileManagerWidget.py</Source> <Source>eric6/MicroPython/MicroPythonFileSystem.py</Source> + <Source>eric6/MicroPython/MicroPythonFileSystemUtilities.py</Source> <Source>eric6/MicroPython/MicroPythonGraphWidget.py</Source> <Source>eric6/MicroPython/MicroPythonReplWidget.py</Source> <Source>eric6/MicroPython/MicroPythonSerialPort.py</Source> @@ -2304,14 +2305,14 @@ <Other>docs/THANKS</Other> <Other>docs/changelog</Other> <Other>eric6.e4p</Other> - <Other>eric6/APIs/Python/zope-2.10.7.api</Other> - <Other>eric6/APIs/Python/zope-2.11.2.api</Other> - <Other>eric6/APIs/Python/zope-3.3.1.api</Other> <Other>eric6/APIs/Python3/PyQt4.bas</Other> <Other>eric6/APIs/Python3/PyQt5.bas</Other> <Other>eric6/APIs/Python3/QScintilla2.bas</Other> <Other>eric6/APIs/Python3/eric6.api</Other> <Other>eric6/APIs/Python3/eric6.bas</Other> + <Other>eric6/APIs/Python/zope-2.10.7.api</Other> + <Other>eric6/APIs/Python/zope-2.11.2.api</Other> + <Other>eric6/APIs/Python/zope-3.3.1.api</Other> <Other>eric6/APIs/QSS/qss.api</Other> <Other>eric6/APIs/Ruby/Ruby-1.8.7.api</Other> <Other>eric6/APIs/Ruby/Ruby-1.8.7.bas</Other>
--- a/eric6/MicroPython/MicroPythonFileManagerWidget.py Mon Jul 22 20:17:33 2019 +0200 +++ b/eric6/MicroPython/MicroPythonFileManagerWidget.py Tue Jul 23 19:43:14 2019 +0200 @@ -19,8 +19,9 @@ from .Ui_MicroPythonFileManagerWidget import Ui_MicroPythonFileManagerWidget -from .MicroPythonFileSystem import ( - MicroPythonFileManager, decoratedName, mode2string, mtime2string +from .MicroPythonFileSystem import MicroPythonFileManager +from .MicroPythonFileSystemUtilities import ( + mtime2string, mode2string, decoratedName, listdirStat ) import UI.PixmapCache @@ -44,6 +45,7 @@ super(MicroPythonFileManagerWidget, self).__init__(parent) self.setupUi(self) + self.syncButton.setIcon(UI.PixmapCache.getIcon("2rightarrow")) self.putButton.setIcon(UI.PixmapCache.getIcon("1rightarrow")) self.getButton.setIcon(UI.PixmapCache.getIcon("1leftarrow")) self.localUpButton.setIcon(UI.PixmapCache.getIcon("1uparrow")) @@ -61,9 +63,35 @@ self.__fileManager.longListFiles.connect(self.__handleLongListFiles) self.__fileManager.currentDir.connect(self.__handleCurrentDir) + self.__fileManager.currentDirChanged.connect(self.__handleCurrentDir) + self.__fileManager.putFileDone.connect(self.__handlePutDone) + self.__fileManager.getFileDone.connect(self.__handleGetDone) + self.__fileManager.rsyncDone.connect(self.__handleRsyncDone) + self.__fileManager.rsyncMessages.connect(self.__handleRsyncMessages) self.__fileManager.longListFilesFailed.connect(self.__handleError) self.__fileManager.currentDirFailed.connect(self.__handleError) + self.__fileManager.currentDirChangeFailed.connect(self.__handleError) + self.__fileManager.putFileFailed.connect(self.__handleError) + self.__fileManager.getFileFailed.connect(self.__handleError) + self.__fileManager.rsyncFailed.connect(self.__handleError) + + # TODO: add context menus for panes (separate menus) + # local pane: + # Change Directory + # + # device pane: + # Change Directory + # Create Directory + # Delete Directory + # Delete Directory Tree (= recursive delete) + # ---------------------------- + # Delete File + # ---------------------------- + # Synchronize Time + # Show Time + # ---------------------------- + # Show Version def start(self): """ @@ -139,28 +167,6 @@ @param dirname name of the local directory to be listed @type str """ # __IGNORE_WARNING_D234__ - def isvisible(name): - return not name.startswith(".") and not name.endswith("~") - - def stat(filename): - try: - rstat = os.lstat(filename) - except Exception: - rstat = os.stat(filename) - return tuple(rstat) - - def listdir_stat(dirname): - try: - if dirname: - files = os.listdir(dirname) - else: - files = os.listdir() - except OSError: - return [] - if dirname in ('', '/'): - return [(f, stat(f)) for f in files if isvisible(f)] - return [(f, stat(os.path.join(dirname, f))) for f in files - if isvisible(f)] if not dirname: dirname = os.getcwd() @@ -168,7 +174,7 @@ dirname = dirname[:-1] self.localCwd.setText(dirname) - filesStatList = listdir_stat(dirname) + filesStatList = listdirStat(dirname) filesList = [( decoratedName(f, s[0], os.path.isdir(os.path.join(dirname, f))), mode2string(s[0]), @@ -198,7 +204,7 @@ name = os.path.join(self.localCwd.text(), item.text(0)) if name.endswith("/"): # directory names end with a '/' - self.__listLocalFiles(name) + self.__listLocalFiles(name[:-1]) elif Utilities.MimeTypes.isTextFile(name): e5App().getObject("ViewManager").getEditor(name) @@ -226,41 +232,177 @@ @pyqtSlot(QTreeWidgetItem, int) def on_deviceFileTreeWidget_itemActivated(self, item, column): """ - Slot documentation goes here. + Private slot to handle the activation of a device item. - @param item DESCRIPTION + If the item is a directory, the current working directory is changed + and the list will be re-populated for this directory. + + @param item reference to the activated item @type QTreeWidgetItem - @param column DESCRIPTION + @param column column of the activation @type int """ - # TODO: not implemented yet - # chdir to activated directory triggering a pwd triggering a lls + name = os.path.join(self.deviceCwd.text(), item.text(0)) + if name.endswith("/"): + # directory names end with a '/' + self.__fileManager.cd(name[:-1]) @pyqtSlot() def on_deviceFileTreeWidget_itemSelectionChanged(self): """ - Slot documentation goes here. + Private slot handling a change of selection in the local pane. """ - # TODO: not implemented yet + enable = bool(len(self.deviceFileTreeWidget.selectedItems())) + if enable: + enable &= not ( + self.deviceFileTreeWidget.selectedItems()[0].text(0) + .endswith("/")) + self.getButton.setEnabled(enable) @pyqtSlot() def on_deviceUpButton_clicked(self): """ - Slot documentation goes here. + Private slot to go up one directory level on the device. + """ + cwd = self.deviceCwd.text() + dirname = os.path.dirname(cwd) + self.__fileManager.cd(dirname) + + def __isFileInList(self, filename, treeWidget): """ - # TODO: not implemented yet - raise NotImplementedError + Private method to check, if a file name is contained in a tree widget. + + @param filename name of the file to check + @type str + @param treeWidget reference to the tree widget to be checked against + @return flag indicating that the file name is present + @rtype bool + """ + itemCount = treeWidget.topLevelItemCount() + if itemCount: + for row in range(itemCount): + if treeWidget.topLevelItem(row).text(0) == filename: + return True + + return False @pyqtSlot() def on_putButton_clicked(self): """ - Slot documentation goes here. + Private slot to copy the selected file to the connected device. """ - # TODO: not implemented yet + selectedItems = self.localFileTreeWidget.selectedItems() + if selectedItems: + filename = selectedItems[0].text(0).strip() + if not filename.endswith("/"): + # it is really a file + if self.__isFileInList(filename, self.deviceFileTreeWidget): + # ask for overwrite permission + ok = E5MessageBox.yesNo( + self, + self.tr("Copy File to Device"), + self.tr("<p>The file <b>{0}</b> exists on the" + " connected device. Overwrite it?</p>") + .format(filename) + ) + if not ok: + return + # TODO: allow to rename the new file + + self.__fileManager.put( + os.path.join(self.localCwd.text(), filename), + os.path.join(self.deviceCwd.text(), filename) + ) @pyqtSlot() def on_getButton_clicked(self): """ - Slot documentation goes here. + Private slot to copy the selected file from the connected device. + """ + selectedItems = self.deviceFileTreeWidget.selectedItems() + if selectedItems: + filename = selectedItems[0].text(0).strip() + if not filename.endswith("/"): + # it is really a file + if self.__isFileInList(filename, self.localFileTreeWidget): + # ask for overwrite permission + ok = E5MessageBox.yesNo( + self, + self.tr("Copy File from Device"), + self.tr("<p>The file <b>{0}</b> exists locally." + " Overwrite it?</p>") + .format(filename) + ) + if not ok: + return + # TODO: allow to rename the new file + + self.__fileManager.get( + os.path.join(self.deviceCwd.text(), filename), + os.path.join(self.localCwd.text(), filename) + ) + + @pyqtSlot(str, str) + def __handlePutDone(self, localFile, deviceFile): + """ + Private slot handling a successful copy of a file to the device. + + @param localFile name of the local file + @type str + @param deviceFile name of the file on the device + @type str + """ + self.__fileManager.lls(self.deviceCwd.text()) + + @pyqtSlot(str, str) + def __handleGetDone(self, deviceFile, localFile): """ - # TODO: not implemented yet + Private slot handling a successful copy of a file from the device. + + @param deviceFile name of the file on the device + @type str + @param localFile name of the local file + @type str + """ + self.__listLocalFiles(self.localCwd.text()) + + @pyqtSlot() + def on_syncButton_clicked(self): + """ + Private slot to synchronize the local directory to the device. + """ + self.__fileManager.rsync( + self.localCwd.text(), + self.deviceCwd.text(), + mirror=True + ) + + @pyqtSlot(str, str) + def __handleRsyncDone(self, localDir, deviceDir): + """ + Private method to handle the completion of the rsync operation. + + @param localDir name of the local directory + @type str + @param deviceDir name of the device directory + @type str + """ + self.__listLocalFiles(self.localCwd.text()) + self.__fileManager.lls(self.deviceCwd.text()) + + @pyqtSlot(list) + def __handleRsyncMessages(self, messages): + """ + Private slot to handle messages from the rsync operation. + + @param messages list of message generated by the rsync operation + @type list + """ + E5MessageBox.information( + self, + self.tr("rsync Messages"), + self.tr("""<p>rsync gave the following messages</p>""" + """<ul><li>{0}</li></ul>""").format( + "</li><li>".join(messages) + ) + )
--- a/eric6/MicroPython/MicroPythonFileManagerWidget.ui Mon Jul 22 20:17:33 2019 +0200 +++ b/eric6/MicroPython/MicroPythonFileManagerWidget.ui Tue Jul 23 19:43:14 2019 +0200 @@ -92,6 +92,13 @@ </spacer> </item> <item> + <widget class="QToolButton" name="syncButton"> + <property name="toolTip"> + <string>Press to sync the local directory to the device directory</string> + </property> + </widget> + </item> + <item> <widget class="QToolButton" name="putButton"> <property name="toolTip"> <string>Press to copy the selected file to the device</string> @@ -206,6 +213,7 @@ <tabstops> <tabstop>localFileTreeWidget</tabstop> <tabstop>deviceFileTreeWidget</tabstop> + <tabstop>syncButton</tabstop> <tabstop>putButton</tabstop> <tabstop>getButton</tabstop> <tabstop>localUpButton</tabstop>
--- a/eric6/MicroPython/MicroPythonFileSystem.py Mon Jul 22 20:17:33 2019 +0200 +++ b/eric6/MicroPython/MicroPythonFileSystem.py Tue Jul 23 19:43:14 2019 +0200 @@ -11,20 +11,23 @@ import ast import time +import os import stat -import os from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QThread from .MicroPythonSerialPort import MicroPythonSerialPort +from .MicroPythonFileSystemUtilities import ( + mtime2string, mode2string, decoratedName, listdirStat +) class MicroPythonFileSystem(QObject): """ Class implementing some file system commands for MicroPython. - Some FTP like commands are provided to perform operations on the file - system of a connected MicroPython device. Supported commands are: + Commands are provided to perform operations on the file system of a + connected MicroPython device. Supported commands are: <ul> <li>ls: directory listing</li> <li>lls: directory listing with meta data</li> @@ -33,9 +36,16 @@ <li>put: copy a file to the connected device</li> <li>get: get a file from the connected device</li> <li>rm: remove a file from the connected device</li> + <li>rmrf: remove a file/directory recursively (like 'rm -rf' in bash) <li>mkdir: create a new directory</li> <li>rmdir: remove an empty directory</li> + </ul> + + There are additional commands related to time and version. + <ul> <li>version: get version info about MicroPython</li> + <li>syncTime: synchronize the time of the connected device</li> + <li>showTime: show the current time of the connected device</li> </ul> """ def __init__(self, parent=None): @@ -176,16 +186,20 @@ raise IOError(self.__shortError(err)) return ast.literal_eval(out.decode("utf-8")) - def lls(self, dirname=""): + def lls(self, dirname="", fullstat=False): """ Public method to get a long directory listing of the connected device including meta data. @param dirname name of the directory to be listed @type str - @return list containing the the directory listing with tuple entries - of the name and and a tuple of mode, size and time - @rtype tuple of str + @param fullstat flag indicating to return the full stat() tuple + @type bool + @return list containing the directory listing with tuple entries of + the name and and a tuple of mode, size and time (if fullstat is + false) or the complete stat() tuple. 'None' is returned in case the + directory doesn't exist. + @rtype tuple of (str, tuple) @exception IOError raised to indicate an issue with the device """ commands = [ @@ -203,7 +217,7 @@ " try:", " files = os.listdir(dirname)", " except OSError:", - " return []", + " return None", " if dirname in ('', '/'):", " return list((f, stat(f)) for f in files)", " return list((f, stat(dirname + '/' + f)) for f in files)", @@ -214,7 +228,13 @@ if err: raise IOError(self.__shortError(err)) fileslist = ast.literal_eval(out.decode("utf-8")) - return [(f, (s[0], s[6], s[8])) for f, s in fileslist] + if fileslist is None: + return None + else: + if fullstat: + return fileslist + else: + return [(f, (s[0], s[6], s[8])) for f, s in fileslist] def cd(self, dirname): """ @@ -269,6 +289,54 @@ if err: raise IOError(self.__shortError(err)) + def rmrf(self, name, recursive=False, force=False): + """ + Public method to remove a file or directory recursively. + + @param name of the file or directory to remove + @type str + @param recursive flag indicating a recursive deletion + @type bool + @param force flag indicating to ignore errors + @type bool + @return flag indicating success + @rtype bool + @exception IOError raised to indicate an issue with the device + """ + assert name + + commands = [ + "import os" + "\n".join([ + "def remove_file(name, recursive=False, force=False):", + " try:", + " mode = os.stat(name)[0]", + " if mode & 0x4000 != 0:", + " if recursive:", + " for file in os.listdir(name):", + " success = remove_file(name + '/' + file," + " recursive, force)", + " if not success and not force:", + " return False", + " os.rmdir(name)", + " else:", + " if not force:", + " return False", + " else:", + " os.remove(name)", + " except:", + " if not force:", + " return False", + " return True", + ]), + "print(remove_file('{0}', {1}, {2}))".format(name, recursive, + force), + ] + out, err = self.__execute(commands) + if err: + raise IOError(self.__shortError(err)) + return ast.literal_eval(out.decode("utf-8")) + def mkdir(self, dirname): """ Public method to create a new directory. @@ -376,7 +444,7 @@ "result = True", "\n".join([ "while result:", - " result = r(32)" + " result = r(32)", " if result:", " u.write(result)", ]), @@ -391,8 +459,6 @@ hostFile.write(out) return True - # TODO: add rsync function - def version(self): """ Public method to get the MicroPython version information of the @@ -464,6 +530,24 @@ out, err = self.__execute(commands) if err: raise IOError(self.__shortError(err)) + + def showTime(self): + """ + Public method to get the current time of the device. + + @return time of the device + @rtype str + @exception IOError raised to indicate an issue with the device + """ + commands = [ + "import time", + "print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()))", + # __IGNORE_WARNING_M601__ + ] + out, err = self.__execute(commands) + if err: + raise IOError(self.__shortError(err)) + return out.decode("utf-8").strip() class MicroPythonFileManager(QObject): @@ -475,35 +559,49 @@ name, mode, size and time for each directory entry @signal currentDir(dirname) emitted to report the current directory of the device + @signal currentDirChanged(dirname) emitted to report back a change of the + current directory @signal getFileDone(deviceFile, localFile) emitted after the file was fetched from the connected device and written to the local file system @signal putFileDone(localFile, deviceFile) emitted after the file was copied to the connected device @signal deleteFileDone(deviceFile) emitted after the file has been deleted on the connected device + @signal rsyncDone(localName, deviceName) emitted after the rsync operation + has been completed + @signal rsyncMessages(list) emitted with a list of messages @signal longListFilesFailed(exc) emitted with a failure message to indicate a failed long listing operation @signal currentDirFailed(exc) emitted with a failure message to indicate that the current directory is not available + @signal currentDirChangeFailed(exc) emitted with a failure message to + indicate that the current directory could not be changed @signal getFileFailed(exc) emitted with a failure message to indicate that the file could not be fetched @signal putFileFailed(exc) emitted with a failure message to indicate that the file could not be copied @signal deleteFileFailed(exc) emitted with a failure message to indicate that the file could not be deleted on the device + @signal rsyncFailed(exc) emitted with a failure message to indicate that + the rsync operation could not be completed """ longListFiles = pyqtSignal(tuple) currentDir = pyqtSignal(str) + currentDirChanged = pyqtSignal(str) getFileDone = pyqtSignal(str, str) putFileDone = pyqtSignal(str, str) deleteFileDone = pyqtSignal(str) + rsyncDone = pyqtSignal(str, str) + rsyncMessages = pyqtSignal(list) longListFilesFailed = pyqtSignal(str) currentDirFailed = pyqtSignal(str) + currentDirChangeFailed = pyqtSignal(str) getFileFailed = pyqtSignal(str) putFileFailed = pyqtSignal(str) deleteFileFailed = pyqtSignal(str) + rsyncFailed = pyqtSignal(str) def __init__(self, port, parent=None): """ @@ -566,6 +664,20 @@ self.currentDirFailed.emit(str(exc)) @pyqtSlot(str) + def cd(self, dirname): + """ + Public slot to change the current directory of the device. + + @param dirname name of the desired current directory + @type str + """ + try: + self.__fs.cd(dirname) + self.currentDirChanged.emit(dirname) + except Exception as exc: + self.currentDirChangeFailed.emit(str(exc)) + + @pyqtSlot(str) @pyqtSlot(str, str) def get(self, deviceFileName, hostFileName=""): """ @@ -599,9 +711,9 @@ """ try: self.__fs.put(hostFileName, deviceFileName) - self.putFileDone(hostFileName, deviceFileName) + self.putFileDone.emit(hostFileName, deviceFileName) except Exception as exc: - self.putFileFailed(str(exc)) + self.putFileFailed.emit(str(exc)) @pyqtSlot(str) def delete(self, deviceFileName): @@ -615,53 +727,138 @@ self.__fs.rm(deviceFileName) self.deleteFileDone.emit(deviceFileName) except Exception as exc: - self.deleteFileFailed(str(exc)) - -################################################################## -## Utility methods below -################################################################## - - -def mtime2string(mtime): - """ - Function to convert a time value to a string representation. - - @param mtime time value - @type int - @return string representation of the given time - @rtype str - """ - return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime)) - - -def mode2string(mode): - """ - Function to convert a mode value to a string representation. + self.deleteFileFailed.emit(str(exc)) - @param mode mode value - @type int - @return string representation of the given mode value - @rtype str - """ - return stat.filemode(mode) - - -def decoratedName(name, mode, isDir=False): - """ - Function to decorate the given name according to the given mode. + def __rsync(self, hostDirectory, deviceDirectory, mirror=True): + """ + Private method to synchronize a local directory to the device. + + @param hostDirectory name of the local directory + @type str + @param deviceDirectory name of the directory on the device + @type str + @param mirror flag indicating to mirror the local directory to + the device directory + @type bool + @return tuple containing a list of messages and list of errors + @rtype tuple of (list of str, list of str) + """ + messages = [] + errors = [] + + if not os.isdir(hostDirectory): + return ([], [self.tr( + "The given name '{0}' is not a directory or does not exist.") + .format(hostDirectory) + ]) + + sourceDict = {} + sourceFiles = listdirStat(hostDirectory) + for name, nstat in sourceFiles: + sourceDict[name] = nstat + + destinationDict = {} + try: + destinationFiles = self.__fs.lls(deviceDirectory, fullstat=True) + except Exception as exc: + return ([], [str(exc)]) + if destinationFiles is None: + # the destination directory does not exist + try: + self.__fs.mkdir(deviceDirectory) + except Exception as exc: + return ([], [str(exc)]) + else: + for name, nstat in destinationFiles: + destinationDict[name] = nstat + + destinationSet = set(destinationDict.keys()) + sourceSet = set(sourceDict.keys()) + toAdd = sourceSet - destinationSet # add to dev + toDelete = destinationSet - sourceSet # delete from dev + toUpdate = destinationSet.intersection(sourceSet) # update files + + for sourceBasename in toAdd: + # name exists in source but not in device + sourceFilename = os.path.join(hostDirectory, sourceBasename) + destFilename = deviceDirectory + "/" + sourceBasename + if os.path.isfile(sourceFilename): + try: + self.__fs.put(sourceFilename, destFilename) + except Exception as exc: + messages.append(str(exc)) + if os.path.isdir(sourceFilename): + # recurse + msg, err = self.__rsync(sourceFilename, destFilename, + mirror=mirror) + messages.extend(msg) + errors.extend(err) + + if mirror: + for destBasename in toDelete: + # name exists in device but not local, delete + destFilename = deviceDirectory + "/" + destBasename + try: + self.__fs.rmrf(destFilename, recursive=True, force=True) + except Exception as exc: + # ignore errors here + messages.append(str(exc)) + + for sourceBasename in toUpdate: + # names exist in both; do an update + sourceStat = sourceDict[sourceBasename] + destStat = destinationDict[sourceBasename] + sourceFilename = os.path.join(hostDirectory, sourceBasename) + destFilename = deviceDirectory + "/" + sourceBasename + destMode = destStat[0] + if os.path.isdir(sourceFilename): + if stat.S_ISDIR(destMode): + # both are directories => recurse + msg, err = self.__rsync(sourceFilename, destFilename, + mirror=mirror) + messages.extend(msg) + errors.extend(err) + else: + messages.append(self.tr( + "Source '{0}' is a directory and destination '{1}'" + " is a file. Ignoring it." + ).format(sourceFilename, destFilename)) + else: + if stat.S_ISDIR(destMode): + messages.append(self.tr( + "Source '{0}' is a file and destination '{1}' is" + " a directory. Ignoring it." + ).format(sourceFilename, destFilename)) + else: + if sourceStat[8] > destStat[8]: # mtime + messages.append(self.tr( + "'{0}' is newer than '{1}' - copying" + ).format(sourceFilename, destFilename)) + try: + self.__fs.put(sourceFilename, destFilename) + except Exception as exc: + messages.append(str(exc)) + + return messages, errors - @param name file or directory name - @type str - @param mode mode value - @type int - @param isDir flag indicating that name is a directory - @type bool - @return decorated file or directory name - @rtype str - """ - if stat.S_ISDIR(mode) or isDir: - # append a '/' for directories - return name + "/" - else: - # no change - return name + def rsync(self, hostDirectory, deviceDirectory, mirror=True): + """ + Public method to synchronize a local directory to the device. + + @param hostDirectory name of the local directory + @type str + @param deviceDirectory name of the directory on the device + @type str + @param mirror flag indicating to mirror the local directory to + the device directory + @type bool + """ + messages, errors = self.__rsync(hostDirectory, deviceDirectory, + mirror=mirror) + if errors: + self.rsyncFailed.emit("\n".join(errors)) + + if messages: + self.rsyncMessages.emit(messages) + + self.rsyncDone.emit(hostDirectory, deviceDirectory)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/MicroPythonFileSystemUtilities.py Tue Jul 23 19:43:14 2019 +0200 @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing some file system utility functions. +""" + +from __future__ import unicode_literals + +import time +import stat +import os + + +def mtime2string(mtime): + """ + Function to convert a time value to a string representation. + + @param mtime time value + @type int + @return string representation of the given time + @rtype str + """ + return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime)) + + +def mode2string(mode): + """ + Function to convert a mode value to a string representation. + + @param mode mode value + @type int + @return string representation of the given mode value + @rtype str + """ + return stat.filemode(mode) + + +def decoratedName(name, mode, isDir=False): + """ + Function to decorate the given name according to the given mode. + + @param name file or directory name + @type str + @param mode mode value + @type int + @param isDir flag indicating that name is a directory + @type bool + @return decorated file or directory name + @rtype str + """ + if stat.S_ISDIR(mode) or isDir: + # append a '/' for directories + return name + "/" + else: + # no change + return name + + +def isVisible(name): + """ + Function to check, if a filesystem entry is a hidden file or directory. + + @param name name to be checked + @type str + @return flag indicating a visible filesystem entry + @rtype bool + """ + return not name.startswith(".") and not name.endswith("~") + + +def fstat(filename): + """ + Function to get the stat() of file. + + @param filename name of the file + @type str + @return tuple containing the stat() result + @rtype tuple + """ + try: + rstat = os.lstat(filename) + except Exception: + rstat = os.stat(filename) + return tuple(rstat) + + +def listdirStat(dirname): + """ + Function to get a list of directory entries and associated stat() tuples. + + @param dirname name of the directory to list + @type str + @return list of tuples containing the entry name and the associated + stat() tuple + @rtype list of tuple of (str, tuple) + """ + try: + if dirname: + files = os.listdir(dirname) + else: + files = os.listdir() + except OSError: + return [] + + if dirname in ('', '/'): + return [(f, fstat(f)) for f in files if isVisible(f)] + + return [(f, fstat(os.path.join(dirname, f))) for f in files + if isVisible(f)]
--- a/eric6/MicroPython/MicroPythonReplWidget.py Mon Jul 22 20:17:33 2019 +0200 +++ b/eric6/MicroPython/MicroPythonReplWidget.py Tue Jul 23 19:43:14 2019 +0200 @@ -334,6 +334,7 @@ menu.addAction(self.tr("Copy"), self.replEdit.copy, copyKeys) menu.addAction(self.tr("Paste"), self.__paste, pasteKeys) menu.addSeparator() + # TODO: add device specific context menu entries menu.exec_(self.replEdit.mapToGlobal(pos)) def setConnected(self, connected): @@ -416,8 +417,6 @@ if self.__plotterRunning: self.on_chartButton_clicked() - - # TODO: add more def __disconnectSerial(self): """