Skip to content

Commit 9a017dc

Browse files
committed
Add error_handler and use it.
1 parent e3d29a8 commit 9a017dc

File tree

8 files changed

+177
-35
lines changed

8 files changed

+177
-35
lines changed

pre_commit/error_handler.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from __future__ import absolute_import
2+
from __future__ import print_function
3+
from __future__ import unicode_literals
4+
5+
import contextlib
6+
import io
7+
import os.path
8+
import traceback
9+
10+
from pre_commit.errors import FatalError
11+
from pre_commit.store import Store
12+
13+
14+
# For testing purposes
15+
class PreCommitSystemExit(SystemExit):
16+
pass
17+
18+
19+
def _log_and_exit(msg, exc, formatted, print_fn=print):
20+
error_msg = '{0}: {1}: {2}'.format(msg, type(exc).__name__, exc)
21+
print_fn(error_msg)
22+
print_fn('Check the log at ~/.pre-commit/pre-commit.log')
23+
store = Store()
24+
store.require_created()
25+
with io.open(os.path.join(store.directory, 'pre-commit.log'), 'w') as log:
26+
log.write(error_msg + '\n')
27+
log.write(formatted + '\n')
28+
raise PreCommitSystemExit(1)
29+
30+
31+
@contextlib.contextmanager
32+
def error_handler():
33+
try:
34+
yield
35+
except FatalError as e:
36+
_log_and_exit('An error has occurred', e, traceback.format_exc())
37+
except Exception as e:
38+
_log_and_exit(
39+
'An unexpected error has occurred',
40+
e,
41+
traceback.format_exc(),
42+
)

pre_commit/errors.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from __future__ import absolute_import
2+
from __future__ import unicode_literals
3+
4+
5+
class FatalError(RuntimeError):
6+
pass

pre_commit/five.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import unicode_literals
22

3-
"""five: six, redux"""
43
# pylint:disable=invalid-name
54
PY2 = str is bytes
65
PY3 = str is not bytes

pre_commit/jsonschema_extensions.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,20 @@ def new_properties(validator, properties, instance, schema):
2020

2121

2222
def default_values(properties, instance):
23-
for property, subschema in properties.items():
23+
for prop, subschema in properties.items():
2424
if 'default' in subschema:
2525
instance.setdefault(
26-
property, copy.deepcopy(subschema['default']),
26+
prop, copy.deepcopy(subschema['default']),
2727
)
2828

2929

3030
def remove_default_values(properties, instance):
31-
for property, subschema in properties.items():
31+
for prop, subschema in properties.items():
3232
if (
33-
'default' in subschema and
34-
instance.get(property) == subschema['default']
33+
'default' in subschema and
34+
instance.get(prop) == subschema['default']
3535
):
36-
del instance[property]
36+
del instance[prop]
3737

3838

