git-cola v1.9.1
[git-cola.git] / cola / core.py
blob0d5b717cc63fea012c1f54caffc167bb03e36b87
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 import os
8 import sys
9 import itertools
10 import subprocess
12 from cola.decorators import interruptable
14 # Some files are not in UTF-8; some other aren't in any codification.
15 # Remember that GIT doesn't care about encodings (saves binary data)
16 _encoding_tests = [
17 'utf-8',
18 'iso-8859-15',
19 'windows1252',
20 'ascii',
21 # <-- add encodings here
24 def decode(enc, encoding=None):
25 """decode(encoded_string) returns an unencoded unicode string
26 """
27 if type(enc) is unicode:
28 return enc
30 if encoding is None:
31 encoding_tests = _encoding_tests
32 else:
33 encoding_tests = itertools.chain([encoding], _encoding_tests)
35 for encoding in encoding_tests:
36 try:
37 return enc.decode(encoding)
38 except:
39 pass
40 # this shouldn't ever happen... FIXME
41 return unicode(enc)
44 def encode(string, encoding=None):
45 """encode(unencoded_string) returns a string encoded in utf-8
46 """
47 if type(string) is not unicode:
48 return string
49 return string.encode(encoding or 'utf-8', 'replace')
52 def read(filename, size=-1, encoding=None):
53 """Read filename and return contents"""
54 with xopen(filename, 'r') as fh:
55 return fread(fh, size=size, encoding=encoding)
58 def write(path, contents, encoding=None):
59 """Writes a unicode string to a file"""
60 with xopen(path, 'wb') as fh:
61 return fwrite(fh, contents, encoding=encoding)
64 @interruptable
65 def fread(fh, size=-1, encoding=None):
66 """Read from a filehandle and retry when interrupted"""
67 return decode(fh.read(size), encoding=encoding)
70 @interruptable
71 def fwrite(fh, content, encoding=None):
72 """Write to a filehandle and retry when interrupted"""
73 return fh.write(encode(content, encoding=encoding))
76 @interruptable
77 def wait(proc):
78 """Wait on a subprocess and retry when interrupted"""
79 return proc.wait()
82 @interruptable
83 def readline(fh, encoding=None):
84 return decode(fh.readline(), encoding=encoding)
87 @interruptable
88 def start_command(cmd, cwd=None, shell=False, add_env=None,
89 universal_newlines=False,
90 stdin=subprocess.PIPE,
91 stdout=subprocess.PIPE,
92 stderr=subprocess.PIPE):
93 """Start the given command, and return a subprocess object.
95 This provides a simpler interface to the subprocess module.
97 """
98 env = None
99 if add_env is not None:
100 env = os.environ.copy()
101 env.update(add_env)
102 return subprocess.Popen(cmd, bufsize=1, stdin=stdin, stdout=stdout,
103 stderr=stderr, cwd=cwd, shell=shell, env=env,
104 universal_newlines=universal_newlines)
107 def run_command(cmd, *args, **kwargs):
108 """Run the given command to completion, and return its results.
110 This provides a simpler interface to the subprocess module.
111 The results are formatted as a 3-tuple: (exit_code, output, errors)
112 The other arguments are passed on to start_command().
115 process = start_command(cmd, *args, **kwargs)
116 (output, errors) = process.communicate()
117 output = decode(output)
118 errors = decode(errors)
119 exit_code = process.returncode
120 return (exit_code, output, errors)
123 @interruptable
124 def _fork_posix(args):
125 """Launch a process in the background."""
126 encoded_args = [encode(arg) for arg in args]
127 return subprocess.Popen(encoded_args).pid
130 def _fork_win32(args):
131 """Launch a background process using crazy win32 voodoo."""
132 # This is probably wrong, but it works. Windows.. wow.
133 if args[0] == 'git-dag':
134 # win32 can't exec python scripts
135 args = [sys.executable] + args
137 enc_args = [encode(arg) for arg in args]
138 abspath = _win32_abspath(enc_args[0])
139 if abspath:
140 # e.g. fork(['git', 'difftool', '--no-prompt', '--', 'path'])
141 enc_args[0] = abspath
142 else:
143 # e.g. fork(['gitk', '--all'])
144 cmdstr = subprocess.list2cmdline(enc_args)
145 sh_exe = _win32_abspath('sh')
146 enc_args = [sh_exe, '-c', cmdstr]
148 DETACHED_PROCESS = 0x00000008 # Amazing!
149 return subprocess.Popen(enc_args, creationflags=DETACHED_PROCESS).pid
152 def _win32_abspath(exe):
153 """Return the absolute path to an .exe if it exists"""
154 if exists(exe):
155 return exe
156 if not exe.endswith('.exe'):
157 exe += '.exe'
158 if exists(exe):
159 return exe
160 for path in getenv('PATH', '').split(os.pathsep):
161 abspath = os.path.join(path, exe)
162 if exists(abspath):
163 return abspath
164 return None
167 # Portability wrappers
168 if sys.platform == 'win32' or sys.platform == 'cygwin':
169 fork = _fork_win32
170 else:
171 fork = _fork_posix
174 def wrap(action, fn, decorator=None):
175 """Wrap arguments with `action`, optionally decorate the result"""
176 if decorator is None:
177 decorator = lambda x: x
178 def wrapped(*args, **kwargs):
179 return decorator(fn(action(*args, **kwargs)))
180 return wrapped
183 def decorate(decorator, fn):
184 """Decorate the result of `fn` with `action`"""
185 def decorated(*args, **kwargs):
186 return decorator(fn(*args, **kwargs))
187 return decorated
190 def getenv(name, default=None):
191 return decode(os.getenv(encode(name), default))
194 def xopen(path, mode='r', encoding=None):
195 return open(encode(path, encoding=encoding), mode)
198 def stdout(msg):
199 sys.stdout.write(encode(msg) + '\n')
202 def stderr(msg):
203 sys.stderr.write(encode(msg) + '\n')
206 abspath = wrap(encode, os.path.abspath, decorator=decode)
207 exists = wrap(encode, os.path.exists)
208 expanduser = wrap(encode, os.path.expanduser, decorator=decode)
209 getcwd = decorate(decode, os.getcwd)
210 isdir = wrap(encode, os.path.isdir)
211 isfile = wrap(encode, os.path.isfile)
212 islink = wrap(encode, os.path.islink)
213 makedirs = wrap(encode, os.makedirs)
214 try:
215 readlink = wrap(encode, os.readlink, decorator=decode)
216 except AttributeError:
217 readlink = lambda p: p
218 realpath = wrap(encode, os.path.realpath, decorator=decode)
219 stat = wrap(encode, os.stat)
220 unlink = wrap(encode, os.unlink)