##// END OF EJS Templates
Backport PR #5806: Don't auto-move .config/ipython if symbolic link
Thomas Kluyver -
Show More
@@ -1,574 +1,577 b''
1 1 # encoding: utf-8
2 2 """
3 3 Utilities for path handling.
4 4 """
5 5
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (C) 2008-2011 The IPython Development Team
8 8 #
9 9 # Distributed under the terms of the BSD License. The full license is in
10 10 # the file COPYING, distributed as part of this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16
17 17 import os
18 18 import sys
19 19 import errno
20 20 import shutil
21 21 import random
22 22 import tempfile
23 23 import warnings
24 24 from hashlib import md5
25 25 import glob
26 26
27 27 import IPython
28 28 from IPython.testing.skipdoctest import skip_doctest
29 29 from IPython.utils.process import system
30 30 from IPython.utils.importstring import import_item
31 31 from IPython.utils import py3compat
32 32 #-----------------------------------------------------------------------------
33 33 # Code
34 34 #-----------------------------------------------------------------------------
35 35
36 36 fs_encoding = sys.getfilesystemencoding()
37 37
38 38 def _get_long_path_name(path):
39 39 """Dummy no-op."""
40 40 return path
41 41
42 42 def _writable_dir(path):
43 43 """Whether `path` is a directory, to which the user has write access."""
44 44 return os.path.isdir(path) and os.access(path, os.W_OK)
45 45
46 46 if sys.platform == 'win32':
47 47 @skip_doctest
48 48 def _get_long_path_name(path):
49 49 """Get a long path name (expand ~) on Windows using ctypes.
50 50
51 51 Examples
52 52 --------
53 53
54 54 >>> get_long_path_name('c:\\docume~1')
55 55 u'c:\\\\Documents and Settings'
56 56
57 57 """
58 58 try:
59 59 import ctypes
60 60 except ImportError:
61 61 raise ImportError('you need to have ctypes installed for this to work')
62 62 _GetLongPathName = ctypes.windll.kernel32.GetLongPathNameW
63 63 _GetLongPathName.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p,
64 64 ctypes.c_uint ]
65 65
66 66 buf = ctypes.create_unicode_buffer(260)
67 67 rv = _GetLongPathName(path, buf, 260)
68 68 if rv == 0 or rv > 260:
69 69 return path
70 70 else:
71 71 return buf.value
72 72
73 73
74 74 def get_long_path_name(path):
75 75 """Expand a path into its long form.
76 76
77 77 On Windows this expands any ~ in the paths. On other platforms, it is
78 78 a null operation.
79 79 """
80 80 return _get_long_path_name(path)
81 81
82 82
83 83 def unquote_filename(name, win32=(sys.platform=='win32')):
84 84 """ On Windows, remove leading and trailing quotes from filenames.
85 85 """
86 86 if win32:
87 87 if name.startswith(("'", '"')) and name.endswith(("'", '"')):
88 88 name = name[1:-1]
89 89 return name
90 90
91 91 def compress_user(path):
92 92 """Reverse of :func:`os.path.expanduser`
93 93 """
94 94 home = os.path.expanduser('~')
95 95 if path.startswith(home):
96 96 path = "~" + path[len(home):]
97 97 return path
98 98
99 99 def get_py_filename(name, force_win32=None):
100 100 """Return a valid python filename in the current directory.
101 101
102 102 If the given name is not a file, it adds '.py' and searches again.
103 103 Raises IOError with an informative message if the file isn't found.
104 104
105 105 On Windows, apply Windows semantics to the filename. In particular, remove
106 106 any quoting that has been applied to it. This option can be forced for
107 107 testing purposes.
108 108 """
109 109
110 110 name = os.path.expanduser(name)
111 111 if force_win32 is None:
112 112 win32 = (sys.platform == 'win32')
113 113 else:
114 114 win32 = force_win32
115 115 name = unquote_filename(name, win32=win32)
116 116 if not os.path.isfile(name) and not name.endswith('.py'):
117 117 name += '.py'
118 118 if os.path.isfile(name):
119 119 return name
120 120 else:
121 121 raise IOError('File `%r` not found.' % name)
122 122
123 123
124 124 def filefind(filename, path_dirs=None):
125 125 """Find a file by looking through a sequence of paths.
126 126
127 127 This iterates through a sequence of paths looking for a file and returns
128 128 the full, absolute path of the first occurence of the file. If no set of
129 129 path dirs is given, the filename is tested as is, after running through
130 130 :func:`expandvars` and :func:`expanduser`. Thus a simple call::
131 131
132 132 filefind('myfile.txt')
133 133
134 134 will find the file in the current working dir, but::
135 135
136 136 filefind('~/myfile.txt')
137 137
138 138 Will find the file in the users home directory. This function does not
139 139 automatically try any paths, such as the cwd or the user's home directory.
140 140
141 141 Parameters
142 142 ----------
143 143 filename : str
144 144 The filename to look for.
145 145 path_dirs : str, None or sequence of str
146 146 The sequence of paths to look for the file in. If None, the filename
147 147 need to be absolute or be in the cwd. If a string, the string is
148 148 put into a sequence and the searched. If a sequence, walk through
149 149 each element and join with ``filename``, calling :func:`expandvars`
150 150 and :func:`expanduser` before testing for existence.
151 151
152 152 Returns
153 153 -------
154 154 Raises :exc:`IOError` or returns absolute path to file.
155 155 """
156 156
157 157 # If paths are quoted, abspath gets confused, strip them...
158 158 filename = filename.strip('"').strip("'")
159 159 # If the input is an absolute path, just check it exists
160 160 if os.path.isabs(filename) and os.path.isfile(filename):
161 161 return filename
162 162
163 163 if path_dirs is None:
164 164 path_dirs = ("",)
165 165 elif isinstance(path_dirs, py3compat.string_types):
166 166 path_dirs = (path_dirs,)
167 167
168 168 for path in path_dirs:
169 169 if path == '.': path = py3compat.getcwd()
170 170 testname = expand_path(os.path.join(path, filename))
171 171 if os.path.isfile(testname):
172 172 return os.path.abspath(testname)
173 173
174 174 raise IOError("File %r does not exist in any of the search paths: %r" %
175 175 (filename, path_dirs) )
176 176
177 177
178 178 class HomeDirError(Exception):
179 179 pass
180 180
181 181
182 182 def get_home_dir(require_writable=False):
183 183 """Return the 'home' directory, as a unicode string.
184 184
185 185 Uses os.path.expanduser('~'), and checks for writability.
186 186
187 187 See stdlib docs for how this is determined.
188 188 $HOME is first priority on *ALL* platforms.
189 189
190 190 Parameters
191 191 ----------
192 192
193 193 require_writable : bool [default: False]
194 194 if True:
195 195 guarantees the return value is a writable directory, otherwise
196 196 raises HomeDirError
197 197 if False:
198 198 The path is resolved, but it is not guaranteed to exist or be writable.
199 199 """
200 200
201 201 homedir = os.path.expanduser('~')
202 202 # Next line will make things work even when /home/ is a symlink to
203 203 # /usr/home as it is on FreeBSD, for example
204 204 homedir = os.path.realpath(homedir)
205 205
206 206 if not _writable_dir(homedir) and os.name == 'nt':
207 207 # expanduser failed, use the registry to get the 'My Documents' folder.
208 208 try:
209 209 try:
210 210 import winreg as wreg # Py 3
211 211 except ImportError:
212 212 import _winreg as wreg # Py 2
213 213 key = wreg.OpenKey(
214 214 wreg.HKEY_CURRENT_USER,
215 215 "Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
216 216 )
217 217 homedir = wreg.QueryValueEx(key,'Personal')[0]
218 218 key.Close()
219 219 except:
220 220 pass
221 221
222 222 if (not require_writable) or _writable_dir(homedir):
223 223 return py3compat.cast_unicode(homedir, fs_encoding)
224 224 else:
225 225 raise HomeDirError('%s is not a writable dir, '
226 226 'set $HOME environment variable to override' % homedir)
227 227
228 228 def get_xdg_dir():
229 229 """Return the XDG_CONFIG_HOME, if it is defined and exists, else None.
230 230
231 231 This is only for non-OS X posix (Linux,Unix,etc.) systems.
232 232 """
233 233
234 234 env = os.environ
235 235
236 236 if os.name == 'posix' and sys.platform != 'darwin':
237 237 # Linux, Unix, AIX, etc.
238 238 # use ~/.config if empty OR not set
239 239 xdg = env.get("XDG_CONFIG_HOME", None) or os.path.join(get_home_dir(), '.config')
240 240 if xdg and _writable_dir(xdg):
241 241 return py3compat.cast_unicode(xdg, fs_encoding)
242 242
243 243 return None
244 244
245 245
246 246 def get_xdg_cache_dir():
247 247 """Return the XDG_CACHE_HOME, if it is defined and exists, else None.
248 248
249 249 This is only for non-OS X posix (Linux,Unix,etc.) systems.
250 250 """
251 251
252 252 env = os.environ
253 253
254 254 if os.name == 'posix' and sys.platform != 'darwin':
255 255 # Linux, Unix, AIX, etc.
256 256 # use ~/.cache if empty OR not set
257 257 xdg = env.get("XDG_CACHE_HOME", None) or os.path.join(get_home_dir(), '.cache')
258 258 if xdg and _writable_dir(xdg):
259 259 return py3compat.cast_unicode(xdg, fs_encoding)
260 260
261 261 return None
262 262
263 263
264 264 def get_ipython_dir():
265 265 """Get the IPython directory for this platform and user.
266 266
267 267 This uses the logic in `get_home_dir` to find the home directory
268 268 and then adds .ipython to the end of the path.
269 269 """
270 270
271 271 env = os.environ
272 272 pjoin = os.path.join
273 273
274 274
275 275 ipdir_def = '.ipython'
276 276
277 277 home_dir = get_home_dir()
278 278 xdg_dir = get_xdg_dir()
279 279
280 280 # import pdb; pdb.set_trace() # dbg
281 281 if 'IPYTHON_DIR' in env:
282 282 warnings.warn('The environment variable IPYTHON_DIR is deprecated. '
283 283 'Please use IPYTHONDIR instead.')
284 284 ipdir = env.get('IPYTHONDIR', env.get('IPYTHON_DIR', None))
285 285 if ipdir is None:
286 286 # not set explicitly, use ~/.ipython
287 287 ipdir = pjoin(home_dir, ipdir_def)
288 288 if xdg_dir:
289 289 # Several IPython versions (up to 1.x) defaulted to .config/ipython
290 290 # on Linux. We have decided to go back to using .ipython everywhere
291 291 xdg_ipdir = pjoin(xdg_dir, 'ipython')
292 292
293 293 if _writable_dir(xdg_ipdir):
294 294 cu = compress_user
295 295 if os.path.exists(ipdir):
296 296 warnings.warn(('Ignoring {0} in favour of {1}. Remove {0} '
297 297 'to get rid of this message').format(cu(xdg_ipdir), cu(ipdir)))
298 elif os.path.islink(xdg_ipdir):
299 warnings.warn(('{0} is deprecated. Move link to {1} '
300 'to get rid of this message').format(cu(xdg_ipdir), cu(ipdir)))
298 301 else:
299 302 warnings.warn('Moving {0} to {1}'.format(cu(xdg_ipdir), cu(ipdir)))
300 303 os.rename(xdg_ipdir, ipdir)
301 304
302 305 ipdir = os.path.normpath(os.path.expanduser(ipdir))
303 306
304 307 if os.path.exists(ipdir) and not _writable_dir(ipdir):
305 308 # ipdir exists, but is not writable
306 309 warnings.warn("IPython dir '%s' is not a writable location,"
307 310 " using a temp directory."%ipdir)
308 311 ipdir = tempfile.mkdtemp()
309 312 elif not os.path.exists(ipdir):
310 313 parent = os.path.dirname(ipdir)
311 314 if not _writable_dir(parent):
312 315 # ipdir does not exist and parent isn't writable
313 316 warnings.warn("IPython parent '%s' is not a writable location,"
314 317 " using a temp directory."%parent)
315 318 ipdir = tempfile.mkdtemp()
316 319
317 320 return py3compat.cast_unicode(ipdir, fs_encoding)
318 321
319 322
320 323 def get_ipython_cache_dir():
321 324 """Get the cache directory it is created if it does not exist."""
322 325 xdgdir = get_xdg_cache_dir()
323 326 if xdgdir is None:
324 327 return get_ipython_dir()
325 328 ipdir = os.path.join(xdgdir, "ipython")
326 329 if not os.path.exists(ipdir) and _writable_dir(xdgdir):
327 330 os.makedirs(ipdir)
328 331 elif not _writable_dir(xdgdir):
329 332 return get_ipython_dir()
330 333
331 334 return py3compat.cast_unicode(ipdir, fs_encoding)
332 335
333 336
334 337 def get_ipython_package_dir():
335 338 """Get the base directory where IPython itself is installed."""
336 339 ipdir = os.path.dirname(IPython.__file__)
337 340 return py3compat.cast_unicode(ipdir, fs_encoding)
338 341
339 342
340 343 def get_ipython_module_path(module_str):
341 344 """Find the path to an IPython module in this version of IPython.
342 345
343 346 This will always find the version of the module that is in this importable
344 347 IPython package. This will always return the path to the ``.py``
345 348 version of the module.
346 349 """
347 350 if module_str == 'IPython':
348 351 return os.path.join(get_ipython_package_dir(), '__init__.py')
349 352 mod = import_item(module_str)
350 353 the_path = mod.__file__.replace('.pyc', '.py')
351 354 the_path = the_path.replace('.pyo', '.py')
352 355 return py3compat.cast_unicode(the_path, fs_encoding)
353 356
354 357 def locate_profile(profile='default'):
355 358 """Find the path to the folder associated with a given profile.
356 359
357 360 I.e. find $IPYTHONDIR/profile_whatever.
358 361 """
359 362 from IPython.core.profiledir import ProfileDir, ProfileDirError
360 363 try:
361 364 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
362 365 except ProfileDirError:
363 366 # IOError makes more sense when people are expecting a path
364 367 raise IOError("Couldn't find profile %r" % profile)
365 368 return pd.location
366 369
367 370 def expand_path(s):
368 371 """Expand $VARS and ~names in a string, like a shell
369 372
370 373 :Examples:
371 374
372 375 In [2]: os.environ['FOO']='test'
373 376
374 377 In [3]: expand_path('variable FOO is $FOO')
375 378 Out[3]: 'variable FOO is test'
376 379 """
377 380 # This is a pretty subtle hack. When expand user is given a UNC path
378 381 # on Windows (\\server\share$\%username%), os.path.expandvars, removes
379 382 # the $ to get (\\server\share\%username%). I think it considered $
380 383 # alone an empty var. But, we need the $ to remains there (it indicates
381 384 # a hidden share).
382 385 if os.name=='nt':
383 386 s = s.replace('$\\', 'IPYTHON_TEMP')
384 387 s = os.path.expandvars(os.path.expanduser(s))
385 388 if os.name=='nt':
386 389 s = s.replace('IPYTHON_TEMP', '$\\')
387 390 return s
388 391
389 392
390 393 def unescape_glob(string):
391 394 """Unescape glob pattern in `string`."""
392 395 def unescape(s):
393 396 for pattern in '*[]!?':
394 397 s = s.replace(r'\{0}'.format(pattern), pattern)
395 398 return s
396 399 return '\\'.join(map(unescape, string.split('\\\\')))
397 400
398 401
399 402 def shellglob(args):
400 403 """
401 404 Do glob expansion for each element in `args` and return a flattened list.
402 405
403 406 Unmatched glob pattern will remain as-is in the returned list.
404 407
405 408 """
406 409 expanded = []
407 410 # Do not unescape backslash in Windows as it is interpreted as
408 411 # path separator:
409 412 unescape = unescape_glob if sys.platform != 'win32' else lambda x: x
410 413 for a in args:
411 414 expanded.extend(glob.glob(a) or [unescape(a)])
412 415 return expanded
413 416
414 417
415 418 def target_outdated(target,deps):
416 419 """Determine whether a target is out of date.
417 420
418 421 target_outdated(target,deps) -> 1/0
419 422
420 423 deps: list of filenames which MUST exist.
421 424 target: single filename which may or may not exist.
422 425
423 426 If target doesn't exist or is older than any file listed in deps, return
424 427 true, otherwise return false.
425 428 """
426 429 try:
427 430 target_time = os.path.getmtime(target)
428 431 except os.error:
429 432 return 1
430 433 for dep in deps:
431 434 dep_time = os.path.getmtime(dep)
432 435 if dep_time > target_time:
433 436 #print "For target",target,"Dep failed:",dep # dbg
434 437 #print "times (dep,tar):",dep_time,target_time # dbg
435 438 return 1
436 439 return 0
437 440
438 441
439 442 def target_update(target,deps,cmd):
440 443 """Update a target with a given command given a list of dependencies.
441 444
442 445 target_update(target,deps,cmd) -> runs cmd if target is outdated.
443 446
444 447 This is just a wrapper around target_outdated() which calls the given
445 448 command if target is outdated."""
446 449
447 450 if target_outdated(target,deps):
448 451 system(cmd)
449 452
450 453 def filehash(path):
451 454 """Make an MD5 hash of a file, ignoring any differences in line
452 455 ending characters."""
453 456 with open(path, "rU") as f:
454 457 return md5(py3compat.str_to_bytes(f.read())).hexdigest()
455 458
456 459 # If the config is unmodified from the default, we'll just delete it.
457 460 # These are consistent for 0.10.x, thankfully. We're not going to worry about
458 461 # older versions.
459 462 old_config_md5 = {'ipy_user_conf.py': 'fc108bedff4b9a00f91fa0a5999140d3',
460 463 'ipythonrc': '12a68954f3403eea2eec09dc8fe5a9b5'}
461 464
462 465 def check_for_old_config(ipython_dir=None):
463 466 """Check for old config files, and present a warning if they exist.
464 467
465 468 A link to the docs of the new config is included in the message.
466 469
467 470 This should mitigate confusion with the transition to the new
468 471 config system in 0.11.
469 472 """
470 473 if ipython_dir is None:
471 474 ipython_dir = get_ipython_dir()
472 475
473 476 old_configs = ['ipy_user_conf.py', 'ipythonrc', 'ipython_config.py']
474 477 warned = False
475 478 for cfg in old_configs:
476 479 f = os.path.join(ipython_dir, cfg)
477 480 if os.path.exists(f):
478 481 if filehash(f) == old_config_md5.get(cfg, ''):
479 482 os.unlink(f)
480 483 else:
481 484 warnings.warn("Found old IPython config file %r (modified by user)"%f)
482 485 warned = True
483 486
484 487 if warned:
485 488 warnings.warn("""
486 489 The IPython configuration system has changed as of 0.11, and these files will
487 490 be ignored. See http://ipython.github.com/ipython-doc/dev/config for details
488 491 of the new config system.
489 492 To start configuring IPython, do `ipython profile create`, and edit
490 493 `ipython_config.py` in <ipython_dir>/profile_default.
491 494 If you need to leave the old config files in place for an older version of
492 495 IPython and want to suppress this warning message, set
493 496 `c.InteractiveShellApp.ignore_old_config=True` in the new config.""")
494 497
495 498 def get_security_file(filename, profile='default'):
496 499 """Return the absolute path of a security file given by filename and profile
497 500
498 501 This allows users and developers to find security files without
499 502 knowledge of the IPython directory structure. The search path
500 503 will be ['.', profile.security_dir]
501 504
502 505 Parameters
503 506 ----------
504 507
505 508 filename : str
506 509 The file to be found. If it is passed as an absolute path, it will
507 510 simply be returned.
508 511 profile : str [default: 'default']
509 512 The name of the profile to search. Leaving this unspecified
510 513 The file to be found. If it is passed as an absolute path, fname will
511 514 simply be returned.
512 515
513 516 Returns
514 517 -------
515 518 Raises :exc:`IOError` if file not found or returns absolute path to file.
516 519 """
517 520 # import here, because profiledir also imports from utils.path
518 521 from IPython.core.profiledir import ProfileDir
519 522 try:
520 523 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
521 524 except Exception:
522 525 # will raise ProfileDirError if no such profile
523 526 raise IOError("Profile %r not found")
524 527 return filefind(filename, ['.', pd.security_dir])
525 528
526 529
527 530 ENOLINK = 1998
528 531
529 532 def link(src, dst):
530 533 """Hard links ``src`` to ``dst``, returning 0 or errno.
531 534
532 535 Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't
533 536 supported by the operating system.
534 537 """
535 538
536 539 if not hasattr(os, "link"):
537 540 return ENOLINK
538 541 link_errno = 0
539 542 try:
540 543 os.link(src, dst)
541 544 except OSError as e:
542 545 link_errno = e.errno
543 546 return link_errno
544 547
545 548
546 549 def link_or_copy(src, dst):
547 550 """Attempts to hardlink ``src`` to ``dst``, copying if the link fails.
548 551
549 552 Attempts to maintain the semantics of ``shutil.copy``.
550 553
551 554 Because ``os.link`` does not overwrite files, a unique temporary file
552 555 will be used if the target already exists, then that file will be moved
553 556 into place.
554 557 """
555 558
556 559 if os.path.isdir(dst):
557 560 dst = os.path.join(dst, os.path.basename(src))
558 561
559 562 link_errno = link(src, dst)
560 563 if link_errno == errno.EEXIST:
561 564 new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), )
562 565 try:
563 566 link_or_copy(src, new_dst)
564 567 except:
565 568 try:
566 569 os.remove(new_dst)
567 570 except OSError:
568 571 pass
569 572 raise
570 573 os.rename(new_dst, dst)
571 574 elif link_errno != 0:
572 575 # Either link isn't supported, or the filesystem doesn't support
573 576 # linking, or 'src' and 'dst' are on different filesystems.
574 577 shutil.copy(src, dst)
General Comments 0
You need to be logged in to leave comments. Login now