codekingpro/portable-devtools
114k
1"""distutils.dir_util2 3Utility functions for manipulating directories and directory trees."""4 5import functools6import itertools7import os8import pathlib9 10from . import file_util11from ._log import log12from .errors import DistutilsFileError, DistutilsInternalError13 14 15class SkipRepeatAbsolutePaths(set):16 """17 Cache for mkpath.18 19 In addition to cheapening redundant calls, eliminates redundant20 "creating /foo/bar/baz" messages in dry-run mode.21 """22 23 def __init__(self):24 SkipRepeatAbsolutePaths.instance = self25 26 @classmethod27 def clear(cls):28 super(cls, cls.instance).clear()29 30 def wrap(self, func):31 @functools.wraps(func)32 def wrapper(path, *args, **kwargs):33 if path.absolute() in self:34 return35 result = func(path, *args, **kwargs)36 self.add(path.absolute())37 return result38 39 return wrapper40 41 42# Python 3.8 compatibility43wrapper = SkipRepeatAbsolutePaths().wrap44 45 46@functools.singledispatch47@wrapper48def mkpath(name: pathlib.Path, mode=0o777, verbose=True) -> None:49 """Create a directory and any missing ancestor directories.50 51 If the directory already exists (or if 'name' is the empty string, which52 means the current directory, which of course exists), then do nothing.53 Raise DistutilsFileError if unable to create some directory along the way54 (eg. some sub-path exists, but is a file rather than a directory).55 If 'verbose' is true, log the directory created.56 """57 if verbose and not name.is_dir():58 log.info("creating %s", name)59 60 try:61 name.mkdir(mode=mode, parents=True, exist_ok=True)62 except OSError as exc:63 raise DistutilsFileError(f"could not create '{name}': {exc.args[-1]}")64 65 66@mkpath.register67def _(name: str, *args, **kwargs):68 return mkpath(pathlib.Path(name), *args, **kwargs)69 70 71@mkpath.register72def _(name: None, *args, **kwargs):73 """74 Detect a common bug -- name is None.75 """76 raise DistutilsInternalError(f"mkpath: 'name' must be a string (got {name!r})")77 78 79def create_tree(base_dir, files, mode=0o777, verbose=True):80 """Create all the empty directories under 'base_dir' needed to put 'files'81 there.82 83 'base_dir' is just the name of a directory which doesn't necessarily84 exist yet; 'files' is a list of filenames to be interpreted relative to85 'base_dir'. 'base_dir' + the directory portion of every file in 'files'86 will be created if it doesn't already exist. 'mode' and 'verbose'87 flags are as for 'mkpath()'.88 """89 # First get the list of directories to create90 need_dir = set(os.path.join(base_dir, os.path.dirname(file)) for file in files)91 92 # Now create them93 for dir in sorted(need_dir):94 mkpath(dir, mode, verbose=verbose)95 96 97def copy_tree(98 src,99 dst,100 preserve_mode=True,101 preserve_times=True,102 preserve_symlinks=False,103 update=False,104 verbose=True,105):106 """Copy an entire directory tree 'src' to a new location 'dst'.107 108 Both 'src' and 'dst' must be directory names. If 'src' is not a109 directory, raise DistutilsFileError. If 'dst' does not exist, it is110 created with 'mkpath()'. The end result of the copy is that every111 file in 'src' is copied to 'dst', and directories under 'src' are112 recursively copied to 'dst'. Return the list of files that were113 copied or might have been copied, using their output name. The114 return value is unaffected by 'update': it is simply115 the list of all files under 'src', with the names changed to be116 under 'dst'.117 118 'preserve_mode' and 'preserve_times' are the same as for119 'copy_file'; note that they only apply to regular files, not to120 directories. If 'preserve_symlinks' is true, symlinks will be121 copied as symlinks (on platforms that support them!); otherwise122 (the default), the destination of the symlink will be copied.123 'update' and 'verbose' are the same as for 'copy_file'.124 """125 if not os.path.isdir(src):126 raise DistutilsFileError(f"cannot copy tree '{src}': not a directory")127 try:128 names = os.listdir(src)129 except OSError as e:130 raise DistutilsFileError(f"error listing files in '{src}': {e.strerror}")131 132 mkpath(dst, verbose=verbose)133 134 copy_one = functools.partial(135 _copy_one,136 src=src,137 dst=dst,138 preserve_symlinks=preserve_symlinks,139 verbose=verbose,140 preserve_mode=preserve_mode,141 preserve_times=preserve_times,142 update=update,143 )144 return list(itertools.chain.from_iterable(map(copy_one, names)))145 146 147def _copy_one(148 name,149 *,150 src,151 dst,152 preserve_symlinks,153 verbose,154 preserve_mode,155 preserve_times,156 update,157):158 src_name = os.path.join(src, name)159 dst_name = os.path.join(dst, name)160 161 if name.startswith('.nfs'):162 # skip NFS rename files163 return164 165 if preserve_symlinks and os.path.islink(src_name):166 link_dest = os.readlink(src_name)167 if verbose >= 1:168 log.info("linking %s -> %s", dst_name, link_dest)169 os.symlink(link_dest, dst_name)170 yield dst_name171 172 elif os.path.isdir(src_name):173 yield from copy_tree(174 src_name,175 dst_name,176 preserve_mode,177 preserve_times,178 preserve_symlinks,179 update,180 verbose=verbose,181 )182 else:183 file_util.copy_file(184 src_name,185 dst_name,186 preserve_mode,187 preserve_times,188 update,189 verbose=verbose,190 )191 yield dst_name192 193 194def _build_cmdtuple(path, cmdtuples):195 """Helper for remove_tree()."""196 for f in os.listdir(path):197 real_f = os.path.join(path, f)198 if os.path.isdir(real_f) and not os.path.islink(real_f):199 _build_cmdtuple(real_f, cmdtuples)200 else:201 cmdtuples.append((os.remove, real_f))202 cmdtuples.append((os.rmdir, path))203 204 205def remove_tree(directory, verbose=True):206 """Recursively remove an entire directory tree.207 208 Any errors are ignored (apart from being reported to stdout if 'verbose'209 is true).210 """211 if verbose >= 1:212 log.info("removing '%s' (and everything under it)", directory)213 cmdtuples = []214 _build_cmdtuple(directory, cmdtuples)215 for cmd in cmdtuples:216 try:217 cmd[0](cmd[1])218 # Clear the cache219 SkipRepeatAbsolutePaths.clear()220 except OSError as exc:221 log.warning("error removing %s: %s", directory, exc)222 223 224def ensure_relative(path):225 """Take the full path 'path', and make it a relative path.226 227 This is useful to make 'path' the second argument to os.path.join().228 """229 drive, path = os.path.splitdrive(path)230 if path[0:1] == os.sep:231 path = drive + path[1:]232 return path233 