utils: add split() and pathjoin()
[git-cola.git] / cola / utils.py
blob26df5f5b15134563d8f085895e6d108eb4857324
1 """Miscellaneous utility functions"""
2 from __future__ import absolute_import, division, print_function, unicode_literals
3 import copy
4 import os
5 import random
6 import re
7 import shlex
8 import sys
9 import tempfile
10 import time
11 import traceback
13 from . import core
14 from . import compat
16 random.seed(hash(time.time()))
19 def asint(obj, default=0):
20 """Make any value into an int, even if the cast fails"""
21 try:
22 value = int(obj)
23 except (TypeError, ValueError):
24 value = default
25 return value
28 def clamp(value, lo, hi):
29 """Clamp a value to the specified range"""
30 return min(hi, max(lo, value))
33 def epoch_millis():
34 return int(time.time() * 1000)
37 def add_parents(paths):
38 """Iterate over each item in the set and add its parent directories."""
39 all_paths = set()
40 for path in paths:
41 while '//' in path:
42 path = path.replace('//', '/')
43 all_paths.add(path)
44 if '/' in path:
45 parent_dir = dirname(path)
46 while parent_dir:
47 all_paths.add(parent_dir)
48 parent_dir = dirname(parent_dir)
49 return all_paths
52 def format_exception(e):
53 exc_type, exc_value, exc_tb = sys.exc_info()
54 details = traceback.format_exception(exc_type, exc_value, exc_tb)
55 details = '\n'.join(map(core.decode, details))
56 if hasattr(e, 'msg'):
57 msg = e.msg
58 else:
59 msg = core.decode(repr(e))
60 return (msg, details)
63 def sublist(a, b):
64 """Subtracts list b from list a and returns the resulting list."""
65 # conceptually, c = a - b
66 c = []
67 for item in a:
68 if item not in b:
69 c.append(item)
70 return c
73 __grep_cache = {}
76 def grep(pattern, items, squash=True):
77 """Greps a list for items that match a pattern
79 :param squash: If only one item matches, return just that item
80 :returns: List of matching items
82 """
83 isdict = isinstance(items, dict)
84 if pattern in __grep_cache:
85 regex = __grep_cache[pattern]
86 else:
87 regex = __grep_cache[pattern] = re.compile(pattern)
88 matched = []
89 matchdict = {}
90 for item in items:
91 match = regex.match(item)
92 if not match:
93 continue
94 groups = match.groups()
95 if not groups:
96 subitems = match.group(0)
97 else:
98 if len(groups) == 1:
99 subitems = groups[0]
100 else:
101 subitems = list(groups)
102 if isdict:
103 matchdict[item] = items[item]
104 else:
105 matched.append(subitems)
107 if isdict:
108 result = matchdict
109 elif squash and len(matched) == 1:
110 result = matched[0]
111 else:
112 result = matched
114 return result
117 def basename(path):
119 An os.path.basename() implementation that always uses '/'
121 Avoid os.path.basename because git's output always
122 uses '/' regardless of platform.
125 return path.rsplit('/', 1)[-1]
128 def strip_one(path):
129 """Strip one level of directory"""
130 return path.strip('/').split('/', 1)[-1]
133 def dirname(path, current_dir=''):
135 An os.path.dirname() implementation that always uses '/'
137 Avoid os.path.dirname because git's output always
138 uses '/' regardless of platform.
141 while '//' in path:
142 path = path.replace('//', '/')
143 path_dirname = path.rsplit('/', 1)[0]
144 if path_dirname == path:
145 return current_dir
146 return path.rsplit('/', 1)[0]
149 def splitpath(path):
150 """Split paths using '/' regardless of platform"""
151 return path.split('/')
154 def split(name):
155 """Split a path-like name. Returns tuple "(head, tail)" where "tail" is
156 everything after the final slash. The "head" may be empty.
158 This is the same as os.path.split() but only uses '/' as the delimiter.
160 >>> split('a/b/c')
161 ('a/b', 'c')
163 >>> split('xyz')
164 ('', 'xyz')
167 return (dirname(name), basename(name))
170 def join(*paths):
171 """Join paths using '/' regardless of platform
173 >>> join('a', 'b', 'c')
174 'a/b/c'
177 return '/'.join(paths)
180 def pathjoin(paths):
181 """Join a list of paths using '/' regardless of platform
183 >>> pathjoin(['a', 'b', 'c'])
184 'a/b/c'
187 return join(*paths)
190 def pathset(path):
191 """Return all of the path components for the specified path
193 >>> pathset('foo/bar/baz') == ['foo', 'foo/bar', 'foo/bar/baz']
194 True
197 result = []
198 parts = splitpath(path)
199 prefix = ''
200 for part in parts:
201 result.append(prefix + part)
202 prefix += part + '/'
204 return result
207 def select_directory(paths):
208 """Return the first directory in a list of paths"""
209 if not paths:
210 return core.getcwd()
212 for path in paths:
213 if core.isdir(path):
214 return path
216 return os.path.dirname(paths[0])
219 def strip_prefix(prefix, string):
220 """Return string, without the prefix. Blow up if string doesn't
221 start with prefix."""
222 assert string.startswith(prefix)
223 return string[len(prefix) :]
226 def sanitize(s):
227 """Removes shell metacharacters from a string."""
228 for c in """ \t!@#$%^&*()\\;,<>"'[]{}~|""":
229 s = s.replace(c, '_')
230 return s
233 def tablength(word, tabwidth):
234 """Return length of a word taking tabs into account
236 >>> tablength("\\t\\t\\t\\tX", 8)
240 return len(word.replace('\t', '')) + word.count('\t') * tabwidth
243 def _shell_split_py2(s):
244 """Python2 requires bytes inputs to shlex.split(). Returns [unicode]"""
245 try:
246 result = shlex.split(core.encode(s))
247 except ValueError:
248 result = core.encode(s).strip().split()
249 # Decode to unicode strings
250 return [core.decode(arg) for arg in result]
253 def _shell_split_py3(s):
254 """Python3 requires unicode inputs to shlex.split(). Converts to unicode"""
255 try:
256 result = shlex.split(s)
257 except ValueError:
258 result = core.decode(s).strip().split()
259 # Already unicode
260 return result
263 def shell_split(s):
264 if compat.PY2:
265 # Encode before calling split()
266 values = _shell_split_py2(s)
267 else:
268 # Python3 does not need the encode/decode dance
269 values = _shell_split_py3(s)
270 return values
273 def tmp_filename(label, suffix=''):
274 label = 'git-cola-' + label.replace('/', '-').replace('\\', '-')
275 fd = tempfile.NamedTemporaryFile(prefix=label + '-', suffix=suffix)
276 fd.close()
277 return fd.name
280 def is_linux():
281 """Is this a linux machine?"""
282 return sys.platform.startswith('linux')
285 def is_debian():
286 """Is it debian?"""
287 return os.path.exists('/usr/bin/apt-get')
290 def is_darwin():
291 """Return True on OSX."""
292 return sys.platform == 'darwin'
295 def is_win32():
296 """Return True on win32"""
297 return sys.platform == 'win32' or sys.platform == 'cygwin'
300 def expandpath(path):
301 """Expand ~user/ and environment $variables"""
302 path = os.path.expandvars(path)
303 if path.startswith('~'):
304 path = os.path.expanduser(path)
305 return path
308 class Group(object):
309 """Operate on a collection of objects as a single unit"""
311 def __init__(self, *members):
312 self._members = members
314 def __getattr__(self, name):
315 """Return a function that relays calls to the group"""
317 def relay(*args, **kwargs):
318 for member in self._members:
319 method = getattr(member, name)
320 method(*args, **kwargs)
322 setattr(self, name, relay)
323 return relay
326 class Proxy(object):
327 """Wrap an object and override attributes"""
329 def __init__(self, obj, **overrides):
330 self._obj = obj
331 for k, v in overrides.items():
332 setattr(self, k, v)
334 def __getattr__(self, name):
335 return getattr(self._obj, name)
338 def slice_fn(input_items, map_fn):
339 """Slice input_items and call map_fn over every slice
341 This exists because of "errno: Argument list too long"
344 # This comment appeared near the top of include/linux/binfmts.h
345 # in the Linux source tree:
347 # /*
348 # * MAX_ARG_PAGES defines the number of pages allocated for arguments
349 # * and envelope for the new program. 32 should suffice, this gives
350 # * a maximum env+arg of 128kB w/4KB pages!
351 # */
352 # #define MAX_ARG_PAGES 32
354 # 'size' is a heuristic to keep things highly performant by minimizing
355 # the number of slices. If we wanted it to run as few commands as
356 # possible we could call "getconf ARG_MAX" and make a better guess,
357 # but it's probably not worth the complexity (and the extra call to
358 # getconf that we can't do on Windows anyways).
360 # In my testing, getconf ARG_MAX on Mac OS X Mountain Lion reported
361 # 262144 and Debian/Linux-x86_64 reported 2097152.
363 # The hard-coded max_arg_len value is safely below both of these
364 # real-world values.
366 # 4K pages x 32 MAX_ARG_PAGES
367 max_arg_len = (32 * 4096) // 4 # allow plenty of space for the environment
368 max_filename_len = 256
369 size = max_arg_len // max_filename_len
371 status = 0
372 outs = []
373 errs = []
375 items = copy.copy(input_items)
376 while items:
377 stat, out, err = map_fn(items[:size])
378 if stat < 0:
379 status = min(stat, status)
380 else:
381 status = max(stat, status)
382 outs.append(out)
383 errs.append(err)
384 items = items[size:]
386 return (status, '\n'.join(outs), '\n'.join(errs))
389 class seq(object):
390 def __init__(self, sequence):
391 self.seq = sequence
393 def index(self, item, default=-1):
394 try:
395 idx = self.seq.index(item)
396 except ValueError:
397 idx = default
398 return idx
400 def __getitem__(self, idx):
401 return self.seq[idx]