path.py
438 lines
| 13.6 KiB
| text/x-python
|
PythonLexer
|
r2498 | # encoding: utf-8 | ||
""" | ||||
Utilities for path handling. | ||||
""" | ||||
|
r16486 | # Copyright (c) IPython Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||||
|
r2498 | |||
import os | ||||
import sys | ||||
|
r11647 | import errno | ||
import shutil | ||||
import random | ||||
|
r8066 | import glob | ||
|
r17939 | from warnings import warn | ||
|
r2498 | |||
|
r2908 | from IPython.utils.process import system | ||
|
r4731 | from IPython.utils import py3compat | ||
|
r21039 | from IPython.utils.decorators import undoc | ||
|
r2498 | #----------------------------------------------------------------------------- | ||
# Code | ||||
#----------------------------------------------------------------------------- | ||||
|
r3808 | fs_encoding = sys.getfilesystemencoding() | ||
|
r4473 | def _writable_dir(path): | ||
"""Whether `path` is a directory, to which the user has write access.""" | ||||
return os.path.isdir(path) and os.access(path, os.W_OK) | ||||
|
r2498 | if sys.platform == 'win32': | ||
def _get_long_path_name(path): | ||||
"""Get a long path name (expand ~) on Windows using ctypes. | ||||
Examples | ||||
-------- | ||||
>>> get_long_path_name('c:\\docume~1') | ||||
|
r23213 | 'c:\\\\Documents and Settings' | ||
|
r2498 | |||
""" | ||||
try: | ||||
import ctypes | ||||
except ImportError: | ||||
raise ImportError('you need to have ctypes installed for this to work') | ||||
_GetLongPathName = ctypes.windll.kernel32.GetLongPathNameW | ||||
_GetLongPathName.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p, | ||||
ctypes.c_uint ] | ||||
buf = ctypes.create_unicode_buffer(260) | ||||
rv = _GetLongPathName(path, buf, 260) | ||||
if rv == 0 or rv > 260: | ||||
return path | ||||
else: | ||||
return buf.value | ||||
|
r19739 | else: | ||
def _get_long_path_name(path): | ||||
"""Dummy no-op.""" | ||||
return path | ||||
|
r2498 | |||
def get_long_path_name(path): | ||||
"""Expand a path into its long form. | ||||
On Windows this expands any ~ in the paths. On other platforms, it is | ||||
a null operation. | ||||
""" | ||||
return _get_long_path_name(path) | ||||
|
r4696 | def unquote_filename(name, win32=(sys.platform=='win32')): | ||
""" On Windows, remove leading and trailing quotes from filenames. | ||||
|
r22438 | |||
This function has been deprecated and should not be used any more: | ||||
unquoting is now taken care of by :func:`IPython.utils.process.arg_split`. | ||||
|
r4696 | """ | ||
|
r22457 | warn("'unquote_filename' is deprecated since IPython 5.0 and should not " | ||
|
r22917 | "be used anymore", DeprecationWarning, stacklevel=2) | ||
|
r4696 | if win32: | ||
if name.startswith(("'", '"')) and name.endswith(("'", '"')): | ||||
name = name[1:-1] | ||||
return name | ||||
|
r22418 | |||
|
r13173 | def compress_user(path): | ||
"""Reverse of :func:`os.path.expanduser` | ||||
|
r22438 | """ | ||
|
r13173 | home = os.path.expanduser('~') | ||
if path.startswith(home): | ||||
path = "~" + path[len(home):] | ||||
return path | ||||
|
r4696 | |||
def get_py_filename(name, force_win32=None): | ||||
|
r2498 | """Return a valid python filename in the current directory. | ||
If the given name is not a file, it adds '.py' and searches again. | ||||
|
r4688 | Raises IOError with an informative message if the file isn't found. | ||
""" | ||||
|
r2498 | |||
name = os.path.expanduser(name) | ||||
|
r22457 | if force_win32 is not None: | ||
warn("The 'force_win32' argument to 'get_py_filename' is deprecated " | ||||
"since IPython 5.0 and should not be used anymore", | ||||
|
r22917 | DeprecationWarning, stacklevel=2) | ||
|
r2498 | if not os.path.isfile(name) and not name.endswith('.py'): | ||
name += '.py' | ||||
if os.path.isfile(name): | ||||
return name | ||||
else: | ||||
|
r7843 | raise IOError('File `%r` not found.' % name) | ||
|
r2498 | |||
def filefind(filename, path_dirs=None): | ||||
"""Find a file by looking through a sequence of paths. | ||||
This iterates through a sequence of paths looking for a file and returns | ||||
|
r24084 | the full, absolute path of the first occurrence of the file. If no set of | ||
|
r2498 | path dirs is given, the filename is tested as is, after running through | ||
:func:`expandvars` and :func:`expanduser`. Thus a simple call:: | ||||
filefind('myfile.txt') | ||||
will find the file in the current working dir, but:: | ||||
filefind('~/myfile.txt') | ||||
Will find the file in the users home directory. This function does not | ||||
automatically try any paths, such as the cwd or the user's home directory. | ||||
|
r4872 | |||
|
r2498 | Parameters | ||
---------- | ||||
filename : str | ||||
The filename to look for. | ||||
path_dirs : str, None or sequence of str | ||||
The sequence of paths to look for the file in. If None, the filename | ||||
need to be absolute or be in the cwd. If a string, the string is | ||||
put into a sequence and the searched. If a sequence, walk through | ||||
each element and join with ``filename``, calling :func:`expandvars` | ||||
and :func:`expanduser` before testing for existence. | ||||
|
r4872 | |||
|
r2498 | Returns | ||
------- | ||||
Raises :exc:`IOError` or returns absolute path to file. | ||||
""" | ||||
|
r4872 | |||
|
r2498 | # If paths are quoted, abspath gets confused, strip them... | ||
filename = filename.strip('"').strip("'") | ||||
# If the input is an absolute path, just check it exists | ||||
if os.path.isabs(filename) and os.path.isfile(filename): | ||||
return filename | ||||
|
r4872 | |||
|
r2498 | if path_dirs is None: | ||
path_dirs = ("",) | ||||
|
r23037 | elif isinstance(path_dirs, str): | ||
|
r2498 | path_dirs = (path_dirs,) | ||
|
r4872 | |||
|
r2498 | for path in path_dirs: | ||
|
r23045 | if path == '.': path = os.getcwd() | ||
|
r2498 | testname = expand_path(os.path.join(path, filename)) | ||
if os.path.isfile(testname): | ||||
return os.path.abspath(testname) | ||||
|
r4872 | |||
raise IOError("File %r does not exist in any of the search paths: %r" % | ||||
|
r2498 | (filename, path_dirs) ) | ||
class HomeDirError(Exception): | ||||
pass | ||||
|
r5384 | def get_home_dir(require_writable=False): | ||
"""Return the 'home' directory, as a unicode string. | ||||
|
r2498 | |||
|
r11499 | Uses os.path.expanduser('~'), and checks for writability. | ||
|
r21517 | |||
|
r5383 | See stdlib docs for how this is determined. | ||
$HOME is first priority on *ALL* platforms. | ||||
|
r21517 | |||
|
r5384 | Parameters | ||
---------- | ||||
|
r21517 | |||
|
r5384 | require_writable : bool [default: False] | ||
if True: | ||||
guarantees the return value is a writable directory, otherwise | ||||
raises HomeDirError | ||||
if False: | ||||
The path is resolved, but it is not guaranteed to exist or be writable. | ||||
|
r2498 | """ | ||
|
r5383 | homedir = os.path.expanduser('~') | ||
|
r6112 | # Next line will make things work even when /home/ is a symlink to | ||
# /usr/home as it is on FreeBSD, for example | ||||
homedir = os.path.realpath(homedir) | ||||
|
r21517 | |||
|
r5385 | if not _writable_dir(homedir) and os.name == 'nt': | ||
# expanduser failed, use the registry to get the 'My Documents' folder. | ||||
|
r2498 | try: | ||
|
r13354 | try: | ||
import winreg as wreg # Py 3 | ||||
except ImportError: | ||||
import _winreg as wreg # Py 2 | ||||
|
r2498 | key = wreg.OpenKey( | ||
wreg.HKEY_CURRENT_USER, | ||||
"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" | ||||
) | ||||
homedir = wreg.QueryValueEx(key,'Personal')[0] | ||||
key.Close() | ||||
except: | ||||
pass | ||||
|
r21517 | |||
|
r5384 | if (not require_writable) or _writable_dir(homedir): | ||
|
r5383 | return py3compat.cast_unicode(homedir, fs_encoding) | ||
|
r2498 | else: | ||
|
r5385 | raise HomeDirError('%s is not a writable dir, ' | ||
'set $HOME environment variable to override' % homedir) | ||||
|
r2498 | |||
|
r3347 | def get_xdg_dir(): | ||
"""Return the XDG_CONFIG_HOME, if it is defined and exists, else None. | ||||
|
r4872 | |||
|
r7086 | This is only for non-OS X posix (Linux,Unix,etc.) systems. | ||
|
r3347 | """ | ||
env = os.environ | ||||
|
r4872 | |||
|
r7086 | if os.name == 'posix' and sys.platform != 'darwin': | ||
# Linux, Unix, AIX, etc. | ||||
|
r5384 | # use ~/.config if empty OR not set | ||
|
r3347 | xdg = env.get("XDG_CONFIG_HOME", None) or os.path.join(get_home_dir(), '.config') | ||
|
r4474 | if xdg and _writable_dir(xdg): | ||
|
r4731 | return py3compat.cast_unicode(xdg, fs_encoding) | ||
|
r4872 | |||
|
r3347 | return None | ||
|
r4872 | |||
|
r2498 | |||
|
r10230 | def get_xdg_cache_dir(): | ||
"""Return the XDG_CACHE_HOME, if it is defined and exists, else None. | ||||
This is only for non-OS X posix (Linux,Unix,etc.) systems. | ||||
""" | ||||
env = os.environ | ||||
if os.name == 'posix' and sys.platform != 'darwin': | ||||
# Linux, Unix, AIX, etc. | ||||
# use ~/.cache if empty OR not set | ||||
xdg = env.get("XDG_CACHE_HOME", None) or os.path.join(get_home_dir(), '.cache') | ||||
if xdg and _writable_dir(xdg): | ||||
return py3compat.cast_unicode(xdg, fs_encoding) | ||||
return None | ||||
|
r21040 | @undoc | ||
|
r2498 | def get_ipython_dir(): | ||
|
r22917 | warn("get_ipython_dir has moved to the IPython.paths module since IPython 4.0.", stacklevel=2) | ||
|
r21040 | from IPython.paths import get_ipython_dir | ||
return get_ipython_dir() | ||||
|
r2498 | |||
|
r21040 | @undoc | ||
|
r10230 | def get_ipython_cache_dir(): | ||
|
r22917 | warn("get_ipython_cache_dir has moved to the IPython.paths module since IPython 4.0.", stacklevel=2) | ||
|
r21040 | from IPython.paths import get_ipython_cache_dir | ||
return get_ipython_cache_dir() | ||||
|
r10230 | |||
|
r21040 | @undoc | ||
|
r2498 | def get_ipython_package_dir(): | ||
|
r22917 | warn("get_ipython_package_dir has moved to the IPython.paths module since IPython 4.0.", stacklevel=2) | ||
|
r21040 | from IPython.paths import get_ipython_package_dir | ||
return get_ipython_package_dir() | ||||
|
r2498 | |||
|
r21040 | @undoc | ||
|
r2498 | def get_ipython_module_path(module_str): | ||
|
r22917 | warn("get_ipython_module_path has moved to the IPython.paths module since IPython 4.0.", stacklevel=2) | ||
|
r21040 | from IPython.paths import get_ipython_module_path | ||
return get_ipython_module_path(module_str) | ||||
|
r2498 | |||
|
r21040 | @undoc | ||
|
r4988 | def locate_profile(profile='default'): | ||
|
r22917 | warn("locate_profile has moved to the IPython.paths module since IPython 4.0.", stacklevel=2) | ||
|
r21040 | from IPython.paths import locate_profile | ||
return locate_profile(profile=profile) | ||||
|
r2498 | |||
def expand_path(s): | ||||
"""Expand $VARS and ~names in a string, like a shell | ||||
:Examples: | ||||
|
r4872 | |||
|
r2498 | In [2]: os.environ['FOO']='test' | ||
In [3]: expand_path('variable FOO is $FOO') | ||||
Out[3]: 'variable FOO is test' | ||||
""" | ||||
# This is a pretty subtle hack. When expand user is given a UNC path | ||||
# on Windows (\\server\share$\%username%), os.path.expandvars, removes | ||||
# the $ to get (\\server\share\%username%). I think it considered $ | ||||
# alone an empty var. But, we need the $ to remains there (it indicates | ||||
# a hidden share). | ||||
if os.name=='nt': | ||||
s = s.replace('$\\', 'IPYTHON_TEMP') | ||||
s = os.path.expandvars(os.path.expanduser(s)) | ||||
if os.name=='nt': | ||||
s = s.replace('IPYTHON_TEMP', '$\\') | ||||
return s | ||||
|
r8119 | def unescape_glob(string): | ||
"""Unescape glob pattern in `string`.""" | ||||
|
r8122 | def unescape(s): | ||
for pattern in '*[]!?': | ||||
s = s.replace(r'\{0}'.format(pattern), pattern) | ||||
return s | ||||
return '\\'.join(map(unescape, string.split('\\\\'))) | ||||
|
r8119 | |||
|
r8067 | def shellglob(args): | ||
|
r8014 | """ | ||
Do glob expansion for each element in `args` and return a flattened list. | ||||
Unmatched glob pattern will remain as-is in the returned list. | ||||
""" | ||||
expanded = [] | ||||
|
r8646 | # Do not unescape backslash in Windows as it is interpreted as | ||
# path separator: | ||||
unescape = unescape_glob if sys.platform != 'win32' else lambda x: x | ||||
|
r8014 | for a in args: | ||
|
r8646 | expanded.extend(glob.glob(a) or [unescape(a)]) | ||
|
r8014 | return expanded | ||
|
r2498 | def target_outdated(target,deps): | ||
"""Determine whether a target is out of date. | ||||
target_outdated(target,deps) -> 1/0 | ||||
deps: list of filenames which MUST exist. | ||||
target: single filename which may or may not exist. | ||||
If target doesn't exist or is older than any file listed in deps, return | ||||
true, otherwise return false. | ||||
""" | ||||
try: | ||||
target_time = os.path.getmtime(target) | ||||
except os.error: | ||||
return 1 | ||||
for dep in deps: | ||||
dep_time = os.path.getmtime(dep) | ||||
if dep_time > target_time: | ||||
#print "For target",target,"Dep failed:",dep # dbg | ||||
#print "times (dep,tar):",dep_time,target_time # dbg | ||||
return 1 | ||||
return 0 | ||||
def target_update(target,deps,cmd): | ||||
"""Update a target with a given command given a list of dependencies. | ||||
target_update(target,deps,cmd) -> runs cmd if target is outdated. | ||||
This is just a wrapper around target_outdated() which calls the given | ||||
command if target is outdated.""" | ||||
if target_outdated(target,deps): | ||||
|
r2908 | system(cmd) | ||
|
r2498 | |||
|
r4177 | |||
|
r11701 | ENOLINK = 1998 | ||
|
r11647 | def link(src, dst): | ||
|
r11650 | """Hard links ``src`` to ``dst``, returning 0 or errno. | ||
|
r11647 | |||
|
r11701 | Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't | ||
|
r11647 | supported by the operating system. | ||
""" | ||||
if not hasattr(os, "link"): | ||||
|
r11701 | return ENOLINK | ||
|
r11647 | link_errno = 0 | ||
try: | ||||
os.link(src, dst) | ||||
except OSError as e: | ||||
link_errno = e.errno | ||||
return link_errno | ||||
def link_or_copy(src, dst): | ||||
"""Attempts to hardlink ``src`` to ``dst``, copying if the link fails. | ||||
Attempts to maintain the semantics of ``shutil.copy``. | ||||
Because ``os.link`` does not overwrite files, a unique temporary file | ||||
will be used if the target already exists, then that file will be moved | ||||
into place. | ||||
""" | ||||
if os.path.isdir(dst): | ||||
dst = os.path.join(dst, os.path.basename(src)) | ||||
link_errno = link(src, dst) | ||||
if link_errno == errno.EEXIST: | ||||
|
r20038 | if os.stat(src).st_ino == os.stat(dst).st_ino: | ||
# dst is already a hard link to the correct file, so we don't need | ||||
# to do anything else. If we try to link and rename the file | ||||
# anyway, we get duplicate files - see http://bugs.python.org/issue21876 | ||||
return | ||||
|
r11647 | new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), ) | ||
try: | ||||
link_or_copy(src, new_dst) | ||||
except: | ||||
try: | ||||
os.remove(new_dst) | ||||
except OSError: | ||||
pass | ||||
raise | ||||
os.rename(new_dst, dst) | ||||
elif link_errno != 0: | ||||
# Either link isn't supported, or the filesystem doesn't support | ||||
# linking, or 'src' and 'dst' are on different filesystems. | ||||
shutil.copy(src, dst) | ||||
|
r16486 | |||
|
r16488 | def ensure_dir_exists(path, mode=0o755): | ||
|
r16486 | """ensure that a directory exists | ||
|
r21517 | |||
|
r16486 | If it doesn't exist, try to create it and protect against a race condition | ||
if another process is doing the same. | ||||
|
r21517 | |||
|
r16488 | The default permissions are 755, which differ from os.makedirs default of 777. | ||
|
r16486 | """ | ||
if not os.path.exists(path): | ||||
try: | ||||
os.makedirs(path, mode=mode) | ||||
except OSError as e: | ||||
if e.errno != errno.EEXIST: | ||||
raise | ||||
|
r16487 | elif not os.path.isdir(path): | ||
raise IOError("%r exists but is not a directory" % path) | ||||