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