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