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