src/eric7/SystemUtilities/FileSystemUtilities.py

branch
eric7
changeset 9624
b47dfa7a137d
child 9639
9e66fd88193c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/SystemUtilities/FileSystemUtilities.py	Sun Dec 18 19:33:46 2022 +0100
@@ -0,0 +1,612 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing file system related utility functions.
+"""
+
+import contextlib
+import ctypes
+import fnmatch
+import os
+import pathlib
+import subprocess
+
+from eric7.SystemUtilities import OSUtilities
+
+
+def toNativeSeparators(path):
+    """
+    Function returning a path, that is using native separator characters.
+
+    @param path path to be converted
+    @type str
+    @return path with converted separator characters
+    @rtype str
+    """
+    return str(pathlib.PurePath(path)) if bool(path) else ""
+
+
+def fromNativeSeparators(path):
+    """
+    Function returning a path, that is using "/" separator characters.
+
+    @param path path to be converted
+    @type str
+    @return path with converted separator characters
+    @rtype str
+    """
+    return pathlib.PurePath(path).as_posix() if bool(path) else ""
+
+
+def normcasepath(path):
+    """
+    Function returning a path, that is normalized with respect to its case
+    and references.
+
+    @param path file path (string)
+    @return case normalized path (string)
+    """
+    return os.path.normcase(os.path.normpath(path))
+
+
+def normcaseabspath(path):
+    """
+    Function returning an absolute path, that is normalized with respect to
+    its case and references.
+
+    @param path file path (string)
+    @return absolute, normalized path (string)
+    """
+    return os.path.normcase(os.path.abspath(path))
+
+
+def normjoinpath(a, *p):
+    """
+    Function returning a normalized path of the joined parts passed into it.
+
+    @param a first path to be joined (string)
+    @param p variable number of path parts to be joined (string)
+    @return normalized path (string)
+    """
+    return os.path.normpath(os.path.join(a, *p))
+
+
+def normabsjoinpath(a, *p):
+    """
+    Function returning a normalized, absolute path of the joined parts passed
+    into it.
+
+    @param a first path to be joined (string)
+    @param p variable number of path parts to be joind (string)
+    @return absolute, normalized path (string)
+    """
+    return os.path.abspath(os.path.join(a, *p))
+
+
+def isinpath(file):
+    """
+    Function to check for an executable file.
+
+    @param file filename of the executable to check (string)
+    @return flag to indicate, if the executable file is accessible
+        via the searchpath defined by the PATH environment variable.
+    """
+    if os.path.isabs(file):
+        return os.access(file, os.X_OK)
+
+    if os.path.exists(os.path.join(os.curdir, file)):
+        return os.access(os.path.join(os.curdir, file), os.X_OK)
+
+    path = OSUtilities.getEnvironmentEntry("PATH")
+
+    # environment variable not defined
+    if path is None:
+        return False
+
+    dirs = path.split(os.pathsep)
+    return any(os.access(os.path.join(directory, file), os.X_OK) for directory in dirs)
+
+
+def startswithPath(path, start):
+    """
+    Function to check, if a path starts with a given start path.
+
+    @param path path to be checked
+    @type str
+    @param start start path
+    @type str
+    @return flag indicating that the path starts with the given start
+        path
+    @rtype bool
+    """
+    return bool(start) and (
+        path == start or normcasepath(path).startswith(normcasepath(start + "/"))
+    )
+
+
+def relativeUniversalPath(path, start):
+    """
+    Function to convert a file path to a path relative to a start path
+    with universal separators.
+
+    @param path file or directory name to convert (string)
+    @param start start path (string)
+    @return relative path or unchanged path, if path does not start with
+        the start path with universal separators (string)
+    """
+    return fromNativeSeparators(os.path.relpath(path, start))
+
+
+def absolutePath(path, start):
+    """
+    Public method to convert a path relative to a start path to an
+    absolute path.
+
+    @param path file or directory name to convert (string)
+    @param start start path (string)
+    @return absolute path (string)
+    """
+    if not os.path.isabs(path):
+        path = os.path.normpath(os.path.join(start, path))
+    return path
+
+
+def absoluteUniversalPath(path, start):
+    """
+    Public method to convert a path relative to a start path with
+    universal separators to an absolute path.
+
+    @param path file or directory name to convert (string)
+    @param start start path (string)
+    @return absolute path with native separators (string)
+    """
+    if not os.path.isabs(path):
+        path = toNativeSeparators(os.path.normpath(os.path.join(start, path)))
+    return path
+
+
+def getExecutablePath(file):
+    """
+    Function to build the full path of an executable file from the environment.
+
+    @param file filename of the executable to check (string)
+    @return full executable name, if the executable file is accessible
+        via the searchpath defined by the PATH environment variable, or an
+        empty string otherwise.
+    """
+    if os.path.isabs(file):
+        if os.access(file, os.X_OK):
+            return file
+        else:
+            return ""
+
+    cur_path = os.path.join(os.curdir, file)
+    if os.path.exists(cur_path) and os.access(cur_path, os.X_OK):
+        return cur_path
+
+    path = os.getenv("PATH")
+
+    # environment variable not defined
+    if path is None:
+        return ""
+
+    dirs = path.split(os.pathsep)
+    for directory in dirs:
+        exe = os.path.join(directory, file)
+        if os.access(exe, os.X_OK):
+            return exe
+
+    return ""
+
+
+def getExecutablePaths(file):
+    """
+    Function to build all full path of an executable file from the environment.
+
+    @param file filename of the executable (string)
+    @return list of full executable names (list of strings), if the executable
+        file is accessible via the searchpath defined by the PATH environment
+        variable, or an empty list otherwise.
+    """
+    paths = []
+
+    if os.path.isabs(file):
+        if os.access(file, os.X_OK):
+            return [file]
+        else:
+            return []
+
+    cur_path = os.path.join(os.curdir, file)
+    if os.path.exists(cur_path) and os.access(cur_path, os.X_OK):
+        paths.append(cur_path)
+
+    path = os.getenv("PATH")
+
+    # environment variable not defined
+    if path is not None:
+        dirs = path.split(os.pathsep)
+        for directory in dirs:
+            exe = os.path.join(directory, file)
+            if os.access(exe, os.X_OK) and exe not in paths:
+                paths.append(exe)
+
+    return paths
+
+
+def getWindowsExecutablePath(file):
+    """
+    Function to build the full path of an executable file from the environment
+    on Windows platforms.
+
+    First an executable with the extension .exe is searched for, thereafter
+    such with the extensions .cmd or .bat and finally the given file name as
+    is. The first match is returned.
+
+    @param file filename of the executable to check (string)
+    @return full executable name, if the executable file is accessible
+        via the searchpath defined by the PATH environment variable, or an
+        empty string otherwise.
+    """
+    if os.path.isabs(file):
+        if os.access(file, os.X_OK):
+            return file
+        else:
+            return ""
+
+    filenames = [file + ".exe", file + ".cmd", file + ".bat", file]
+
+    for filename in filenames:
+        cur_path = os.path.join(os.curdir, filename)
+        if os.path.exists(cur_path) and os.access(cur_path, os.X_OK):
+            return os.path.abspath(cur_path)
+
+    path = os.getenv("PATH")
+
+    # environment variable not defined
+    if path is None:
+        return ""
+
+    dirs = path.split(os.pathsep)
+    for directory in dirs:
+        for filename in filenames:
+            exe = os.path.join(directory, filename)
+            if os.access(exe, os.X_OK):
+                return exe
+
+    return ""
+
+
+def isExecutable(exe):
+    """
+    Function to check, if a file is executable.
+
+    @param exe filename of the executable to check (string)
+    @return flag indicating executable status (boolean)
+    """
+    return os.access(exe, os.X_OK)
+
+
+def isDrive(path):
+    """
+    Function to check, if a path is a Windows drive.
+
+    @param path path name to be checked
+    @type str
+    @return flag indicating a Windows drive
+    @rtype bool
+    """
+    isWindowsDrive = False
+    drive, directory = os.path.splitdrive(path)
+    if (
+        drive
+        and len(drive) == 2
+        and drive.endswith(":")
+        and directory in ["", "\\", "/"]
+    ):
+        isWindowsDrive = True
+
+    return isWindowsDrive
+
+
+def samepath(f1, f2):
+    """
+    Function to compare two paths.
+
+    @param f1 first path for the compare (string)
+    @param f2 second path for the compare (string)
+    @return flag indicating whether the two paths represent the
+        same path on disk.
+    """
+    if f1 is None or f2 is None:
+        return False
+
+    if normcaseabspath(os.path.realpath(f1)) == normcaseabspath(os.path.realpath(f2)):
+        return True
+
+    return False
+
+
+def samefilepath(f1, f2):
+    """
+    Function to compare two paths. Strips the filename.
+
+    @param f1 first filepath for the compare (string)
+    @param f2 second filepath for the compare (string)
+    @return flag indicating whether the two paths represent the
+        same path on disk.
+    """
+    if f1 is None or f2 is None:
+        return False
+
+    if normcaseabspath(os.path.dirname(os.path.realpath(f1))) == normcaseabspath(
+        os.path.dirname(os.path.realpath(f2))
+    ):
+        return True
+
+    return False
+
+
+try:
+    EXTSEP = os.extsep
+except AttributeError:
+    EXTSEP = "."
+
+
+def splitPath(name):
+    """
+    Function to split a pathname into a directory part and a file part.
+
+    @param name path name (string)
+    @return a tuple of 2 strings (dirname, filename).
+    """
+    if os.path.isdir(name):
+        dn = os.path.abspath(name)
+        fn = "."
+    else:
+        dn, fn = os.path.split(name)
+    return (dn, fn)
+
+
+def joinext(prefix, ext):
+    """
+    Function to join a file extension to a path.
+
+    The leading "." of ext is replaced by a platform specific extension
+    separator if necessary.
+
+    @param prefix the basepart of the filename (string)
+    @param ext the extension part (string)
+    @return the complete filename (string)
+    """
+    if ext[0] != ".":
+        ext = ".{0}".format(ext)
+        # require leading separator to match os.path.splitext
+    return prefix + EXTSEP + ext[1:]
+
+
+def compactPath(path, width, measure=len):
+    """
+    Function to return a compacted path fitting inside the given width.
+
+    @param path path to be compacted (string)
+    @param width width for the compacted path (integer)
+    @param measure reference to a function used to measure the length of the
+        string
+    @return compacted path (string)
+    """
+    if measure(path) <= width:
+        return path
+
+    ellipsis = "..."
+
+    head, tail = os.path.split(path)
+    mid = len(head) // 2
+    head1 = head[:mid]
+    head2 = head[mid:]
+    while head1:
+        # head1 is same size as head2 or one shorter
+        path = os.path.join("{0}{1}{2}".format(head1, ellipsis, head2), tail)
+        if measure(path) <= width:
+            return path
+        head1 = head1[:-1]
+        head2 = head2[1:]
+    path = os.path.join(ellipsis, tail)
+    if measure(path) <= width:
+        return path
+    while tail:
+        path = "{0}{1}".format(ellipsis, tail)
+        if measure(path) <= width:
+            return path
+        tail = tail[1:]
+    return ""
+
+
+def direntries(
+    path, filesonly=False, pattern=None, followsymlinks=True, checkStop=None
+):
+    """
+    Function returning a list of all files and directories.
+
+    @param path root of the tree to check
+    @type str
+    @param filesonly flag indicating that only files are wanted
+    @type bool
+    @param pattern a filename pattern or list of filename patterns to check
+        against
+    @type str or list of str
+    @param followsymlinks flag indicating whether symbolic links
+        should be followed
+    @type bool
+    @param checkStop function to be called to check for a stop
+    @type function
+    @return list of all files and directories in the tree rooted
+        at path. The names are expanded to start with path.
+    @rtype list of strs
+    """
+    patterns = pattern if isinstance(pattern, list) else [pattern]
+    files = [] if filesonly else [path]
+    try:
+        entries = os.listdir(path)
+        for entry in entries:
+            if checkStop and checkStop():
+                break
+
+            if entry in [
+                ".svn",
+                ".hg",
+                ".git",
+                ".ropeproject",
+                ".eric7project",
+                ".jedi",
+            ]:
+                continue
+
+            fentry = os.path.join(path, entry)
+            if (
+                pattern
+                and not os.path.isdir(fentry)
+                and not any(fnmatch.fnmatch(entry, p) for p in patterns)
+            ):
+                # entry doesn't fit the given pattern
+                continue
+
+            if os.path.isdir(fentry):
+                if os.path.islink(fentry) and not followsymlinks:
+                    continue
+                files += direntries(
+                    fentry, filesonly, pattern, followsymlinks, checkStop
+                )
+            else:
+                files.append(fentry)
+    except OSError:
+        pass
+    except UnicodeDecodeError:
+        pass
+    return files
+
+
+def getDirs(path, excludeDirs):
+    """
+    Function returning a list of all directories below path.
+
+    @param path root of the tree to check
+    @param excludeDirs basename of directories to ignore
+    @return list of all directories found
+    """
+    try:
+        names = os.listdir(path)
+    except OSError:
+        return []
+
+    dirs = []
+    for name in names:
+        if os.path.isdir(os.path.join(path, name)) and not os.path.islink(
+            os.path.join(path, name)
+        ):
+            exclude = 0
+            for e in excludeDirs:
+                if name.split(os.sep, 1)[0] == e:
+                    exclude = 1
+                    break
+            if not exclude:
+                dirs.append(os.path.join(path, name))
+
+    for name in dirs[:]:
+        if not os.path.islink(name):
+            dirs += getDirs(name, excludeDirs)
+
+    return dirs
+
+
+def findVolume(volumeName, findAll=False):
+    """
+    Function to find the directory belonging to a given volume name.
+
+    @param volumeName name of the volume to search for
+    @type str
+    @param findAll flag indicating to get the directories for all volumes
+        starting with the given name (defaults to False)
+    @type bool (optional)
+    @return directory path or list of directory paths for the given volume
+        name
+    @rtype str or list of str
+    """
+    volumeDirectories = []
+    volumeDirectory = None
+
+    if OSUtilities.isWindowsPlatform():
+        # we are on a Windows platform
+        def getVolumeName(diskName):
+            """
+            Local function to determine the volume of a disk or device.
+
+            Each disk or external device connected to windows has an
+            attribute called "volume name". This function returns the
+            volume name for the given disk/device.
+
+            Code from http://stackoverflow.com/a/12056414
+            """
+            volumeNameBuffer = ctypes.create_unicode_buffer(1024)
+            ctypes.windll.kernel32.GetVolumeInformationW(
+                ctypes.c_wchar_p(diskName),
+                volumeNameBuffer,
+                ctypes.sizeof(volumeNameBuffer),
+                None,
+                None,
+                None,
+                None,
+                0,
+            )
+            return volumeNameBuffer.value
+
+        #
+        # In certain circumstances, volumes are allocated to USB
+        # storage devices which cause a Windows popup to raise if their
+        # volume contains no media. Wrapping the check in SetErrorMode
+        # with SEM_FAILCRITICALERRORS (1) prevents this popup.
+        #
+        oldMode = ctypes.windll.kernel32.SetErrorMode(1)
+        try:
+            for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
+                dirpath = "{0}:\\".format(disk)
+                if os.path.exists(dirpath):
+                    if findAll:
+                        if getVolumeName(dirpath).startswith(volumeName):
+                            volumeDirectories.append(dirpath)
+                    else:
+                        if getVolumeName(dirpath) == volumeName:
+                            volumeDirectory = dirpath
+                            break
+        finally:
+            ctypes.windll.kernel32.SetErrorMode(oldMode)
+    else:
+        # we are on a Linux or macOS platform
+        for mountCommand in ["mount", "/sbin/mount", "/usr/sbin/mount"]:
+            with contextlib.suppress(FileNotFoundError):
+                mountOutput = subprocess.run(  # secok
+                    mountCommand, check=True, capture_output=True, text=True
+                ).stdout.splitlines()
+                mountedVolumes = [
+                    x.split(" type")[0].split(maxsplit=2)[2] for x in mountOutput
+                ]
+                if findAll:
+                    for volume in mountedVolumes:
+                        if volumeName in volume:
+                            volumeDirectories.append(volume)
+                    if volumeDirectories:
+                        break
+                else:
+                    for volume in mountedVolumes:
+                        if volume.endswith(volumeName):
+                            volumeDirectory = volume
+                            break
+                    if volumeDirectory:
+                        break
+
+    if findAll:
+        return volumeDirectories
+    else:
+        return volumeDirectory

eric ide

mercurial