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