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