Merged changes from master
[zeroinstall/solver.git] / zeroinstall / support / __init__.py
blobd1cc51a0436f223930357850d7b0fa7cefafc298
1 """
2 Useful support routines (for internal use).
4 These functions aren't really Zero Install specific; they're things we might
5 wish were in the standard library.
7 @since: 0.27
8 """
10 # Copyright (C) 2009, Thomas Leonard
11 # See the README file for details, or visit http://0install.net.
13 from zeroinstall import _, logger
14 import sys, os
16 def find_in_path(prog):
17 """Search $PATH for prog.
18 If prog is an absolute path, return it unmodified.
19 @param prog: name of executable to find
20 @return: the full path of prog, or None if not found
21 @since: 0.27
22 """
23 if os.path.isabs(prog): return prog
24 if os.name == "nt":
25 prog += '.exe'
26 for d in os.environ.get('PATH', '/bin:/usr/bin').split(os.pathsep):
27 path = os.path.join(d, prog)
28 if os.path.isfile(path):
29 return path
30 return None
32 def read_bytes(fd, nbytes, null_ok = False):
33 """Read exactly nbytes from fd.
34 @param fd: file descriptor to read from
35 @param nbytes: number of bytes to read
36 @param null_ok: if True, it's OK to receive EOF immediately (we then return None)
37 @return: the bytes read
38 @raise Exception: if we received less than nbytes of data
39 """
40 data = b''
41 while nbytes:
42 got = os.read(fd, nbytes)
43 if not got:
44 if null_ok and not data:
45 return None
46 raise Exception(_("Unexpected end-of-stream. Data so far %(data)s; expecting %(bytes)d bytes more.")
47 % {'data': repr(data), 'bytes': nbytes})
48 data += got
49 nbytes -= len(got)
50 logger.debug(_("Message received: %r"), data)
51 return data
53 def pretty_size(size):
54 """Format a size for printing.
55 @param size: the size in bytes
56 @type size: int (or None)
57 @return: the formatted size
58 @rtype: str
59 @since: 0.27"""
60 if size is None:
61 return '?'
62 if size < 2048:
63 return _('%d bytes') % size
64 size = float(size)
65 for unit in (_('KB'), _('MB'), _('GB'), _('TB')):
66 size /= 1024
67 if size < 2048:
68 break
69 return _('%(size).1f %(unit)s') % {'size': size, 'unit': unit}
71 def ro_rmtree(root):
72 """Like shutil.rmtree, except that we also delete read-only items.
73 @param root: the root of the subtree to remove
74 @type root: str
75 @since: 0.28"""
76 import shutil
77 import platform
78 if (os.getcwd() + os.path.sep).startswith(root + os.path.sep):
79 import warnings
80 warnings.warn("Removing tree ({tree}) containing the current directory ({cwd}) - this will not work on Windows".format(cwd = os.getcwd(), tree = root), stacklevel = 2)
82 if platform.system() == 'Windows':
83 for main, dirs, files in os.walk(root):
84 for i in files + dirs:
85 os.chmod(os.path.join(main, i), 0o700)
86 os.chmod(root, 0o700)
87 else:
88 for main, dirs, files in os.walk(root):
89 os.chmod(main, 0o700)
90 shutil.rmtree(root)
92 def raise_with_traceback(ex, tb):
93 """Raise an exception in a way that works on Python 2 and Python 3"""
94 if hasattr(ex, 'with_traceback'):
95 raise ex # Python 3
96 exec("raise ex, None, tb", {'ex': ex, 'tb': tb}) # Python 2
97 assert 0
99 def portable_rename(src, dst):
100 """Rename 'src' to 'dst', which must be on the same filesystem.
101 On POSIX systems, this operation is atomic.
102 On Windows, do the best we can by deleting dst and then renaming.
103 @since: 1.9"""
104 if os.name == "nt" and os.path.exists(dst):
105 os.unlink(dst)
106 os.rename(src, dst)
108 def windows_args_escape(args):
109 """Combines multiple strings into one for use as a Windows command-line argument.
110 This coressponds to Windows' handling of command-line arguments as specified in: http://msdn.microsoft.com/library/17w5ykft.
111 @since: 1.11"""
112 def _escape(arg):
113 # Add leading quotation mark if there are whitespaces
114 import string
115 contains_whitespace = any(whitespace in arg for whitespace in string.whitespace)
116 result = '"' if contains_whitespace else ''
118 # Split by quotation marks
119 parts = arg.split('"')
120 for i, part in enumerate(parts):
121 # Count slashes preceeding the quotation mark
122 slashes_count = len(part) - len(part.rstrip('\\'))
124 result = result + part
125 if i < len(parts) - 1:
126 # Not last part
127 result = result + ("\\" * slashes_count) # Double number of slashes
128 result = result + "\\" + '"' # Escaped quotation mark
129 elif contains_whitespace:
130 # Last part if there are whitespaces
131 result = result + ("\\" * slashes_count) # Double number of slashes
132 result = result + '"' # Non-escaped quotation mark
134 return result
136 return ' '.join(map(_escape, args))
138 if sys.version_info[0] > 2:
139 # Python 3
140 unicode = str
141 basestring = str
142 intern = sys.intern
143 raw_input = input
145 def urlparse(url):
146 from urllib import parse
147 return parse.urlparse(url)
148 else:
149 # Python 2
150 unicode = unicode # (otherwise it can't be imported)
151 basestring = basestring
152 intern = intern
153 raw_input = raw_input
155 def urlparse(url):
156 import urlparse
157 return urlparse.urlparse(url)