MicroPythonFileManager: finished the file access via a local directory. micropython

Mon, 12 Aug 2019 14:53:07 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 12 Aug 2019 14:53:07 +0200
branch
micropython
changeset 7131
f75e990caf99
parent 7130
6014d37d9683
child 7132
c4682cfcd160

MicroPythonFileManager: finished the file access via a local directory.

eric6/MicroPython/MicroPythonFileManager.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonFileManagerWidget.py file | annotate | diff | comparison | revisions
--- a/eric6/MicroPython/MicroPythonFileManager.py	Sat Aug 10 20:06:37 2019 +0200
+++ b/eric6/MicroPython/MicroPythonFileManager.py	Mon Aug 12 14:53:07 2019 +0200
@@ -11,6 +11,7 @@
 
 import os
 import stat
+import shutil
 
 from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject
 
@@ -171,7 +172,8 @@
         except Exception as exc:
             self.error.emit("delete", str(exc))
     
-    def __rsync(self, hostDirectory, deviceDirectory, mirror=True):
+    def __rsync(self, hostDirectory, deviceDirectory, mirror=True,
+                localDevice=False):
         """
         Private method to synchronize a local directory to the device.
         
@@ -182,6 +184,8 @@
         @param mirror flag indicating to mirror the local directory to
             the device directory
         @type bool
+        @param localDevice flag indicating device access via local file system
+        @type bool
         @return list of errors
         @rtype list of str
         """
@@ -197,26 +201,40 @@
             self.tr("Synchronizing <b>{0}</b>.").format(deviceDirectory)
         )
         
+        doneMessage = self.tr("Done synchronizing <b>{0}</b>.").format(
+            deviceDirectory)
+        
         sourceDict = {}
         sourceFiles = listdirStat(hostDirectory)
         for name, nstat in sourceFiles:
             sourceDict[name] = nstat
         
         destinationDict = {}
-        try:
-            destinationFiles = self.__commandsInterface.lls(deviceDirectory,
-                                                            fullstat=True)
-        except Exception as exc:
-            return [str(exc)]
-        if destinationFiles is None:
-            # the destination directory does not exist
+        if localDevice:
+            if not os.path.isdir(deviceDirectory):
+                # simply copy destination to source
+                shutil.copytree(hostDirectory, deviceDirectory)
+                self.rsyncProgressMessage.emit(doneMessage)
+                return errors
+            else:
+                destinationFiles = listdirStat(deviceDirectory)
+                for name, nstat in destinationFiles:
+                    destinationDict[name] = nstat
+        else:
             try:
-                self.__commandsInterface.mkdir(deviceDirectory)
+                destinationFiles = self.__commandsInterface.lls(deviceDirectory,
+                                                                fullstat=True)
             except Exception as exc:
                 return [str(exc)]
-        else:
-            for name, nstat in destinationFiles:
-                destinationDict[name] = nstat
+            if destinationFiles is None:
+                # the destination directory does not exist
+                try:
+                    self.__commandsInterface.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())
@@ -224,87 +242,153 @@
         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
-            self.rsyncProgressMessage.emit(
-                self.tr("Adding <b>{0}</b>...").format(destFilename))
-            if os.path.isfile(sourceFilename):
-                try:
-                    self.__commandsInterface.put(sourceFilename, destFilename)
-                except Exception as exc:
+        if localDevice:
+            for sourceBasename in toAdd:
+                # name exists in source but not in device
+                sourceFilename = os.path.join(hostDirectory, sourceBasename)
+                destFilename = os.path.join(deviceDirectory, sourceBasename)
+                self.rsyncProgressMessage.emit(
+                    self.tr("Adding <b>{0}</b>...").format(destFilename))
+                if os.path.isfile(sourceFilename):
+                    shutil.copy2(sourceFilename, destFilename)
+                elif os.path.isdir(sourceFilename):
+                    # recurse
+                    errs = self.__rsync(sourceFilename, destFilename,
+                                        mirror=mirror, localDevice=localDevice)
                     # just note issues but ignore them otherwise
