Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/IPython/utils/path.py: 19%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Utilities for path handling.
3"""
5# Copyright (c) IPython Development Team.
6# Distributed under the terms of the Modified BSD License.
8import os
9import sys
10import errno
11import shutil
12import random
13import glob
14import warnings
16from IPython.utils.process import system
18#-----------------------------------------------------------------------------
19# Code
20#-----------------------------------------------------------------------------
21fs_encoding = sys.getfilesystemencoding()
23def _writable_dir(path: str) -> bool:
24 """Whether `path` is a directory, to which the user has write access."""
25 return os.path.isdir(path) and os.access(path, os.W_OK)
27if sys.platform == 'win32':
28 def _get_long_path_name(path):
29 """Get a long path name (expand ~) on Windows using ctypes.
31 Examples
32 --------
34 >>> get_long_path_name('c:\\\\docume~1')
35 'c:\\\\Documents and Settings'
37 """
38 try:
39 import ctypes
40 except ImportError as e:
41 raise ImportError('you need to have ctypes installed for this to work') from e
42 _GetLongPathName = ctypes.windll.kernel32.GetLongPathNameW
43 _GetLongPathName.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p,
44 ctypes.c_uint ]
46 buf = ctypes.create_unicode_buffer(260)
47 rv = _GetLongPathName(path, buf, 260)
48 if rv == 0 or rv > 260:
49 return path
50 else:
51 return buf.value
52else:
53 def _get_long_path_name(path):
54 """Dummy no-op."""
55 return path
59def get_long_path_name(path):
60 """Expand a path into its long form.
62 On Windows this expands any ~ in the paths. On other platforms, it is
63 a null operation.
64 """
65 return _get_long_path_name(path)
68def compress_user(path: str) -> str:
69 """Reverse of :func:`os.path.expanduser`"""
70 home = os.path.expanduser("~")
71 # Windows filesystems are case-insensitive and mix separators, so compare
72 # with normcase (a no-op on POSIX). It preserves length, so len(prefix)
73 # still indexes the original, un-normcased path correctly below.
74 if os.path.normcase(path) == os.path.normcase(home):
75 return "~"
76 # Compare against home + separator, so that a path which merely shares a
77 # prefix with home (/home/alice-backup vs /home/alice) is left alone.
78 prefix = os.path.join(home, "")
79 if os.path.normcase(path).startswith(os.path.normcase(prefix)):
80 path = "~" + os.sep + path[len(prefix) :]
81 return path
83def get_py_filename(name):
84 """Return a valid python filename in the current directory.
86 If the given name is not a file, it adds '.py' and searches again.
87 Raises IOError with an informative message if the file isn't found.
88 """
90 name = os.path.expanduser(name)
91 if os.path.isfile(name):
92 return name
93 if not name.endswith(".py"):
94 py_name = name + ".py"
95 if os.path.isfile(py_name):
96 return py_name
97 raise OSError("File `%r` not found." % name)
100def filefind(filename: str, path_dirs=None) -> str:
101 """Find a file by looking through a sequence of paths.
103 This iterates through a sequence of paths looking for a file and returns
104 the full, absolute path of the first occurrence of the file. If no set of
105 path dirs is given, the filename is tested as is, after running through
106 :func:`expandvars` and :func:`expanduser`. Thus a simple call::
108 filefind('myfile.txt')
110 will find the file in the current working dir, but::
112 filefind('~/myfile.txt')
114 Will find the file in the users home directory. This function does not
115 automatically try any paths, such as the cwd or the user's home directory.
117 Parameters
118 ----------
119 filename : str
120 The filename to look for.
121 path_dirs : str, None or sequence of str
122 The sequence of paths to look for the file in. If None, the filename
123 need to be absolute or be in the cwd. If a string, the string is
124 put into a sequence and the searched. If a sequence, walk through
125 each element and join with ``filename``, calling :func:`expandvars`
126 and :func:`expanduser` before testing for existence.
128 Returns
129 -------
130 path : str
131 returns absolute path to file.
133 Raises
134 ------
135 IOError
136 """
138 # If paths are quoted, abspath gets confused, strip them...
139 filename = filename.strip('"').strip("'")
140 # If the input is an absolute path, just check it exists
141 if os.path.isabs(filename) and os.path.isfile(filename):
142 return filename
144 if path_dirs is None:
145 path_dirs = ("",)
146 elif isinstance(path_dirs, str):
147 path_dirs = (path_dirs,)
149 for path in path_dirs:
150 if path == '.': path = os.getcwd()
151 testname = expand_path(os.path.join(path, filename))
152 if os.path.isfile(testname):
153 return os.path.abspath(testname)
155 raise OSError("File %r does not exist in any of the search paths: %r" %
156 (filename, path_dirs) )
159class HomeDirError(Exception):
160 pass
163def get_home_dir(require_writable: bool=False) -> str:
164 """Return the 'home' directory, as a unicode string.
166 Uses os.path.expanduser('~'), and checks for writability.
168 See stdlib docs for how this is determined.
169 For Python <3.8, $HOME is first priority on *ALL* platforms.
170 For Python >=3.8 on Windows, %HOME% is no longer considered.
172 Parameters
173 ----------
174 require_writable : bool [default: False]
175 if True:
176 guarantees the return value is a writable directory, otherwise
177 raises HomeDirError
178 if False:
179 The path is resolved, but it is not guaranteed to exist or be writable.
180 """
182 homedir = os.path.expanduser('~')
183 # Next line will make things work even when /home/ is a symlink to
184 # /usr/home as it is on FreeBSD, for example
185 homedir = os.path.realpath(homedir)
187 if not _writable_dir(homedir) and os.name == 'nt':
188 # expanduser failed, use the registry to get the 'My Documents' folder.
189 try:
190 import winreg as wreg
191 with wreg.OpenKey(
192 wreg.HKEY_CURRENT_USER,
193 r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
194 ) as key:
195 homedir = wreg.QueryValueEx(key,'Personal')[0]
196 except Exception:
197 pass
199 if (not require_writable) or _writable_dir(homedir):
200 assert isinstance(homedir, str), "Homedir should be unicode not bytes"
201 return homedir
202 else:
203 raise HomeDirError('%s is not a writable dir, '
204 'set $HOME environment variable to override' % homedir)
206def get_xdg_dir() -> str | None:
207 """Return the XDG_CONFIG_HOME, if it is defined and exists, else None.
209 This is only for non-OS X posix (Linux,Unix,etc.) systems.
210 """
212 env = os.environ
214 if os.name == "posix":
215 # Linux, Unix, AIX, etc.
216 # use ~/.config if empty OR not set
217 xdg = env.get("XDG_CONFIG_HOME", None) or os.path.join(get_home_dir(), '.config')
218 if xdg and _writable_dir(xdg):
219 assert isinstance(xdg, str)
220 return xdg
222 return None
225def get_xdg_cache_dir():
226 """Return the XDG_CACHE_HOME, if it is defined and exists, else None.
228 This is only for non-OS X posix (Linux,Unix,etc.) systems.
229 """
231 env = os.environ
233 if os.name == "posix":
234 # Linux, Unix, AIX, etc.
235 # use ~/.cache if empty OR not set
236 xdg = env.get("XDG_CACHE_HOME", None) or os.path.join(get_home_dir(), '.cache')
237 if xdg and _writable_dir(xdg):
238 assert isinstance(xdg, str)
239 return xdg
241 return None
244def expand_path(s: str) -> str:
245 """Expand $VARS and ~names in a string, like a shell
247 :Examples:
249 In [2]: os.environ['FOO']='test'
251 In [3]: expand_path('variable FOO is $FOO')
252 Out[3]: 'variable FOO is test'
253 """
254 # This is a pretty subtle hack. When expand user is given a UNC path
255 # on Windows (\\server\share$\%username%), os.path.expandvars, removes
256 # the $ to get (\\server\share\%username%). I think it considered $
257 # alone an empty var. But, we need the $ to remains there (it indicates
258 # a hidden share).
259 if os.name=='nt':
260 s = s.replace('$\\', 'IPYTHON_TEMP')
261 s = os.path.expandvars(os.path.expanduser(s))
262 if os.name=='nt':
263 s = s.replace('IPYTHON_TEMP', '$\\')
264 return s
267def unescape_glob(string):
268 """Unescape glob pattern in `string`."""
269 def unescape(s):
270 for pattern in '*[]!?':
271 s = s.replace(fr'\{pattern}', pattern)
272 return s
273 return '\\'.join(map(unescape, string.split('\\\\')))
276def shellglob(args):
277 """
278 Do glob expansion for each element in `args` and return a flattened list.
280 Unmatched glob pattern will remain as-is in the returned list.
282 """
283 expanded = []
284 # Do not unescape backslash in Windows as it is interpreted as
285 # path separator:
286 unescape = unescape_glob if sys.platform != 'win32' else lambda x: x
287 for a in args:
288 expanded.extend(glob.glob(a) or [unescape(a)])
289 return expanded
291ENOLINK = 1998
293def link(src, dst):
294 """Hard links ``src`` to ``dst``, returning 0 or errno.
296 Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't
297 supported by the operating system.
298 """
300 if not hasattr(os, "link"):
301 return ENOLINK
302 link_errno = 0
303 try:
304 os.link(src, dst)
305 except OSError as e:
306 link_errno = e.errno
307 return link_errno
310def link_or_copy(src, dst):
311 """Attempts to hardlink ``src`` to ``dst``, copying if the link fails.
313 Attempts to maintain the semantics of ``shutil.copy``.
315 Because ``os.link`` does not overwrite files, a unique temporary file
316 will be used if the target already exists, then that file will be moved
317 into place.
318 """
320 if os.path.isdir(dst):
321 dst = os.path.join(dst, os.path.basename(src))
323 link_errno = link(src, dst)
324 if link_errno == errno.EEXIST:
325 if os.stat(src).st_ino == os.stat(dst).st_ino:
326 # dst is already a hard link to the correct file, so we don't need
327 # to do anything else. If we try to link and rename the file
328 # anyway, we get duplicate files - see http://bugs.python.org/issue21876
329 return
331 new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), )
332 try:
333 link_or_copy(src, new_dst)
334 except Exception:
335 try:
336 os.remove(new_dst)
337 except OSError:
338 pass
339 raise
340 os.rename(new_dst, dst)
341 elif link_errno != 0:
342 # Either link isn't supported, or the filesystem doesn't support
343 # linking, or 'src' and 'dst' are on different filesystems.
344 shutil.copy(src, dst)
346def ensure_dir_exists(path: str, mode: int=0o755):
347 """ensure that a directory exists
349 If it doesn't exist, try to create it and protect against a race condition
350 if another process is doing the same.
352 The default permissions are 755, which differ from os.makedirs default of 777.
353 """
354 if not os.path.exists(path):
355 try:
356 os.makedirs(path, mode=mode)
357 except OSError as e:
358 if e.errno != errno.EEXIST:
359 raise
360 elif not os.path.isdir(path):
361 raise OSError("%r exists but is not a directory" % path)