cmds: provide $DIRNAME in the environment for guitool commands
[git-cola.git] / cola / core.py
blob532cf324f80034300fdd0ffe685c1e7a6ee926ae
1 """This module provides core functions for handling unicode and UNIX quirks
3 The @interruptable functions retry when system calls are interrupted,
4 e.g. when python raises an IOError or OSError with errno == EINTR.
6 """
7 from __future__ import division, absolute_import, unicode_literals
8 import os
9 import functools
10 import sys
11 import itertools
12 import platform
13 import subprocess
15 from .decorators import interruptable
16 from .compat import ustr
17 from .compat import PY2
18 from .compat import PY3
19 from .compat import WIN32
21 # /usr/include/stdlib.h
22 #define EXIT_SUCCESS 0 /* Successful exit status. */
23 #define EXIT_FAILURE 1 /* Failing exit status. */
24 EXIT_SUCCESS = 0
25 EXIT_FAILURE = 1
27 # /usr/include/sysexits.h
28 # #define EX_USAGE 64 /* command line usage error */
29 # #define EX_NOINPUT 66 /* cannot open input */
30 # #define EX_UNAVAILABLE 69 /* service unavailable */
31 EXIT_USAGE = 64
32 EXIT_NOINPUT = 66
33 EXIT_UNAVAILABLE = 69
35 # Some files are not in UTF-8; some other aren't in any codification.
36 # Remember that GIT doesn't care about encodings (saves binary data)
37 _encoding_tests = [
38 'utf-8',
39 'iso-8859-15',
40 'windows1252',
41 'ascii',
42 # <-- add encodings here
46 def decode(enc, encoding=None, errors='strict'):
47 """decode(encoded_string) returns an unencoded unicode string
48 """
49 if enc is None or type(enc) is ustr:
50 return enc
52 if encoding is None:
53 encoding_tests = _encoding_tests
54 else:
55 encoding_tests = itertools.chain([encoding], _encoding_tests)
57 for encoding in encoding_tests:
58 try:
59 return enc.decode(encoding, errors)
60 except:
61 pass
62 return enc.decode('utf-8', errors='ignore')
65 def encode(string, encoding=None):
66 """encode(unencoded_string) returns a string encoded in utf-8
67 """
68 if type(string) is not ustr:
69 return string
70 return string.encode(encoding or 'utf-8', 'replace')
73 def mkpath(path, encoding=None):
74 # The Windows API requires unicode strings regardless of python version
75 if WIN32:
76 return decode(path, encoding=encoding)
77 # UNIX prefers bytes
78 return encode(path, encoding=encoding)
81 def list2cmdline(cmd):
82 return subprocess.list2cmdline([decode(c) for c in cmd])
85 def read(filename, size=-1, encoding=None, errors='strict'):
86 """Read filename and return contents"""
87 with xopen(filename, 'rb') as fh:
88 return xread(fh, size=size, encoding=encoding, errors=errors)
91 def write(path, contents, encoding=None):
92 """Writes a unicode string to a file"""
93 with xopen(path, 'wb') as fh:
94 return xwrite(fh, contents, encoding=encoding)
97 @interruptable
98 def xread(fh, size=-1, encoding=None, errors='strict'):
99 """Read from a filehandle and retry when interrupted"""
100 return decode(fh.read(size), encoding=encoding, errors=errors)
103 @interruptable
104 def xwrite(fh, content, encoding=None):
105 """Write to a filehandle and retry when interrupted"""
106 return fh.write(encode(content, encoding=encoding))
109 @interruptable
110 def wait(proc):
111 """Wait on a subprocess and retry when interrupted"""
112 return proc.wait()
115 @interruptable
116 def readline(fh, encoding=None):
117 return decode(fh.readline(), encoding=encoding)
120 @interruptable
121 def start_command(cmd, cwd=None, add_env=None,
122 universal_newlines=False,
123 stdin=subprocess.PIPE,
124 stdout=subprocess.PIPE,
125 no_win32_startupinfo=False,
126 stderr=subprocess.PIPE,
127 **extra):
128 """Start the given command, and return a subprocess object.
130 This provides a simpler interface to the subprocess module.
133 env = extra.pop('env', None)
134 if add_env is not None:
135 env = os.environ.copy()
136 env.update(add_env)
138 # Python3 on windows always goes through list2cmdline() internally inside
139 # of subprocess.py so we must provide unicode strings here otherwise
140 # Python3 breaks when bytes are provided.
142 # Additionally, the preferred usage on Python3 is to pass unicode
143 # strings to subprocess. Python will automatically encode into the
144 # default encoding (utf-8) when it gets unicode strings.
145 shell = extra.get('shell', False)
146 cmd = prep_for_subprocess(cmd, shell=shell)
148 if WIN32 and cwd == getcwd():
149 # Windows cannot deal with passing a cwd that contains unicode
150 # but we luckily can pass None when the supplied cwd is the same
151 # as our current directory and get the same effect.
152 # Not doing this causes unicode encoding errors when launching
153 # the subprocess.
154 cwd = None
156 if PY2 and cwd:
157 cwd = encode(cwd)
159 if WIN32:
160 # If git-cola is invoked on Windows using "start pythonw git-cola",
161 # a console window will briefly flash on the screen each time
162 # git-cola invokes git, which is very annoying. The code below
163 # prevents this by ensuring that any window will be hidden.
164 startupinfo = subprocess.STARTUPINFO()
165 startupinfo.dwFlags = subprocess.STARTF_USESHOWWINDOW
166 startupinfo.wShowWindow = subprocess.SW_HIDE
167 extra['startupinfo'] = startupinfo
169 if WIN32 and not no_win32_startupinfo:
170 CREATE_NO_WINDOW = 0x08000000
171 extra['creationflags'] = CREATE_NO_WINDOW
173 return subprocess.Popen(cmd, bufsize=1, stdin=stdin, stdout=stdout,
174 stderr=stderr, cwd=cwd, env=env,
175 universal_newlines=universal_newlines, **extra)
178 def prep_for_subprocess(cmd, shell=False):
179 """Decode on Python3, encode on Python2"""
180 # See the comment in start_command()
181 if shell:
182 if PY3:
183 cmd = decode(cmd)
184 else:
185 cmd = encode(cmd)
186 else:
187 if PY3:
188 cmd = [decode(c) for c in cmd]
189 else:
190 cmd = [encode(c) for c in cmd]
191 return cmd
194 @interruptable
195 def communicate(proc):
196 return proc.communicate()
199 def run_command(cmd, encoding=None, *args, **kwargs):
200 """Run the given command to completion, and return its results.
202 This provides a simpler interface to the subprocess module.
203 The results are formatted as a 3-tuple: (exit_code, output, errors)
204 The other arguments are passed on to start_command().
207 process = start_command(cmd, *args, **kwargs)
208 (output, errors) = communicate(process)
209 output = decode(output, encoding=encoding)
210 errors = decode(errors, encoding=encoding)
211 exit_code = process.returncode
212 return (exit_code, output or '', errors or '')
215 @interruptable
216 def _fork_posix(args, cwd=None):
217 """Launch a process in the background."""
218 encoded_args = [encode(arg) for arg in args]
219 return subprocess.Popen(encoded_args, cwd=cwd).pid
222 def _fork_win32(args, cwd=None):
223 """Launch a background process using crazy win32 voodoo."""
224 # This is probably wrong, but it works. Windows.. wow.
225 if args[0] == 'git-dag':
226 # win32 can't exec python scripts
227 args = [sys.executable] + args
228 args[0] = _win32_find_exe(args[0])
230 if PY3:
231 # see comment in start_command()
232 argv = [decode(arg) for arg in args]
233 else:
234 argv = [encode(arg) for arg in args]
236 DETACHED_PROCESS = 0x00000008 # Amazing!
237 return subprocess.Popen(argv, cwd=cwd, creationflags=DETACHED_PROCESS).pid
240 def _win32_find_exe(exe):
241 """Find the actual file for a Windows executable.
243 This function goes through the same process that the Windows shell uses to
244 locate an executable, taking into account the PATH and PATHEXT environment
245 variables. This allows us to avoid passing shell=True to subprocess.Popen.
247 For reference, see:
248 http://technet.microsoft.com/en-us/library/cc723564.aspx#XSLTsection127121120120
251 # try the argument itself
252 candidates = [exe]
253 # if argument does not have an extension, also try it with each of the
254 # extensions specified in PATHEXT
255 if '.' not in exe:
256 extensions = getenv('PATHEXT', '').split(os.pathsep)
257 candidates.extend([(exe + ext) for ext in extensions
258 if ext.startswith('.')])
259 # search the current directory first
260 for candidate in candidates:
261 if exists(candidate):
262 return candidate
263 # if the argument does not include a path separator, search each of the
264 # directories on the PATH
265 if not os.path.dirname(exe):
266 for path in getenv('PATH').split(os.pathsep):
267 if path:
268 for candidate in candidates:
269 full_path = os.path.join(path, candidate)
270 if exists(full_path):
271 return full_path
272 # not found, punt and return the argument unchanged
273 return exe
276 # Portability wrappers
277 if sys.platform == 'win32' or sys.platform == 'cygwin':
278 fork = _fork_win32
279 else:
280 fork = _fork_posix
283 def _decorator_noop(x):
284 return x
287 def wrap(action, fn, decorator=None):
288 """Wrap arguments with `action`, optionally decorate the result"""
289 if decorator is None:
290 decorator = _decorator_noop
292 @functools.wraps(fn)
293 def wrapped(*args, **kwargs):
294 return decorator(fn(action(*args, **kwargs)))
296 return wrapped
299 def decorate(decorator, fn):
300 """Decorate the result of `fn` with `action`"""
301 @functools.wraps(fn)
302 def decorated(*args, **kwargs):
303 return decorator(fn(*args, **kwargs))
304 return decorated
307 def getenv(name, default=None):
308 return decode(os.getenv(name, default))
311 def xopen(path, mode='r', encoding=None):
312 return open(mkpath(path, encoding=encoding), mode)
315 def stdout(msg, linesep='\n'):
316 msg = msg + linesep
317 if PY2:
318 msg = encode(msg, encoding='utf-8')
319 sys.stdout.write(msg)
322 def stderr(msg, linesep='\n'):
323 msg = msg + linesep
324 if PY2:
325 msg = encode(msg, encoding='utf-8')
326 sys.stderr.write(msg)
329 def error(msg, status=EXIT_FAILURE, linesep='\n'):
330 stderr(msg, linesep=linesep)
331 sys.exit(status)
334 @interruptable
335 def node():
336 return platform.node()
339 abspath = wrap(mkpath, os.path.abspath, decorator=decode)
340 chdir = wrap(mkpath, os.chdir)
341 exists = wrap(mkpath, os.path.exists)
342 expanduser = wrap(encode, os.path.expanduser, decorator=decode)
343 if PY2:
344 getcwd = decorate(decode, os.getcwd)
345 else:
346 getcwd = os.getcwd
347 isdir = wrap(mkpath, os.path.isdir)
348 isfile = wrap(mkpath, os.path.isfile)
349 islink = wrap(mkpath, os.path.islink)
350 makedirs = wrap(mkpath, os.makedirs)
351 try:
352 readlink = wrap(mkpath, os.readlink, decorator=decode)
353 except AttributeError:
355 def _readlink_noop(p):
356 return p
358 readlink = _readlink_noop
360 realpath = wrap(mkpath, os.path.realpath, decorator=decode)
361 relpath = wrap(mkpath, os.path.relpath, decorator=decode)
362 stat = wrap(mkpath, os.stat)
363 unlink = wrap(mkpath, os.unlink)
364 walk = wrap(mkpath, os.walk)