-                    errors.append(str(exc))
-            if os.path.isdir(sourceFilename):
-                # recurse
-                errs = self.__rsync(sourceFilename, destFilename,
-                                    mirror=mirror)
-                # just note issues but ignore them otherwise
-                errors.extend(errs)
-        
-        if mirror:
-            for destBasename in toDelete:
-                # name exists in device but not local, delete
-                destFilename = deviceDirectory + "/" + destBasename
+                    errors.extend(errs)
+            
+            if mirror:
+                for destBasename in toDelete:
+                    # name exists in device but not local, delete
+                    destFilename = os.path.join(deviceDirectory, destBasename)
+                    if os.path.isdir(destFilename):
+                        shutil.rmtree(destFilename, ignore_errors=True)
+                    elif os.path.isfile(destFilename):
+                        os.remove(destFilename)
+            
+            for sourceBasename in toUpdate:
+                # names exist in both; do an update
+                sourceStat = sourceDict[sourceBasename]
+                destStat = destinationDict[sourceBasename]
+                sourceFilename = os.path.join(hostDirectory, sourceBasename)
+                destFilename = os.path.join(deviceDirectory, sourceBasename)
+                destMode = destStat[0]
+                if os.path.isdir(sourceFilename):
+                    if os.path.isdir(destFilename):
+                        # both are directories => recurs
+                        errs = self.__rsync(sourceFilename, destFilename,
+                                            mirror=mirror,
+                                            localDevice=localDevice)
+                        # just note issues but ignore them otherwise
+                        errors.extend(errs)
+                    else:
+                        self.rsyncProgressMessage.emit(
+                            self.tr("Source <b>{0}</b> is a directory and"
+                                    " destination <b>{1}</b> is a file."
+                                    " Ignoring it.")
+                            .format(sourceFilename, destFilename)
+                        )
+                else:
+                    if os.path.isdir(destFilename):
+                        self.rsyncProgressMessage.emit(
+                            self.tr("Source <b>{0}</b> is a file and"
+                                    " destination <b>{1}</b> is a directory."
+                                    " Ignoring it.")
+                            .format(sourceFilename, destFilename)
+                        )
+                    else:
+                        if sourceStat[8] > destStat[8]:     # mtime
+                            self.rsyncProgressMessage.emit(
+                                self.tr("Updating <b>{0}</b>...")
+                                .format(destFilename)
+                            )
+                        shutil.copy2(sourceFilename, destFilename)
+        else:
+            for sourceBasename in toAdd:
+                # name exists in source but not in device
+                sourceFilename = os.path.join(hostDirectory, sourceBasename)
+                destFilename = deviceDirectory + "/" + sourceBasename
                 self.rsyncProgressMessage.emit(
-                    self.tr("Removing <b>{0}</b>...").format(destFilename))
-                try:
-                    self.__commandsInterface.rmrf(destFilename, recursive=True,
-                                                  force=True)
-                except Exception as exc:
-                    # just note issues but ignore them otherwise
-                    errors.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 => recurs
+                    self.tr("Adding <b>{0}</b>...").format(destFilename))
+                if os.path.isfile(sourceFilename):
+                    try:
+                        self.__commandsInterface.put(sourceFilename,
+                                                     destFilename)
+                    except Exception as exc:
+                        # just note issues but ignore them otherwise
+                        errors.append(str(exc))
+                elif os.path.isdir(sourceFilename):
+                    # recurse
                     errs = self.__rsync(sourceFilename, destFilename,
                                         mirror=mirror)
                     # just note issues but ignore them otherwise
                     errors.extend(errs)
