Format: text/x-python
⬇ Download File (12.1 KB)
   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