3939
_AddDefaultsValidator = extend_validator_cls(

pre_commit/main.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pre_commit.commands.install_uninstall import install
1010
from pre_commit.commands.install_uninstall import uninstall
1111
from pre_commit.commands.run import run
12+
from pre_commit.error_handler import error_handler
1213
from pre_commit.runner import Runner
1314
from pre_commit.util import entry
1415

@@ -83,29 +84,30 @@ def main(argv):
8384
else:
8485
parser.parse_args(['--help'])
8586

86-
runner = Runner.create()
87+
with error_handler():
88+
runner = Runner.create()
89+
90+
if args.command == 'install':
91+
return install(
92+
runner, overwrite=args.overwrite, hooks=args.install_hooks,
93+
)
94+
elif args.command == 'uninstall':
95+
return uninstall(runner)
96+
elif args.command == 'clean':
97+
return clean(runner)
98+
elif args.command == 'autoupdate':
99+
return autoupdate(runner)
100+
elif args.command == 'run':
101+
return run(runner, args)
102+
else:
103+
raise NotImplementedError(
104+
'Command {0} not implemented.'.format(args.command)
105+
)
87106

88-
if args.command == 'install':
89-
return install(
90-
runner, overwrite=args.overwrite, hooks=args.install_hooks,
91-
)
92-
elif args.command == 'uninstall':
93-
return uninstall(runner)
94-
elif args.command == 'clean':
95-
return clean(runner)
96-
elif args.command == 'autoupdate':
97-
return autoupdate(runner)
98-
elif args.command == 'run':
99-
return run(runner, args)
100-
else:
101-
raise NotImplementedError(
102-
'Command {0} not implemented.'.format(args.command)
107+
raise AssertionError(
108+
'Command {0} failed to exit with a returncode'.format(args.command)
103109
)
104110

105-
raise AssertionError(
106-
'Command {0} failed to exit with a returncode'.format(args.command)
107-
)
108-
109111

110112
if __name__ == '__main__':
111113
exit(main())

pre_commit/output.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616

1717

1818
def get_hook_message(
19-
start,
20-
postfix='',
21-
end_msg=None,
22-
end_len=0,
23-
end_color=None,
24-
use_color=None,
25-
cols=COLS,
19+
start,
20+
postfix='',
21+
end_msg=None,
22+
end_len=0,
23+
end_color=None,
24+
use_color=None,
25+
cols=COLS,
2626
):
2727
"""Prints a message for running a hook.
2828

pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[MESSAGES CONTROL]
2-
disable=missing-docstring,abstract-method,redefined-builtin,useless-else-on-loop,redefined-outer-name,invalid-name
2+
disable=locally-disabled,fixme,missing-docstring,abstract-method,useless-else-on-loop,invalid-name
33

44
[REPORTS]
55
output-format=colorized

tests/error_handler_test.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from __future__ import absolute_import
2+
from __future__ import unicode_literals
3+
4+
import io
5+
import os.path
6+
import mock
7+
import pytest
8+
import re
9+
10+
from pre_commit import error_handler
11+
from pre_commit.errors import FatalError
12+
13+
14+
@pytest.yield_fixture
15+
def mocked_log_and_exit():
16+
with mock.patch.object(error_handler, '_log_and_exit') as log_and_exit:
17+
yield log_and_exit
18+
19+
20+
def test_error_handler_no_exception(mocked_log_and_exit):
21+
with error_handler.error_handler():
22+
pass
23+
assert mocked_log_and_exit.call_count == 0
24+
25+
26+
def test_error_handler_fatal_error(mocked_log_and_exit):
27+
exc = FatalError('just a test')
28+
with error_handler.error_handler():
29+
raise exc
30+
31+
mocked_log_and_exit.assert_called_once_with(
32+
'An error has occurred',
33+
exc,
34+
# Tested below
35+
mock.ANY,
36+
)
37+
38+
assert re.match(
39+
'Traceback \(most recent call last\):\n'
40+
' File ".+/pre_commit/error_handler.py", line \d+, in error_handler\n'
41+
' yield\n'
42+
' File ".+/tests/error_handler_test.py", line \d+, '
43+
'in test_error_handler_fatal_error\n'
44+
' raise exc\n'
45+
'(pre_commit\.errors\.)?FatalError: just a test\n',
46+
mocked_log_and_exit.call_args[0][2],
47+
)
48+
49+
50+
def test_error_handler_uncaught_error(mocked_log_and_exit):
51+
exc = ValueError('another test')
52+
with error_handler.error_handler():
53+
raise exc
54+
55+
mocked_log_and_exit.assert_called_once_with(
56+
'An unexpected error has occurred',
57+
exc,
58+
# Tested below
59+
mock.ANY,
60+
)
61+
assert re.match(
62+
'Traceback \(most recent call last\):\n'
63+
' File ".+/pre_commit/error_handler.py", line \d+, in error_handler\n'
64+
' yield\n'
65+
' File ".+/tests/error_handler_test.py", line \d+, '
66+
'in test_error_handler_uncaught_error\n'
67+
' raise exc\n'
68+
'ValueError: another test\n',
69+
mocked_log_and_exit.call_args[0][2],
70+
)
71+
72+
73+
def test_log_and_exit(mock_out_store_directory):
74+
mocked_print = mock.Mock()
75+
with pytest.raises(error_handler.PreCommitSystemExit):
76+
error_handler._log_and_exit(
77+
'msg', FatalError('hai'), "I'm a stacktrace",
78+
print_fn=mocked_print,
79+
)
80+
81+
printed = '\n'.join(call[0][0] for call in mocked_print.call_args_list)
82+
assert printed == (
83+
'msg: FatalError: hai\n'
84+
'Check the log at ~/.pre-commit/pre-commit.log'
85+
)
86+
87+
log_file = os.path.join(mock_out_store_directory, 'pre-commit.log')
88+
assert os.path.exists(log_file)
89+
contents = io.open(log_file).read()
90+
assert contents == (
91+
'msg: FatalError: hai\n'
92+
"I'm a stacktrace\n"
93+
)

0 commit comments

Comments
 (0)