-                else:
+        
+            if mirror:
+                for destBasename in toDelete:
+                    # name exists in device but not local, delete
+                    destFilename = deviceDirectory + "/" + destBasename
                     self.rsyncProgressMessage.emit(
-                        self.tr("Source <b>{0}</b> is a directory and"
-                                " destination <b>{1}</b> is a file. Ignoring"
-                                " it.")
-                        .format(sourceFilename, destFilename)
-                    )
-            else:
-                if stat.S_ISDIR(destMode):
-                    self.rsyncProgressMessage.emit(
-                        self.tr("Source <b>{0}</b> is a file and destination"
-                                " <b>{1}</b> is a directory. Ignoring it.")
-                        .format(sourceFilename, destFilename)
-                    )
+                        self.tr("Removing <b>{0}</b>...").format(destFilename))
+                    try:
+                        self.__commandsInterface.rmrf(destFilename,
+                                                      recursive=True,
+                                                      force=True)
+                    except Exception as exc:
+                        # just note issues but ignore them otherwise
+                        errors.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 => recurs
+                        errs = self.__rsync(sourceFilename, destFilename,
+                                            mirror=mirror)
+                        # just note issues but ignore them otherwise
+                        errors.extend(errs)
+                    else:
+                        self.rsyncProgressMessage.emit(
+                            self.tr("Source <b>{0}</b> is a directory and"
+                                    " destination <b>{1}</b> is a file."
+                                    " Ignoring it.")
+                            .format(sourceFilename, destFilename)
+                        )
                 else:
-                    if sourceStat[8] > destStat[8]:     # mtime
+                    if stat.S_ISDIR(destMode):
                         self.rsyncProgressMessage.emit(
-                            self.tr("Updating <b>{0}</b>...")
-                            .format(destFilename)
+                            self.tr("Source <b>{0}</b> is a file and"
+                                    " destination <b>{1}</b> is a directory."
+                                    " Ignoring it.")
+                            .format(sourceFilename, destFilename)
                         )
-                        try:
-                            self.__commandsInterface.put(sourceFilename,
-                                                         destFilename)
-                        except Exception as exc:
-                            errors.append(str(exc))
+                    else:
+                        if sourceStat[8] > destStat[8]:     # mtime
+                            self.rsyncProgressMessage.emit(
+                                self.tr("Updating <b>{0}</b>...")
+                                .format(destFilename)
+                            )
+                            try:
+                                self.__commandsInterface.put(sourceFilename,
+                                                             destFilename)
+                            except Exception as exc:
+                                errors.append(str(exc))
         
-        self.rsyncProgressMessage.emit(
-            self.tr("Done synchronizing <b>{0}</b>.").format(deviceDirectory)
-        )
+        self.rsyncProgressMessage.emit(doneMessage)
         
         return errors
     
     @pyqtSlot(str, str)
     @pyqtSlot(str, str, bool)
-    def rsync(self, hostDirectory, deviceDirectory, mirror=True):
+    @pyqtSlot(str, str, bool, bool)
+    def rsync(self, hostDirectory, deviceDirectory, mirror=True,
+              localDevice=False):
         """
         Public slot to synchronize a local directory to the device.
         
@@ -315,8 +399,11 @@
         @param mirror flag indicating to mirror the local directory to
             the device directory
         @type bool
+        @param localDevice flag indicating device access via local file system
+        @type bool
         """
-        errors = self.__rsync(hostDirectory, deviceDirectory, mirror=mirror)
+        errors = self.__rsync(hostDirectory, deviceDirectory, mirror=mirror,
+                              localDevice=localDevice)
         if errors:
             self.error.emit("rsync", "\n".join(errors))
         
--- a/eric6/MicroPython/MicroPythonFileManagerWidget.py	Sat Aug 10 20:06:37 2019 +0200
+++ b/eric6/MicroPython/MicroPythonFileManagerWidget.py	Mon Aug 12 14:53:07 2019 +0200
@@ -11,7 +11,6 @@
 
 import os
 import shutil
