Skip to content

Commit 03617b2

Browse files
committed
Don't crash out on OSErrors in subprocess calls
1 parent 58a16bc commit 03617b2

File tree

3 files changed

+39
-14
lines changed

3 files changed

+39
-14
lines changed

pre_commit/error_handler.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,15 @@
88
import pre_commit.constants as C
99
from pre_commit import output
1010
from pre_commit.store import Store
11+
from pre_commit.util import force_bytes
1112

1213

1314
class FatalError(RuntimeError):
1415
pass
1516

1617

17-
def _exception_to_bytes(exc: BaseException) -> bytes:
18-
with contextlib.suppress(TypeError):
19-
return bytes(exc) # type: ignore
20-
with contextlib.suppress(Exception):
21-
return str(exc).encode()
22-
return f'<unprintable {type(exc).__name__} object>'.encode()
23-
24-
2518
def _log_and_exit(msg: str, exc: BaseException, formatted: str) -> None:
26-
error_msg = f'{msg}: {type(exc).__name__}: '.encode()
27-
error_msg += _exception_to_bytes(exc)
19+
error_msg = f'{msg}: {type(exc).__name__}: '.encode() + force_bytes(exc)
2820
output.write_line_b(error_msg)
2921
log_path = os.path.join(Store().directory, 'pre-commit.log')
3022
output.write_line(f'Check the log at {log_path}')

pre_commit/util.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ def yaml_dump(o: Any) -> str:
4343
)
4444

4545

46+
def force_bytes(exc: Any) -> bytes:
47+
with contextlib.suppress(TypeError):
48+
return bytes(exc)
49+
with contextlib.suppress(Exception):
50+
return str(exc).encode()
51+
return f'<unprintable {type(exc).__name__} object>'.encode()
52+
53+
4654
@contextlib.contextmanager
4755
def clean_path_on_failure(path: str) -> Generator[None, None, None]:
4856
"""Cleans up the directory on an exceptional failure."""
@@ -120,6 +128,10 @@ def _setdefault_kwargs(kwargs: Dict[str, Any]) -> None:
120128
kwargs.setdefault(arg, subprocess.PIPE)
121129

122130

131+
def _oserror_to_output(e: OSError) -> Tuple[int, bytes, None]:
132+
return 1, force_bytes(e).rstrip(b'\n') + b'\n', None
133+
134+
123135
def cmd_output_b(
124136
*cmd: str,
125137
retcode: Optional[int] = 0,
@@ -132,9 +144,13 @@ def cmd_output_b(
132144
except parse_shebang.ExecutableNotFoundError as e:
133145
returncode, stdout_b, stderr_b = e.to_output()
134146
else:
135-
proc = subprocess.Popen(cmd, **kwargs)
136-
stdout_b, stderr_b = proc.communicate()
137-
returncode = proc.returncode
147+
try:
148+
proc = subprocess.Popen(cmd, **kwargs)
149+
except OSError as e:
150+
returncode, stdout_b, stderr_b = _oserror_to_output(e)
151+
else:
152+
stdout_b, stderr_b = proc.communicate()
153+
returncode = proc.returncode
138154

139155
if retcode is not None and retcode != returncode:
140156
raise CalledProcessError(returncode, cmd, retcode, stdout_b, stderr_b)
@@ -205,7 +221,11 @@ def cmd_output_p(
205221
with open(os.devnull) as devnull, Pty() as pty:
206222
assert pty.r is not None
207223
kwargs.update({'stdin': devnull, 'stdout': pty.w, 'stderr': pty.w})
208-
proc = subprocess.Popen(cmd, **kwargs)
224+
try:
225+
proc = subprocess.Popen(cmd, **kwargs)
226+
except OSError as e:
227+
return _oserror_to_output(e)
228+
209229
pty.close_w()
210230

211231
buf = b''

tests/util_test.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pre_commit.util import cmd_output
1010
from pre_commit.util import cmd_output_b
1111
from pre_commit.util import cmd_output_p
12+
from pre_commit.util import make_executable
1213
from pre_commit.util import parse_version
1314
from pre_commit.util import rmtree
1415
from pre_commit.util import tmpdir
@@ -92,6 +93,18 @@ def test_cmd_output_exe_not_found_bytes(fn):
9293
assert out == b'Executable `dne` not found'
9394

9495

96+
@pytest.mark.parametrize('fn', (cmd_output_b, cmd_output_p))
97+
def test_cmd_output_no_shebang(tmpdir, fn):
98+
f = tmpdir.join('f').ensure()
99+
make_executable(f)
100+
101+
# previously this raised `OSError` -- the output is platform specific
102+
ret, out, _ = fn(str(f), retcode=None, stderr=subprocess.STDOUT)
103+
assert ret == 1
104+
assert isinstance(out, bytes)
105+
assert out.endswith(b'\n')
106+
107+
95108
def test_parse_version():
96109
assert parse_version('0.0') == parse_version('0.0')
97110
assert parse_version('0.1') > parse_version('0.0')

0 commit comments

Comments
 (0)