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

149 statements  

1""" 

2Utilities for path handling. 

3""" 

4 

5# Copyright (c) IPython Development Team. 

6# Distributed under the terms of the Modified BSD License. 

7 

8import os 

9import sys 

10import errno 

11import shutil 

12import random 

13import glob 

14import warnings 

15 

16from IPython.utils.process import system 

17 

18#----------------------------------------------------------------------------- 

19# Code 

20#----------------------------------------------------------------------------- 

21fs_encoding = sys.getfilesystemencoding() 

22 

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) 

26 

27if sys.platform == 'win32': 

28 def _get_long_path_name(path): 

29 """Get a long path name (expand ~) on Windows using ctypes. 

30 

31 Examples 

32 -------- 

33 

34 >>> get_long_path_name('c:\\\\docume~1') 

35 'c:\\\\Documents and Settings' 

36 

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 ] 

45 

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 

56 

57 

58 

59def get_long_path_name(path): 

60 """Expand a path into its long form. 

61 

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) 

66 

67 

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 

82 

83def get_py_filename(name): 

84 """Return a valid python filename in the current directory. 

85 

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 """ 

89 

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) 

98 

99 

100def filefind(filename: str, path_dirs=None) -> str: 

101 """Find a file by looking through a sequence of paths. 

102 

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:: 

107 

108 filefind('myfile.txt') 

109 

110 will find the file in the current working dir, but:: 

111 

112 filefind('~/myfile.txt') 

113 

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. 

116 

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. 

127 

128 Returns 

129 ------- 

130 path : str 

131 returns absolute path to file. 

132 

133 Raises 

134 ------ 

135 IOError 

136 """ 

137 

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 

143 

144 if path_dirs is None: 

145 path_dirs = ("",) 

146 elif isinstance(path_dirs, str): 

147 path_dirs = (path_dirs,) 

148 

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) 

154 

155 raise OSError("File %r does not exist in any of the search paths: %r" % 

156 (filename, path_dirs) ) 

157 

158 

159class HomeDirError(Exception): 

160 pass 

161 

162 

163def get_home_dir(require_writable: bool=False) -> str: 

164 """Return the 'home' directory, as a unicode string. 

165 

166 Uses os.path.expanduser('~'), and checks for writability. 

167 

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. 

171 

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 """ 

181 

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) 

186 

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 

198 

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) 

205 

206def get_xdg_dir() -> str | None: 

207 """Return the XDG_CONFIG_HOME, if it is defined and exists, else None. 

208 

209 This is only for non-OS X posix (Linux,Unix,etc.) systems. 

210 """ 

211 

212 env = os.environ 

213 

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 

221 

222 return None 

223 

224 

225def get_xdg_cache_dir(): 

226 """Return the XDG_CACHE_HOME, if it is defined and exists, else None. 

227 

228 This is only for non-OS X posix (Linux,Unix,etc.) systems. 

229 """ 

230 

231 env = os.environ 

232 

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 

240 

241 return None 

242 

243 

244def expand_path(s: str) -> str: 

245 """Expand $VARS and ~names in a string, like a shell 

246 

247 :Examples: 

248 

249 In [2]: os.environ['FOO']='test' 

250 

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 

265 

266 

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('\\\\'))) 

274 

275 

276def shellglob(args): 

277 """ 

278 Do glob expansion for each element in `args` and return a flattened list. 

279 

280 Unmatched glob pattern will remain as-is in the returned list. 

281 

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 

290 

291ENOLINK = 1998 

292 

293def link(src, dst): 

294 """Hard links ``src`` to ``dst``, returning 0 or errno. 

295 

296 Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't 

297 supported by the operating system. 

298 """ 

299 

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 

308 

309 

310def link_or_copy(src, dst): 

311 """Attempts to hardlink ``src`` to ``dst``, copying if the link fails. 

312 

313 Attempts to maintain the semantics of ``shutil.copy``. 

314 

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 """ 

319 

320 if os.path.isdir(dst): 

321 dst = os.path.join(dst, os.path.basename(src)) 

322 

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 

330 

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) 

345 

346def ensure_dir_exists(path: str, mode: int=0o755): 

347 """ensure that a directory exists 

348 

349 If it doesn't exist, try to create it and protect against a race condition 

350 if another process is doing the same. 

351 

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)