-import time
 
 from PyQt5.QtCore import pyqtSlot, Qt, QPoint
 from PyQt5.QtWidgets import (
@@ -116,9 +115,6 @@
         self.__localMenu.addSeparator()
         self.__localDelFileAct = self.__localMenu.addAction(
             self.tr("Delete File"), self.__deleteLocalFile)
-        self.__localMenu.addSeparator()
-        self.__localMenu.addAction(
-            self.tr("Show Time"), self.__showLocalTime)
         
         self.__deviceMenu = QMenu(self)
         if not self.__repl.isMicrobit():
@@ -126,8 +122,9 @@
                 self.tr("Change Directory"), self.__changeDeviceDirectory)
             self.__deviceMenu.addAction(
                 self.tr("Create Directory"), self.__createDeviceDirectory)
-            self.__devDelDirAct = self.__deviceMenu.addAction(
-                self.tr("Delete Directory"), self.__deleteDeviceDirectory)
+            if not self.__deviceWithLocalAccess:
+                self.__devDelDirAct = self.__deviceMenu.addAction(
+                    self.tr("Delete Directory"), self.__deleteDeviceDirectory)
             self.__devDelDirTreeAct = self.__deviceMenu.addAction(
                 self.tr("Delete Directory Tree"),
                 self.__deleteDeviceDirectoryTree)
@@ -298,7 +295,6 @@
         dirname = self.localCwd.text()
         self.__listLocalFiles(dirname)
     
-    # TODO:
     @pyqtSlot(QTreeWidgetItem, int)
     def on_deviceFileTreeWidget_itemActivated(self, item, column):
         """
@@ -313,9 +309,16 @@
         @type int
         """
         name = os.path.join(self.deviceCwd.text(), item.text(0))
-        if name.endswith("/"):
-            # directory names end with a '/'
-            self.__fileManager.cd(name[:-1])
+        if self.__deviceWithLocalAccess:
+            if name.endswith("/"):
+                # directory names end with a '/'
+                self.__listLocalFiles(name[:-1], True)
+            elif Utilities.MimeTypes.isTextFile(name):
+                e5App().getObject("ViewManager").getEditor(name)
+        else:
+            if name.endswith("/"):
+                # directory names end with a '/'
+                self.__fileManager.cd(name[:-1])
     
     @pyqtSlot()
     def on_deviceFileTreeWidget_itemSelectionChanged(self):
@@ -330,7 +333,6 @@
         self.getButton.setEnabled(enable)
         self.getAsButton.setEnabled(enable)
     
-    # TODO:
     @pyqtSlot()
     def on_deviceUpButton_clicked(self):
         """
@@ -338,19 +340,24 @@
         """
         cwd = self.deviceCwd.text()
         dirname = os.path.dirname(cwd)
-        self.__fileManager.cd(dirname)
+        if self.__deviceWithLocalAccess:
+            self.__listLocalFiles(dirname, True)
+        else:
+            self.__fileManager.cd(dirname)
     
-    # TODO:
     @pyqtSlot()
     def on_deviceReloadButton_clicked(self):
         """
         Private slot to reload the device list.
         """
         dirname = self.deviceCwd.text()
-        if dirname:
-            self.__fileManager.lls(dirname)
+        if self.__deviceWithLocalAccess:
+            self.__listLocalFiles(dirname, True)
         else:
-            self.__fileManager.pwd()
+            if dirname:
+                self.__fileManager.lls(dirname)
+            else:
+                self.__fileManager.pwd()
     
     def __isFileInList(self, filename, treeWidget):
         """
@@ -370,7 +377,6 @@
         
         return False
     
-    # TODO:
     @pyqtSlot()
     def on_putButton_clicked(self, putAs=False):
         """
@@ -409,13 +415,21 @@
                     elif action == "rename":
                         deviceFilename = os.path.basename(resultFilename)
                 
-                deviceCwd = self.deviceCwd.text()
-                if deviceCwd:
-                    deviceFilename = deviceCwd + "/" + deviceFilename
-                self.__fileManager.put(
-                    os.path.join(self.localCwd.text(), filename),
-                    deviceFilename
-                )
+                if self.__deviceWithLocalAccess:
+                    shutil.copy2(
+                        os.path.join(self.localCwd.text(), filename),
+                        os.path.join(self.deviceCwd.text(), deviceFilename)
+                    )
+                    self.__listLocalFiles(self.deviceCwd.text(),
+                                          localDevice=True)
+                else:
+                    deviceCwd = self.deviceCwd.text()
+                    if deviceCwd and deviceCwd != "/":
+                        deviceFilename = deviceCwd + "/" + deviceFilename
+                    self.__fileManager.put(
+                        os.path.join(self.localCwd.text(), filename),
+                        deviceFilename
+                    )
     
     @pyqtSlot()
     def on_putAsButton_clicked(self):
