##// END OF EJS Templates
IPython/utils/path.py: Deprecate unused `target_outdated`, `target_update` (#14348)...
M Bussonnier -
r28666:2af3d033 merge
parent child Browse files
Show More
@@ -1,391 +1,407 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 Utilities for path handling.
3 Utilities for path handling.
4 """
4 """
5
5
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 import os
9 import os
10 import sys
10 import sys
11 import errno
11 import errno
12 import shutil
12 import shutil
13 import random
13 import random
14 import glob
14 import glob
15 import warnings
15
16
16 from IPython.utils.process import system
17 from IPython.utils.process import system
17
18
18 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
19 # Code
20 # Code
20 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
21 fs_encoding = sys.getfilesystemencoding()
22 fs_encoding = sys.getfilesystemencoding()
22
23
23 def _writable_dir(path):
24 def _writable_dir(path):
24 """Whether `path` is a directory, to which the user has write access."""
25 """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 return os.path.isdir(path) and os.access(path, os.W_OK)
26
27
27 if sys.platform == 'win32':
28 if sys.platform == 'win32':
28 def _get_long_path_name(path):
29 def _get_long_path_name(path):
29 """Get a long path name (expand ~) on Windows using ctypes.
30 """Get a long path name (expand ~) on Windows using ctypes.
30
31
31 Examples
32 Examples
32 --------
33 --------
33
34
34 >>> get_long_path_name('c:\\\\docume~1')
35 >>> get_long_path_name('c:\\\\docume~1')
35 'c:\\\\Documents and Settings'
36 'c:\\\\Documents and Settings'
36
37
37 """
38 """
38 try:
39 try:
39 import ctypes
40 import ctypes
40 except ImportError as e:
41 except ImportError as e:
41 raise ImportError('you need to have ctypes installed for this to work') from e
42 raise ImportError('you need to have ctypes installed for this to work') from e
42 _GetLongPathName = ctypes.windll.kernel32.GetLongPathNameW
43 _GetLongPathName = ctypes.windll.kernel32.GetLongPathNameW
43 _GetLongPathName.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p,
44 _GetLongPathName.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p,
44 ctypes.c_uint ]
45 ctypes.c_uint ]
45
46
46 buf = ctypes.create_unicode_buffer(260)
47 buf = ctypes.create_unicode_buffer(260)
47 rv = _GetLongPathName(path, buf, 260)
48 rv = _GetLongPathName(path, buf, 260)
48 if rv == 0 or rv > 260:
49 if rv == 0 or rv > 260:
49 return path
50 return path
50 else:
51 else:
51 return buf.value
52 return buf.value
52 else:
53 else:
53 def _get_long_path_name(path):
54 def _get_long_path_name(path):
54 """Dummy no-op."""
55 """Dummy no-op."""
55 return path
56 return path
56
57
57
58
58
59
59 def get_long_path_name(path):
60 def get_long_path_name(path):
60 """Expand a path into its long form.
61 """Expand a path into its long form.
61
62
62 On Windows this expands any ~ in the paths. On other platforms, it is
63 On Windows this expands any ~ in the paths. On other platforms, it is
63 a null operation.
64 a null operation.
64 """
65 """
65 return _get_long_path_name(path)
66 return _get_long_path_name(path)
66
67
67
68
68 def compress_user(path):
69 def compress_user(path):
69 """Reverse of :func:`os.path.expanduser`
70 """Reverse of :func:`os.path.expanduser`
70 """
71 """
71 home = os.path.expanduser('~')
72 home = os.path.expanduser('~')
72 if path.startswith(home):
73 if path.startswith(home):
73 path = "~" + path[len(home):]
74 path = "~" + path[len(home):]
74 return path
75 return path
75
76
76 def get_py_filename(name):
77 def get_py_filename(name):
77 """Return a valid python filename in the current directory.
78 """Return a valid python filename in the current directory.
78
79
79 If the given name is not a file, it adds '.py' and searches again.
80 If the given name is not a file, it adds '.py' and searches again.
80 Raises IOError with an informative message if the file isn't found.
81 Raises IOError with an informative message if the file isn't found.
81 """
82 """
82
83
83 name = os.path.expanduser(name)
84 name = os.path.expanduser(name)
84 if os.path.isfile(name):
85 if os.path.isfile(name):
85 return name
86 return name
86 if not name.endswith(".py"):
87 if not name.endswith(".py"):
87 py_name = name + ".py"
88 py_name = name + ".py"
88 if os.path.isfile(py_name):
89 if os.path.isfile(py_name):
89 return py_name
90 return py_name
90 raise IOError("File `%r` not found." % name)
91 raise IOError("File `%r` not found." % name)
91
92
92
93
93 def filefind(filename: str, path_dirs=None) -> str:
94 def filefind(filename: str, path_dirs=None) -> str:
94 """Find a file by looking through a sequence of paths.
95 """Find a file by looking through a sequence of paths.
95
96
96 This iterates through a sequence of paths looking for a file and returns
97 This iterates through a sequence of paths looking for a file and returns
97 the full, absolute path of the first occurrence of the file. If no set of
98 the full, absolute path of the first occurrence of the file. If no set of
98 path dirs is given, the filename is tested as is, after running through
99 path dirs is given, the filename is tested as is, after running through
99 :func:`expandvars` and :func:`expanduser`. Thus a simple call::
100 :func:`expandvars` and :func:`expanduser`. Thus a simple call::
100
101
101 filefind('myfile.txt')
102 filefind('myfile.txt')
102
103
103 will find the file in the current working dir, but::
104 will find the file in the current working dir, but::
104
105
105 filefind('~/myfile.txt')
106 filefind('~/myfile.txt')
106
107
107 Will find the file in the users home directory. This function does not
108 Will find the file in the users home directory. This function does not
108 automatically try any paths, such as the cwd or the user's home directory.
109 automatically try any paths, such as the cwd or the user's home directory.
109
110
110 Parameters
111 Parameters
111 ----------
112 ----------
112 filename : str
113 filename : str
113 The filename to look for.
114 The filename to look for.
114 path_dirs : str, None or sequence of str
115 path_dirs : str, None or sequence of str
115 The sequence of paths to look for the file in. If None, the filename
116 The sequence of paths to look for the file in. If None, the filename
116 need to be absolute or be in the cwd. If a string, the string is
117 need to be absolute or be in the cwd. If a string, the string is
117 put into a sequence and the searched. If a sequence, walk through
118 put into a sequence and the searched. If a sequence, walk through
118 each element and join with ``filename``, calling :func:`expandvars`
119 each element and join with ``filename``, calling :func:`expandvars`
119 and :func:`expanduser` before testing for existence.
120 and :func:`expanduser` before testing for existence.
120
121
121 Returns
122 Returns
122 -------
123 -------
123 path : str
124 path : str
124 returns absolute path to file.
125 returns absolute path to file.
125
126
126 Raises
127 Raises
127 ------
128 ------
128 IOError
129 IOError
129 """
130 """
130
131
131 # If paths are quoted, abspath gets confused, strip them...
132 # If paths are quoted, abspath gets confused, strip them...
132 filename = filename.strip('"').strip("'")
133 filename = filename.strip('"').strip("'")
133 # If the input is an absolute path, just check it exists
134 # If the input is an absolute path, just check it exists
134 if os.path.isabs(filename) and os.path.isfile(filename):
135 if os.path.isabs(filename) and os.path.isfile(filename):
135 return filename
136 return filename
136
137
137 if path_dirs is None:
138 if path_dirs is None:
138 path_dirs = ("",)
139 path_dirs = ("",)
139 elif isinstance(path_dirs, str):
140 elif isinstance(path_dirs, str):
140 path_dirs = (path_dirs,)
141 path_dirs = (path_dirs,)
141
142
142 for path in path_dirs:
143 for path in path_dirs:
143 if path == '.': path = os.getcwd()
144 if path == '.': path = os.getcwd()
144 testname = expand_path(os.path.join(path, filename))
145 testname = expand_path(os.path.join(path, filename))
145 if os.path.isfile(testname):
146 if os.path.isfile(testname):
146 return os.path.abspath(testname)
147 return os.path.abspath(testname)
147
148
148 raise IOError("File %r does not exist in any of the search paths: %r" %
149 raise IOError("File %r does not exist in any of the search paths: %r" %
149 (filename, path_dirs) )
150 (filename, path_dirs) )
150
151
151
152
152 class HomeDirError(Exception):
153 class HomeDirError(Exception):
153 pass
154 pass
154
155
155
156
156 def get_home_dir(require_writable=False) -> str:
157 def get_home_dir(require_writable=False) -> str:
157 """Return the 'home' directory, as a unicode string.
158 """Return the 'home' directory, as a unicode string.
158
159
159 Uses os.path.expanduser('~'), and checks for writability.
160 Uses os.path.expanduser('~'), and checks for writability.
160
161
161 See stdlib docs for how this is determined.
162 See stdlib docs for how this is determined.
162 For Python <3.8, $HOME is first priority on *ALL* platforms.
163 For Python <3.8, $HOME is first priority on *ALL* platforms.
163 For Python >=3.8 on Windows, %HOME% is no longer considered.
164 For Python >=3.8 on Windows, %HOME% is no longer considered.
164
165
165 Parameters
166 Parameters
166 ----------
167 ----------
167 require_writable : bool [default: False]
168 require_writable : bool [default: False]
168 if True:
169 if True:
169 guarantees the return value is a writable directory, otherwise
170 guarantees the return value is a writable directory, otherwise
170 raises HomeDirError
171 raises HomeDirError
171 if False:
172 if False:
172 The path is resolved, but it is not guaranteed to exist or be writable.
173 The path is resolved, but it is not guaranteed to exist or be writable.
173 """
174 """
174
175
175 homedir = os.path.expanduser('~')
176 homedir = os.path.expanduser('~')
176 # Next line will make things work even when /home/ is a symlink to
177 # Next line will make things work even when /home/ is a symlink to
177 # /usr/home as it is on FreeBSD, for example
178 # /usr/home as it is on FreeBSD, for example
178 homedir = os.path.realpath(homedir)
179 homedir = os.path.realpath(homedir)
179
180
180 if not _writable_dir(homedir) and os.name == 'nt':
181 if not _writable_dir(homedir) and os.name == 'nt':
181 # expanduser failed, use the registry to get the 'My Documents' folder.
182 # expanduser failed, use the registry to get the 'My Documents' folder.
182 try:
183 try:
183 import winreg as wreg
184 import winreg as wreg
184 with wreg.OpenKey(
185 with wreg.OpenKey(
185 wreg.HKEY_CURRENT_USER,
186 wreg.HKEY_CURRENT_USER,
186 r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
187 r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
187 ) as key:
188 ) as key:
188 homedir = wreg.QueryValueEx(key,'Personal')[0]
189 homedir = wreg.QueryValueEx(key,'Personal')[0]
189 except:
190 except:
190 pass
191 pass
191
192
192 if (not require_writable) or _writable_dir(homedir):
193 if (not require_writable) or _writable_dir(homedir):
193 assert isinstance(homedir, str), "Homedir should be unicode not bytes"
194 assert isinstance(homedir, str), "Homedir should be unicode not bytes"
194 return homedir
195 return homedir
195 else:
196 else:
196 raise HomeDirError('%s is not a writable dir, '
197 raise HomeDirError('%s is not a writable dir, '
197 'set $HOME environment variable to override' % homedir)
198 'set $HOME environment variable to override' % homedir)
198
199
199 def get_xdg_dir():
200 def get_xdg_dir():
200 """Return the XDG_CONFIG_HOME, if it is defined and exists, else None.
201 """Return the XDG_CONFIG_HOME, if it is defined and exists, else None.
201
202
202 This is only for non-OS X posix (Linux,Unix,etc.) systems.
203 This is only for non-OS X posix (Linux,Unix,etc.) systems.
203 """
204 """
204
205
205 env = os.environ
206 env = os.environ
206
207
207 if os.name == "posix":
208 if os.name == "posix":
208 # Linux, Unix, AIX, etc.
209 # Linux, Unix, AIX, etc.
209 # use ~/.config if empty OR not set
210 # use ~/.config if empty OR not set
210 xdg = env.get("XDG_CONFIG_HOME", None) or os.path.join(get_home_dir(), '.config')
211 xdg = env.get("XDG_CONFIG_HOME", None) or os.path.join(get_home_dir(), '.config')
211 if xdg and _writable_dir(xdg):
212 if xdg and _writable_dir(xdg):
212 assert isinstance(xdg, str)
213 assert isinstance(xdg, str)
213 return xdg
214 return xdg
214
215
215 return None
216 return None
216
217
217
218
218 def get_xdg_cache_dir():
219 def get_xdg_cache_dir():
219 """Return the XDG_CACHE_HOME, if it is defined and exists, else None.
220 """Return the XDG_CACHE_HOME, if it is defined and exists, else None.
220
221
221 This is only for non-OS X posix (Linux,Unix,etc.) systems.
222 This is only for non-OS X posix (Linux,Unix,etc.) systems.
222 """
223 """
223
224
224 env = os.environ
225 env = os.environ
225
226
226 if os.name == "posix":
227 if os.name == "posix":
227 # Linux, Unix, AIX, etc.
228 # Linux, Unix, AIX, etc.
228 # use ~/.cache if empty OR not set
229 # use ~/.cache if empty OR not set
229 xdg = env.get("XDG_CACHE_HOME", None) or os.path.join(get_home_dir(), '.cache')
230 xdg = env.get("XDG_CACHE_HOME", None) or os.path.join(get_home_dir(), '.cache')
230 if xdg and _writable_dir(xdg):
231 if xdg and _writable_dir(xdg):
231 assert isinstance(xdg, str)
232 assert isinstance(xdg, str)
232 return xdg
233 return xdg
233
234
234 return None
235 return None
235
236
236
237
237 def expand_path(s):
238 def expand_path(s):
238 """Expand $VARS and ~names in a string, like a shell
239 """Expand $VARS and ~names in a string, like a shell
239
240
240 :Examples:
241 :Examples:
241
242
242 In [2]: os.environ['FOO']='test'
243 In [2]: os.environ['FOO']='test'
243
244
244 In [3]: expand_path('variable FOO is $FOO')
245 In [3]: expand_path('variable FOO is $FOO')
245 Out[3]: 'variable FOO is test'
246 Out[3]: 'variable FOO is test'
246 """
247 """
247 # This is a pretty subtle hack. When expand user is given a UNC path
248 # This is a pretty subtle hack. When expand user is given a UNC path
248 # on Windows (\\server\share$\%username%), os.path.expandvars, removes
249 # on Windows (\\server\share$\%username%), os.path.expandvars, removes
249 # the $ to get (\\server\share\%username%). I think it considered $
250 # the $ to get (\\server\share\%username%). I think it considered $
250 # alone an empty var. But, we need the $ to remains there (it indicates
251 # alone an empty var. But, we need the $ to remains there (it indicates
251 # a hidden share).
252 # a hidden share).
252 if os.name=='nt':
253 if os.name=='nt':
253 s = s.replace('$\\', 'IPYTHON_TEMP')
254 s = s.replace('$\\', 'IPYTHON_TEMP')
254 s = os.path.expandvars(os.path.expanduser(s))
255 s = os.path.expandvars(os.path.expanduser(s))
255 if os.name=='nt':
256 if os.name=='nt':
256 s = s.replace('IPYTHON_TEMP', '$\\')
257 s = s.replace('IPYTHON_TEMP', '$\\')
257 return s
258 return s
258
259
259
260
260 def unescape_glob(string):
261 def unescape_glob(string):
261 """Unescape glob pattern in `string`."""
262 """Unescape glob pattern in `string`."""
262 def unescape(s):
263 def unescape(s):
263 for pattern in '*[]!?':
264 for pattern in '*[]!?':
264 s = s.replace(r'\{0}'.format(pattern), pattern)
265 s = s.replace(r'\{0}'.format(pattern), pattern)
265 return s
266 return s
266 return '\\'.join(map(unescape, string.split('\\\\')))
267 return '\\'.join(map(unescape, string.split('\\\\')))
267
268
268
269
269 def shellglob(args):
270 def shellglob(args):
270 """
271 """
271 Do glob expansion for each element in `args` and return a flattened list.
272 Do glob expansion for each element in `args` and return a flattened list.
272
273
273 Unmatched glob pattern will remain as-is in the returned list.
274 Unmatched glob pattern will remain as-is in the returned list.
274
275
275 """
276 """
276 expanded = []
277 expanded = []
277 # Do not unescape backslash in Windows as it is interpreted as
278 # Do not unescape backslash in Windows as it is interpreted as
278 # path separator:
279 # path separator:
279 unescape = unescape_glob if sys.platform != 'win32' else lambda x: x
280 unescape = unescape_glob if sys.platform != 'win32' else lambda x: x
280 for a in args:
281 for a in args:
281 expanded.extend(glob.glob(a) or [unescape(a)])
282 expanded.extend(glob.glob(a) or [unescape(a)])
282 return expanded
283 return expanded
283
284
284
285
285 def target_outdated(target,deps):
286 def target_outdated(target,deps):
286 """Determine whether a target is out of date.
287 """Determine whether a target is out of date.
287
288
288 target_outdated(target,deps) -> 1/0
289 target_outdated(target,deps) -> 1/0
289
290
290 deps: list of filenames which MUST exist.
291 deps: list of filenames which MUST exist.
291 target: single filename which may or may not exist.
292 target: single filename which may or may not exist.
292
293
293 If target doesn't exist or is older than any file listed in deps, return
294 If target doesn't exist or is older than any file listed in deps, return
294 true, otherwise return false.
295 true, otherwise return false.
296
297 .. deprecated:: 8.22
295 """
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 )
296 try:
304 try:
297 target_time = os.path.getmtime(target)
305 target_time = os.path.getmtime(target)
298 except os.error:
306 except os.error:
299 return 1
307 return 1
300 for dep in deps:
308 for dep in deps:
301 dep_time = os.path.getmtime(dep)
309 dep_time = os.path.getmtime(dep)
302 if dep_time > target_time:
310 if dep_time > target_time:
303 #print "For target",target,"Dep failed:",dep # dbg
311 #print "For target",target,"Dep failed:",dep # dbg
304 #print "times (dep,tar):",dep_time,target_time # dbg
312 #print "times (dep,tar):",dep_time,target_time # dbg
305 return 1
313 return 1
306 return 0
314 return 0
307
315
308
316
309 def target_update(target,deps,cmd):
317 def target_update(target,deps,cmd):
310 """Update a target with a given command given a list of dependencies.
318 """Update a target with a given command given a list of dependencies.
311
319
312 target_update(target,deps,cmd) -> runs cmd if target is outdated.
320 target_update(target,deps,cmd) -> runs cmd if target is outdated.
313
321
314 This is just a wrapper around target_outdated() which calls the given
322 This is just a wrapper around target_outdated() which calls the given
315 command if target is outdated."""
323 command if target is outdated.
324
325 .. deprecated:: 8.22
326 """
316
327
317 if target_outdated(target,deps):
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):
318 system(cmd)
334 system(cmd)
319
335
320
336
321 ENOLINK = 1998
337 ENOLINK = 1998
322
338
323 def link(src, dst):
339 def link(src, dst):
324 """Hard links ``src`` to ``dst``, returning 0 or errno.
340 """Hard links ``src`` to ``dst``, returning 0 or errno.
325
341
326 Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't
342 Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't
327 supported by the operating system.
343 supported by the operating system.
328 """
344 """
329
345
330 if not hasattr(os, "link"):
346 if not hasattr(os, "link"):
331 return ENOLINK
347 return ENOLINK
332 link_errno = 0
348 link_errno = 0
333 try:
349 try:
334 os.link(src, dst)
350 os.link(src, dst)
335 except OSError as e:
351 except OSError as e:
336 link_errno = e.errno
352 link_errno = e.errno
337 return link_errno
353 return link_errno
338
354
339
355
340 def link_or_copy(src, dst):
356 def link_or_copy(src, dst):
341 """Attempts to hardlink ``src`` to ``dst``, copying if the link fails.
357 """Attempts to hardlink ``src`` to ``dst``, copying if the link fails.
342
358
343 Attempts to maintain the semantics of ``shutil.copy``.
359 Attempts to maintain the semantics of ``shutil.copy``.
344
360
345 Because ``os.link`` does not overwrite files, a unique temporary file
361 Because ``os.link`` does not overwrite files, a unique temporary file
346 will be used if the target already exists, then that file will be moved
362 will be used if the target already exists, then that file will be moved
347 into place.
363 into place.
348 """
364 """
349
365
350 if os.path.isdir(dst):
366 if os.path.isdir(dst):
351 dst = os.path.join(dst, os.path.basename(src))
367 dst = os.path.join(dst, os.path.basename(src))
352
368
353 link_errno = link(src, dst)
369 link_errno = link(src, dst)
354 if link_errno == errno.EEXIST:
370 if link_errno == errno.EEXIST:
355 if os.stat(src).st_ino == os.stat(dst).st_ino:
371 if os.stat(src).st_ino == os.stat(dst).st_ino:
356 # dst is already a hard link to the correct file, so we don't need
372 # dst is already a hard link to the correct file, so we don't need
357 # to do anything else. If we try to link and rename the file
373 # to do anything else. If we try to link and rename the file
358 # anyway, we get duplicate files - see http://bugs.python.org/issue21876
374 # anyway, we get duplicate files - see http://bugs.python.org/issue21876
359 return
375 return
360
376
361 new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), )
377 new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), )
362 try:
378 try:
363 link_or_copy(src, new_dst)
379 link_or_copy(src, new_dst)
364 except:
380 except:
365 try:
381 try:
366 os.remove(new_dst)
382 os.remove(new_dst)
367 except OSError:
383 except OSError:
368 pass
384 pass
369 raise
385 raise
370 os.rename(new_dst, dst)
386 os.rename(new_dst, dst)
371 elif link_errno != 0:
387 elif link_errno != 0:
372 # Either link isn't supported, or the filesystem doesn't support
388 # Either link isn't supported, or the filesystem doesn't support
373 # linking, or 'src' and 'dst' are on different filesystems.
389 # linking, or 'src' and 'dst' are on different filesystems.
374 shutil.copy(src, dst)
390 shutil.copy(src, dst)
375
391
376 def ensure_dir_exists(path, mode=0o755):
392 def ensure_dir_exists(path, mode=0o755):
377 """ensure that a directory exists
393 """ensure that a directory exists
378
394
379 If it doesn't exist, try to create it and protect against a race condition
395 If it doesn't exist, try to create it and protect against a race condition
380 if another process is doing the same.
396 if another process is doing the same.
381
397
382 The default permissions are 755, which differ from os.makedirs default of 777.
398 The default permissions are 755, which differ from os.makedirs default of 777.
383 """
399 """
384 if not os.path.exists(path):
400 if not os.path.exists(path):
385 try:
401 try:
386 os.makedirs(path, mode=mode)
402 os.makedirs(path, mode=mode)
387 except OSError as e:
403 except OSError as e:
388 if e.errno != errno.EEXIST:
404 if e.errno != errno.EEXIST:
389 raise
405 raise
390 elif not os.path.isdir(path):
406 elif not os.path.isdir(path):
391 raise IOError("%r exists but is not a directory" % path)
407 raise IOError("%r exists but is not a directory" % path)
General Comments 0
You need to be logged in to leave comments. Login now