Skip to content

Commit 44c6d0b

Browse files
committed
Proc, #519: Rework error-exc msgs & log thread-pumps errors
+ No WindowsError exception. + Add `test_exc.py` for unicode issues. + Single-arg for decoding-streams in pump-func.
1 parent f11fdf1 commit 44c6d0b

File tree

9 files changed

+240
-57
lines changed

9 files changed

+240
-57
lines changed

‎git/cmd.py

+40-24
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
)
4646
import io
4747
from _io import UnsupportedOperation
48+
from git.exc import CommandError
4849

4950
execute_kwargs = set(('istream', 'with_keep_cwd', 'with_extended_output',
5051
'with_exceptions', 'as_process', 'stdout_as_string',
@@ -56,9 +57,6 @@
5657

5758
__all__ = ('Git',)
5859

59-
if is_win:
60-
WindowsError = OSError # @ReservedAssignment
61-
6260
if PY3:
6361
_bchr = bchr
6462
else:
@@ -73,17 +71,23 @@ def _bchr(c):
7371
# Documentation
7472
## @{
7573

76-
def handle_process_output(process, stdout_handler, stderr_handler, finalizer,
77-
decode_stdout=True, decode_stderr=True):
74+
def handle_process_output(process, stdout_handler, stderr_handler, finalizer, decode_streams=True):
7875
"""Registers for notifications to lean that process output is ready to read, and dispatches lines to
7976
the respective line handlers. We are able to handle carriage returns in case progress is sent by that
8077
mean. For performance reasons, we only apply this to stderr.
8178
This function returns once the finalizer returns
79+
8280
:return: result of finalizer
8381
:param process: subprocess.Popen instance
8482
:param stdout_handler: f(stdout_line_string), or None
8583
:param stderr_hanlder: f(stderr_line_string), or None
86-
:param finalizer: f(proc) - wait for proc to finish"""
84+
:param finalizer: f(proc) - wait for proc to finish
85+
:param decode_streams:
86+
Assume stdout/stderr streams are binary and decode them vefore pushing \
87+
their contents to handlers.
88+
Set it to False if `universal_newline == True` (then streams are in text-mode)
89+
or if decoding must happen later (i.e. for Diffs).
90+
"""
8791

8892
def _parse_lines_from_buffer(buf):
8993
line = b''
@@ -156,18 +160,29 @@ def _deplete_buffer(fno, handler, buf_list, decode):
156160
# Oh ... probably we are on windows. or TC mockap provided for streams.
157161
# Anyhow, select.select() can only handle sockets, we have files
158162
# The only reliable way to do this now is to use threads and wait for both to finish
159-
def _handle_lines(fd, handler, decode):
160-
for line in fd:
161-
if handler:
162-
if decode:
163-
line = line.decode(defenc)
164-
handler(line)
165-
163+
def pump_stream(cmdline, name, stream, is_decode, handler):
164+
try:
165+
for line in stream:
166+
if handler:
167+
if is_decode:
168+
line = line.decode(defenc)
169+
handler(line)
170+
except Exception as ex:
171+
log.error("Pumping %r of cmd(%s) failed due to: %r", name, cmdline, ex)
172+
raise CommandError(['<%s-pump>' % name] + cmdline, ex)
173+
finally:
174+
stream.close()
175+
176+
cmdline = getattr(process, 'args', '') # PY3+ only
177+
if not isinstance(cmdline, (tuple, list)):
178+
cmdline = cmdline.split()
166179
threads = []
167-
for fd, handler, decode in zip((process.stdout, process.stderr),
168-
(stdout_handler, stderr_handler),
169-
(decode_stdout, decode_stderr),):
170-
t = threading.Thread(target=_handle_lines, args=(fd, handler, decode))
180+
for name, stream, handler in (
181+
('stdout', process.stdout, stdout_handler),
182+
('stderr', process.stderr, stderr_handler),
183+
):
184+
t = threading.Thread(target=pump_stream,
185+
args=(cmdline, name, stream, decode_streams, handler))
171186
t.setDaemon(True)
172187
t.start()
173188
threads.append(t)
@@ -177,8 +192,8 @@ def _handle_lines(fd, handler, decode):
177192
else:
178193
# poll is preferred, as select is limited to file handles up to 1024 ... . This could otherwise be
179194
# an issue for us, as it matters how many handles our own process has
180-
fdmap = {outfn: (stdout_handler, [b''], decode_stdout),
181-
errfn: (stderr_handler, [b''], decode_stderr)}
195+
fdmap = {outfn: (stdout_handler, [b''], decode_streams),
196+
errfn: (stderr_handler, [b''], decode_streams)}
182197

183198
READ_ONLY = select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR # @UndefinedVariable
184199
CLOSED = select.POLLHUP | select.POLLERR # @UndefinedVariable
@@ -334,7 +349,8 @@ def __del__(self):
334349
try:
335350
proc.terminate()
336351
proc.wait() # ensure process goes away
337-
except (OSError, WindowsError):
352+
except OSError as ex:
353+
log.info("Ignored error after process has dies: %r", ex)
338354
pass # ignore error when process already died
339355
except AttributeError:
340356
# try windows
@@ -638,12 +654,12 @@ def execute(self, command,
638654
env.update(self._environment)
639655

640656
if is_win:
641-
cmd_not_found_exception = WindowsError
657+
cmd_not_found_exception = OSError
642658
if kill_after_timeout:
643-
raise GitCommandError('"kill_after_timeout" feature is not supported on Windows.')
659+
raise GitCommandError(command, '"kill_after_timeout" feature is not supported on Windows.')
644660
else:
645661
if sys.version_info[0] > 2:
646-
cmd_not_found_exception = FileNotFoundError # NOQA # this is defined, but flake8 doesn't know
662+
cmd_not_found_exception = FileNotFoundError # NOQA # exists, flake8 unknown @UndefinedVariable
647663
else:
648664
cmd_not_found_exception = OSError
649665
# end handle
@@ -663,7 +679,7 @@ def execute(self, command,
663679
**subprocess_kwargs
664680
)
665681
except cmd_not_found_exception as err:
666-
raise GitCommandNotFound('%s: %s' % (command[0], err))
682+
raise GitCommandNotFound(command, err)
667683

668684
if as_process:
669685
return self.AutoInterrupt(proc, command)

‎git/diff.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ def _index_from_patch_format(cls, repo, proc):
407407

408408
## FIXME: Here SLURPING raw, need to re-phrase header-regexes linewise.
409409
text = []
410-
handle_process_output(proc, text.append, None, finalize_process, decode_stdout=False)
410+
handle_process_output(proc, text.append, None, finalize_process, decode_streams=False)
411411

412412
# for now, we have to bake the stream
413413
text = b''.join(text)
@@ -499,6 +499,6 @@ def handle_diff_line(line):
499499
new_file, deleted_file, rename_from, rename_to, '', change_type)
500500
index.append(diff)
501501

502-
handle_process_output(proc, handle_diff_line, None, finalize_process, decode_stdout=False)
502+
handle_process_output(proc, handle_diff_line, None, finalize_process, decode_streams=False)
503503

504504
return index

‎git/exc.py

+48-24
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
""" Module containing all exceptions thrown througout the git package, """
77

88
from gitdb.exc import * # NOQA
9-
from git.compat import UnicodeMixin, safe_decode
9+
from git.compat import UnicodeMixin, safe_decode, string_types
1010

1111

1212
class InvalidGitRepositoryError(Exception):
@@ -21,25 +21,56 @@ class NoSuchPathError(OSError):
2121
""" Thrown if a path could not be access by the system. """
2222

2323

24-
class GitCommandNotFound(Exception):
24+
class CommandError(UnicodeMixin, Exception):
25+
"""Base class for exceptions thrown at every stage of `Popen()` execution.
26+
27+
:param command:
28+
A non-empty list of argv comprising the command-line.
29+
"""
30+
31+
#: A unicode print-format with 2 `%s for `<cmdline>` and the rest,
32+
#: e.g.
33+
#: u"'%s' failed%s"
34+
_msg = u"Cmd('%s') failed%s"
35+
36+
def __init__(self, command, status=None, stderr=None, stdout=None):
37+
assert isinstance(command, (tuple, list)), command
38+
self.command = command
39+
self.status = status
40+
if status:
41+
if isinstance(status, Exception):
42+
status = u"%s('%s')" % (type(status).__name__, safe_decode(str(status)))
43+
else:
44+
try:
45+
status = u'exit code(%s)' % int(status)
46+
except:
47+
s = safe_decode(str(status))
48+
status = u"'%s'" % s if isinstance(status, string_types) else s
49+
50+
self._cmd = safe_decode(command[0])
51+
self._cmdline = u' '.join(safe_decode(i) for i in command)
52+
self._cause = status and u" due to: %s" % status or "!"
53+
self.stdout = stdout and u"\n stdout: '%s'" % safe_decode(stdout) or ''
54+
self.stderr = stderr and u"\n stderr: '%s'" % safe_decode(stderr) or ''
55+
56+
def __unicode__(self):
57+
return (self._msg + "\n cmdline: %s%s%s") % (
58+
self._cmd, self._cause, self._cmdline, self.stdout, self.stderr)
59+
60+
61+
class GitCommandNotFound(CommandError):
2562
"""Thrown if we cannot find the `git` executable in the PATH or at the path given by
2663
the GIT_PYTHON_GIT_EXECUTABLE environment variable"""
27-
pass
64+
def __init__(self, command, cause):
65+
super(GitCommandNotFound, self).__init__(command, cause)
66+
self._msg = u"Cmd('%s') not found%s"
2867

2968

30-
class GitCommandError(UnicodeMixin, Exception):
69+
class GitCommandError(CommandError):
3170
""" Thrown if execution of the git command fails with non-zero status code. """
3271

3372
def __init__(self, command, status, stderr=None, stdout=None):
34-
self.stderr = stderr
35-
self.stdout = stdout
36-
self.status = status
37-
self.command = command
38-
39-
def __unicode__(self):
40-
cmdline = u' '.join(safe_decode(i) for i in self.command)
41-
return (u"'%s' returned with exit code %s\n stdout: '%s'\n stderr: '%s'"
42-
% (cmdline, self.status, safe_decode(self.stdout), safe_decode(self.stderr)))
73+
super(GitCommandError, self).__init__(command, status, stderr, stdout)
4374

4475

4576
class CheckoutError(Exception):
@@ -76,20 +107,13 @@ class UnmergedEntriesError(CacheError):
76107
entries in the cache"""
77108

78109

79-
class HookExecutionError(UnicodeMixin, Exception):
110+
class HookExecutionError(CommandError):
80111
"""Thrown if a hook exits with a non-zero exit code. It provides access to the exit code and the string returned
81112
via standard output"""
82113

83-
def __init__(self, command, status, stdout=None, stderr=None):
84-
self.command = command
85-
self.status = status
86-
self.stdout = stdout
87-
self.stderr = stderr
88-
89-
def __unicode__(self):
90-
cmdline = u' '.join(safe_decode(i) for i in self.command)
91-
return (u"'%s' hook failed with %r\n stdout: '%s'\n stderr: '%s'"
92-
% (cmdline, self.status, safe_decode(self.stdout), safe_decode(self.stderr)))
114+
def __init__(self, command, status, stderr=None, stdout=None):
115+
super(HookExecutionError, self).__init__(command, status, stderr, stdout)
116+
self._msg = u"Hook('%s') failed%s"
93117

94118

95119
class RepositoryDirtyError(Exception):

‎git/index/base.py

+1
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,7 @@ def handle_stderr(proc, iter_checked_out_files):
10911091
kwargs['as_process'] = True
10921092
kwargs['istream'] = subprocess.PIPE
10931093
proc = self.repo.git.checkout_index(args, **kwargs)
1094+
# FIXME: Reading from GIL!
10941095
make_exc = lambda: GitCommandError(("git-checkout-index",) + tuple(args), 128, proc.stderr.read())
10951096
checked_out_files = list()
10961097

‎git/remote.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -681,8 +681,7 @@ def stdout_handler(line):
681681
# END for each line
682682

683683
try:
684-
handle_process_output(proc, stdout_handler, progress_handler, finalize_process,
685-
decode_stdout=False, decode_stderr=False)
684+
handle_process_output(proc, stdout_handler, progress_handler, finalize_process, decode_streams=False)
686685
except Exception:
687686
if len(output) == 0:
688687
raise

0 commit comments

Comments
 (0)