@@ -425,7 +439,6 @@
         """
         self.on_putButton_clicked(putAs=True)
     
-    # TODO:
     @pyqtSlot()
     def on_getButton_clicked(self, getAs=False):
         """
@@ -463,16 +476,20 @@
                     elif action == "rename":
                         localFilename = resultFilename
                 
-                deviceCwd = self.deviceCwd.text()
-                if deviceCwd:
-                    filename = deviceCwd + "/" + filename
-                if not os.path.isabs(localFilename):
-                    localFilename = os.path.join(self.localCwd.text(),
-                                                 localFilename)
-                self.__fileManager.get(
-                    filename,
-                    localFilename
-                )
+                if self.__deviceWithLocalAccess:
+                    shutil.copy2(
+                        os.path.join(self.deviceCwd.text(), filename),
+                        os.path.join(self.localCwd.text(), localFilename)
+                    )
+                    self.__listLocalFiles(self.localCwd.text())
+                else:
+                    deviceCwd = self.deviceCwd.text()
+                    if deviceCwd:
+                        filename = deviceCwd + "/" + filename
+                    self.__fileManager.get(
+                        filename,
+                        os.path.join(self.localCwd.text(), localFilename)
+                    )
     
     @pyqtSlot()
     def on_getAsButton_clicked(self):
@@ -494,7 +511,6 @@
         """
         self.__listLocalFiles(self.localCwd.text())
     
-    # TODO:
     @pyqtSlot()
     def on_syncButton_clicked(self):
         """
@@ -503,7 +519,8 @@
         self.__fileManager.rsync(
             self.localCwd.text(),
             self.deviceCwd.text(),
-            mirror=True
+            mirror=True,
+            localDevice=self.__deviceWithLocalAccess,
         )
     
     @pyqtSlot(str, str)
@@ -675,15 +692,24 @@
                             dirname, str(exc))
                     )
     
-    # TODO:
     @pyqtSlot()
-    def __deleteLocalFile(self):
+    def __deleteLocalFile(self, localDevice=False):
         """
         Private slot to delete a local file.
+        
+        @param localDevice flag indicating device access via local file system
+        @type bool
         """
-        if bool(len(self.localFileTreeWidget.selectedItems())):
-            name = self.localFileTreeWidget.selectedItems()[0].text(0)
-            filename = os.path.join(self.localCwd.text(), name)
+        if localDevice:
+            cwdWidget = self.deviceCwd
+            fileTreeWidget = self.deviceFileTreeWidget
+        else:
+            cwdWidget = self.localCwd
+            fileTreeWidget = self.localFileTreeWidget
+        
+        if bool(len(fileTreeWidget.selectedItems())):
+            name = fileTreeWidget.selectedItems()[0].text(0)
+            filename = os.path.join(cwdWidget.text(), name)
             dlg = DeleteFilesConfirmationDialog(
                 self,
                 self.tr("Delete File"),
@@ -693,7 +719,8 @@
             if dlg.exec_() == QDialog.Accepted:
                 try:
                     os.remove(filename)
-                    self.__listLocalFiles(self.localCwd.text())
+                    self.__listLocalFiles(cwdWidget.text(),
+                                          localDevice=localDevice)
                 except (OSError, IOError) as exc:
                     E5MessageBox.critical(
                         self,
@@ -703,26 +730,6 @@
                             filename, str(exc))
                     )
     
