Sun, 18 Dec 2022 19:33:46 +0100
Refactored the Utilities and Globals modules in order to enhance the maintainability.
# -*- 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