1 # encoding: utf-8 2 """ 3 Utilities for path handling. 4 """ 5 6 # Copyright (c) IPython Development Team. 7 # Distributed under the terms of the Modified BSD License. 8 9 import os 10 import sys 11 import errno 12 import shutil 13 import random 14 import glob 15 import warnings 16 17 from IPython.utils.process import system 18 19 #----------------------------------------------------------------------------- 20 # Code 21 #----------------------------------------------------------------------------- 22 fs_encoding = sys.getfilesystemencoding() 23 24 def _writable_dir(path): 25 """Whether `path` is a directory, to which the user has write access.""" 26 return os.path.isdir(path) and os.access(path, os.W_OK) 27 28 if sys.platform == 'win32': 29 def _get_long_path_name(path): 30 """Get a long path name (expand ~) on Windows using ctypes. 31 32 Examples 33 -------- 34 35 >>> get_long_path_name('c:\\\\docume~1') 36 'c:\\\\Documents and Settings' 37 38 """ 39 try: 40 import ctypes 41 except ImportError as e: 42 raise ImportError('you need to have ctypes installed for this to work') from e 43 _GetLongPathName = ctypes.windll.kernel32.GetLongPathNameW 44 _GetLongPathName.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p, 45 ctypes.c_uint ] 46 47 buf = ctypes.create_unicode_buffer(260) 48 rv = _GetLongPathName(path, buf, 260) 49 if rv == 0 or rv > 260: 50 return path 51 else: 52 return buf.value 53 else: 54 def _get_long_path_name(path): 55 """Dummy no-op.""" 56 return path 57 58 59 60 def get_long_path_name(path): 61 """Expand a path into its long form. 62 63 On Windows this expands any ~ in the paths. On other platforms, it is 64 a null operation. 65 """ 66 return _get_long_path_name(path) 67 68 69 def compress_user(path): 70 """Reverse of :func:`os.path.expanduser` 71 """ 72 home = os.path.expanduser('~') 73 if path.startswith(home): 74 path = "~" + path[len(home):] 75 return path 76 77 def get_py_filename(name): 78 """Return a valid python filename in the current directory. 79 80 If the given name is not a file, it adds '.py' and searches again. 81 Raises IOError with an informative message if the file isn't found. 82 """ 83 84 name = os.path.expanduser(name) 85 if os.path.isfile(name): 86 return name 87 if not name.endswith(".py"): 88 py_name = name + ".py" 89 if os.path.isfile(py_name): 90 return py_name 91 raise IOError("File `%r` not found." % name) 92 93 94 def filefind(filename: str, path_dirs=None) -> str: 95 """Find a file by looking through a sequence of paths. 96 97 This iterates through a sequence of paths looking for a file and returns 98 the full, absolute path of the first occurrence of the file. If no set of 99 path dirs is given, the filename is tested as is, after running through 100 :func:`expandvars` and :func:`expanduser`. Thus a simple call:: 101 102 filefind('myfile.txt') 103 104 will find the file in the current working dir, but:: 105 106 filefind('~/myfile.txt') 107 108 Will find the file in the users home directory. This function does not 109 automatically try any paths, such as the cwd or the user's home directory. 110 111 Parameters 112 ---------- 113 filename : str 114 The filename to look for. 115 path_dirs : str, None or sequence of str 116 The sequence of paths to look for the file in. If None, the filename 117 need to be absolute or be in the cwd. If a string, the string is 118 put into a sequence and the searched. If a sequence, walk through 119 each element and join with ``filename``, calling :func:`expandvars` 120 and :func:`expanduser` before testing for existence. 121 122 Returns 123 ------- 124 path : str 125 returns absolute path to file. 126 127 Raises 128 ------ 129 IOError 130 """ 131 132 # If paths are quoted, abspath gets confused, strip them... 133 filename = filename.strip('"').strip("'") 134 # If the input is an absolute path, just check it exists 135 if os.path.isabs(filename) and os.path.isfile(filename): 136 return filename 137 138 if path_dirs is None: 139 path_dirs = ("",) 140 elif isinstance(path_dirs, str): 141 path_dirs = (path_dirs,) 142 143 for path in path_dirs: 144 if path == '.': path = os.getcwd() 145 testname = expand_path(os.path.join(path, filename)) 146 if os.path.isfile(testname): 147 return os.path.abspath(testname) 148 149 raise IOError("File %r does not exist in any of the search paths: %r" % 150 (filename, path_dirs) ) 151 152 153 class HomeDirError(Exception): 154 pass 155 156 157 def get_home_dir(require_writable=False) -> str: 158 """Return the 'home' directory, as a unicode string. 159 160 Uses os.path.expanduser('~'), and checks for writability. 161 162 See stdlib docs for how this is determined. 163 For Python <3.8, $HOME is first priority on *ALL* platforms. 164 For Python >=3.8 on Windows, %HOME% is no longer considered. 165 166 Parameters 167 ---------- 168 require_writable : bool [default: False] 169 if True: 170 guarantees the return value is a writable directory, otherwise 171 raises HomeDirError 172 if False: 173 The path is resolved, but it is not guaranteed to exist or be writable. 174 """ 175 176 homedir = os.path.expanduser('~') 177 # Next line will make things work even when /home/ is a symlink to 178 # /usr/home as it is on FreeBSD, for example 179 homedir = os.path.realpath(homedir) 180 181 if not _writable_dir(homedir) and os.name == 'nt': 182 # expanduser failed, use the registry to get the 'My Documents' folder. 183 try: 184 import winreg as wreg 185 with wreg.OpenKey( 186 wreg.HKEY_CURRENT_USER, 187 r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" 188 ) as key: 189 homedir = wreg.QueryValueEx(key,'Personal')[0] 190 except: 191 pass 192 193 if (not require_writable) or _writable_dir(homedir): 194 assert isinstance(homedir, str), "Homedir should be unicode not bytes" 195 return homedir 196 else: 197 raise HomeDirError('%s is not a writable dir, ' 198 'set $HOME environment variable to override' % homedir) 199 200 def get_xdg_dir(): 201 """Return the XDG_CONFIG_HOME, if it is defined and exists, else None. 202 203 This is only for non-OS X posix (Linux,Unix,etc.) systems. 204 """ 205 206 env = os.environ 207 208 if os.name == "posix": 209 # Linux, Unix, AIX, etc. 210 # use ~/.config if empty OR not set 211 xdg = env.get("XDG_CONFIG_HOME", None) or os.path.join(get_home_dir(), '.config') 212 if xdg and _writable_dir(xdg): 213 assert isinstance(xdg, str) 214 return xdg 215 216 return None 217 218 219 def get_xdg_cache_dir(): 220 """Return the XDG_CACHE_HOME, if it is defined and exists, else None. 221 222 This is only for non-OS X posix (Linux,Unix,etc.) systems. 223 """ 224 225 env = os.environ 226 227 if os.name == "posix": 228 # Linux, Unix, AIX, etc. 229 # use ~/.cache if empty OR not set 230 xdg = env.get("XDG_CACHE_HOME", None) or os.path.join(get_home_dir(), '.cache') 231 if xdg and _writable_dir(xdg): 232 assert isinstance(xdg, str) 233 return xdg 234 235 return None 236 237 238 def expand_path(s): 239 """Expand $VARS and ~names in a string, like a shell 240 241 :Examples: 242 243 In [2]: os.environ['FOO']='test' 244 245 In [3]: expand_path('variable FOO is $FOO') 246 Out[3]: 'variable FOO is test' 247 """ 248 # This is a pretty subtle hack. When expand user is given a UNC path 249 # on Windows (\\server\share$\%username%), os.path.expandvars, removes 250 # the $ to get (\\server\share\%username%). I think it considered $ 251 # alone an empty var. But, we need the $ to remains there (it indicates 252 # a hidden share). 253 if os.name=='nt': 254 s = s.replace('$\\', 'IPYTHON_TEMP') 255 s = os.path.expandvars(os.path.expanduser(s)) 256 if os.name=='nt': 257 s = s.replace('IPYTHON_TEMP', '$\\') 258 return s 259 260 261 def unescape_glob(string): 262 """Unescape glob pattern in `string`.""" 263 def unescape(s): 264 for pattern in '*[]!?': 265 s = s.replace(r'\{0}'.format(pattern), pattern) 266 return s 267 return '\\'.join(map(unescape, string.split('\\\\'))) 268 269 270 def shellglob(args): 271 """ 272 Do glob expansion for each element in `args` and return a flattened list. 273 274 Unmatched glob pattern will remain as-is in the returned list. 275 276 """ 277 expanded = [] 278 # Do not unescape backslash in Windows as it is interpreted as 279 # path separator: 280 unescape = unescape_glob if sys.platform != 'win32' else lambda x: x 281 for a in args: 282 expanded.extend(glob.glob(a) or [unescape(a)]) 283 return expanded 284 285 286 def target_outdated(target,deps): 287 """Determine whether a target is out of date. 288 289 target_outdated(target,deps) -> 1/0 290 291 deps: list of filenames which MUST exist. 292 target: single filename which may or may not exist. 293 294 If target doesn't exist or is older than any file listed in deps, return 295 true, otherwise return false. 296 297 .. deprecated:: 8.22 298 """ 299 warnings.warn( 300 "`target_outdated` is deprecated since IPython 8.22 and will be removed in future versions", 301 DeprecationWarning, 302 stacklevel=2, 303 ) 304 try: 305 target_time = os.path.getmtime(target) 306 except os.error: 307 return 1 308 for dep in deps: 309 dep_time = os.path.getmtime(dep) 310 if dep_time > target_time: 311 # print("For target",target,"Dep failed:",dep) # dbg 312 # print("times (dep,tar):",dep_time,target_time) # dbg 313 return 1 314 return 0 315 316 317 def target_update(target,deps,cmd): 318 """Update a target with a given command given a list of dependencies. 319 320 target_update(target,deps,cmd) -> runs cmd if target is outdated. 321 322 This is just a wrapper around target_outdated() which calls the given 323 command if target is outdated. 324 325 .. deprecated:: 8.22 326 """ 327 328 warnings.warn( 329 "`target_update` is deprecated since IPython 8.22 and will be removed in future versions", 330 DeprecationWarning, 331 stacklevel=2, 332 ) 333 if target_outdated(target, deps): 334 system(cmd) 335 336 337 ENOLINK = 1998 338 339 def link(src, dst): 340 """Hard links ``src`` to ``dst``, returning 0 or errno. 341 342 Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't 343 supported by the operating system. 344 """ 345 346 if not hasattr(os, "link"): 347 return ENOLINK 348 link_errno = 0 349 try: 350 os.link(src, dst) 351 except OSError as e: 352 link_errno = e.errno 353 return link_errno 354 355 356 def link_or_copy(src, dst): 357 """Attempts to hardlink ``src`` to ``dst``, copying if the link fails. 358 359 Attempts to maintain the semantics of ``shutil.copy``. 360 361 Because ``os.link`` does not overwrite files, a unique temporary file 362 will be used if the target already exists, then that file will be moved 363 into place. 364 """ 365 366 if os.path.isdir(dst): 367 dst = os.path.join(dst, os.path.basename(src)) 368 369 link_errno = link(src, dst) 370 if link_errno == errno.EEXIST: 371 if os.stat(src).st_ino == os.stat(dst).st_ino: 372 # dst is already a hard link to the correct file, so we don't need 373 # to do anything else. If we try to link and rename the file 374 # anyway, we get duplicate files - see http://bugs.python.org/issue21876 375 return 376 377 new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), ) 378 try: 379 link_or_copy(src, new_dst) 380 except: 381 try: 382 os.remove(new_dst) 383 except OSError: 384 pass 385 raise 386 os.rename(new_dst, dst) 387 elif link_errno != 0: 388 # Either link isn't supported, or the filesystem doesn't support 389 # linking, or 'src' and 'dst' are on different filesystems. 390 shutil.copy(src, dst) 391 392 def ensure_dir_exists(path, mode=0o755): 393 """ensure that a directory exists 394 395 If it doesn't exist, try to create it and protect against a race condition 396 if another process is doing the same. 397 398 The default permissions are 755, which differ from os.makedirs default of 777. 399 """ 400 if not os.path.exists(path): 401 try: 402 os.makedirs(path, mode=mode) 403 except OSError as e: 404 if e.errno != errno.EEXIST: 405 raise 406 elif not os.path.isdir(path): 407 raise IOError("%r exists but is not a directory" % path) 408