-    # TODO: move to repl widget super menu
-    @pyqtSlot()
-    def __showLocalTime(self):
-        """
-        Private slot to show the local date and time.
-        """
-        localdatetime = time.localtime()
-        loacldate = time.strftime('%Y-%m-%d', localdatetime)
-        localtime = time.strftime('%H:%M:%S', localdatetime)
-        E5MessageBox.information(
-            self,
-            self.tr("Local Date and Time"),
-            self.tr("<h3>Local Date and Time</h3>"
-                    "<table>"
-                    "<tr><td><b>Date</b></td><td>{0}</td></tr>"
-                    "<tr><td><b>Time</b></td><td>{1}</td></tr>"
-                    "</table>"
-                    ).format(loacldate, localtime)
-        )
-    
     ##################################################################
     ## Context menu methods for the device files below
     ##################################################################
@@ -744,7 +751,8 @@
             isDir = False
             isFile = False
         if not self.__repl.isMicrobit():
-            self.__devDelDirAct.setEnabled(isDir)
+            if not self.__deviceWithLocalAccess:
+                self.__devDelDirAct.setEnabled(isDir)
             self.__devDelDirTreeAct.setEnabled(isDir)
         self.__devDelFileAct.setEnabled(isFile)
         
@@ -798,7 +806,11 @@
         else:
             if bool(len(self.deviceFileTreeWidget.selectedItems())):
                 name = self.deviceFileTreeWidget.selectedItems()[0].text(0)
-                dirname = self.deviceCwd.text() + "/" + name[:-1]
+                cwd = self.deviceCwd.text()
+                if cwd and cwd != "/":
+                    dirname = cwd + "/" + name[:-1]
+                else:
+                    dirname = "/" + name[:-1]
                 dlg = DeleteFilesConfirmationDialog(
                     self,
                     self.tr("Delete Directory"),
@@ -819,7 +831,11 @@
         else:
             if bool(len(self.deviceFileTreeWidget.selectedItems())):
                 name = self.deviceFileTreeWidget.selectedItems()[0].text(0)
-                dirname = self.deviceCwd.text() + "/" + name[:-1]
+                cwd = self.deviceCwd.text()
+                if cwd and cwd != "/":
+                    dirname = cwd + "/" + name[:-1]
+                else:
+                    dirname = "/" + name[:-1]
                 dlg = DeleteFilesConfirmationDialog(
                     self,
                     self.tr("Delete Directory Tree"),
@@ -829,27 +845,29 @@
                 if dlg.exec_() == QDialog.Accepted:
                     self.__fileManager.rmdir(dirname, recursive=True)
     
-    # TODO:
     @pyqtSlot()
     def __deleteDeviceFile(self):
         """
         Private slot to delete a file.
         """
-        if bool(len(self.deviceFileTreeWidget.selectedItems())):
-            name = self.deviceFileTreeWidget.selectedItems()[0].text(0)
-            dirname = self.deviceCwd.text()
-            if dirname:
-                filename = dirname + "/" + name
-            else:
-                filename = name
-            dlg = DeleteFilesConfirmationDialog(
-                self,
-                self.tr("Delete File"),
-                self.tr(
-                    "Do you really want to delete this file?"),
-                [filename])
-            if dlg.exec_() == QDialog.Accepted:
-                self.__fileManager.delete(filename)
+        if self.__deviceWithLocalAccess:
+            self.__deleteLocalFile(True)
+        else:
+            if bool(len(self.deviceFileTreeWidget.selectedItems())):
+                name = self.deviceFileTreeWidget.selectedItems()[0].text(0)
+                dirname = self.deviceCwd.text()
+                if dirname and dirname != "/":
+                    filename = dirname + "/" + name
+                else:
+                    filename = "/" + name
+                dlg = DeleteFilesConfirmationDialog(
+                    self,
+                    self.tr("Delete File"),
+                    self.tr(
+                        "Do you really want to delete this file?"),
+                    [filename])
+                if dlg.exec_() == QDialog.Accepted:
+                    self.__fileManager.delete(filename)
     
     @pyqtSlot()
     def __showFileSystemInfo(self):

eric ide

mercurial