Skip to content

Commit 89ece60

Browse files
committed
Allow the exception handling in TracebackHandler to be customised.
1 parent c7950ea commit 89ece60

File tree

24 files changed

+104
-26
lines changed

24 files changed

+104
-26
lines changed

consolekit/tracebacks.py

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
Functions for handling exceptions and their tracebacks.
66
77
.. versionadded:: 1.0.0
8+
.. latex:vspace:: -10px
89
"""
910
#
1011
# Copyright © 2020-2021 Dominic Davis-Foster <[email protected]>
@@ -30,12 +31,17 @@
3031

3132
# stdlib
3233
import contextlib
33-
from typing import Callable, ContextManager, Type, TypeVar
34+
import sys
35+
from typing import TYPE_CHECKING, Callable, ContextManager, List, Type, TypeVar, Union
3436

3537
# 3rd party
3638
import click
3739
from domdf_python_tools.compat import nullcontext
3840

41+
if sys.version_info >= (3, 7) or TYPE_CHECKING:
42+
# stdlib
43+
from typing import NoReturn
44+
3945
__all__ = ["TracebackHandler", "handle_tracebacks", "traceback_handler", "traceback_option"]
4046

4147
_C = TypeVar("_C", bound=click.Command)
@@ -65,38 +71,65 @@ class TracebackHandler:
6571
is the name of the exception class to handle.
6672
6773
.. versionadded:: 1.0.0
74+
75+
:param exception: The exception to raise after handling the traceback.
76+
If not running within a click command or group you'll likely
77+
want to set this to :exc:`SystemExit(1) <SystemExit>`.
78+
:default exception: :class:`click.Abort() <click.Abort>`
79+
80+
.. versionchanged:: 1.4.0 Added the ``exception`` argument.
81+
6882
.. seealso:: :func:`~.handle_tracebacks`.
6983
.. latex:clearpage::
7084
""" # noqa: D400
7185

72-
def handle_EOFError(self, e: EOFError) -> bool: # noqa: D102
73-
raise e
86+
exception: Exception
87+
"""
88+
The exception to raise after handling the traceback.
7489
75-
def handle_KeyboardInterrupt(self, e: KeyboardInterrupt) -> bool: # noqa: D102
76-
raise e
90+
.. versionadded:: 1.4.0
91+
"""
7792

78-
def handle_ClickException(self, e: click.ClickException) -> bool: # noqa: D102
79-
raise e
93+
def __init__(self, exception: BaseException = click.Abort()):
94+
self.exception: BaseException = exception # type: ignore[assignment]
8095

81-
def handle_Abort(self, e: click.Abort) -> bool: # noqa: D102
82-
raise e
96+
def abort(self, msg: Union[str, List[str]]) -> "NoReturn":
97+
"""
98+
Abort the current process by calling ``self.exception``.
99+
100+
.. versionadded:: 1.4.0
101+
102+
:param msg: The message to write to stderr before raising the exception.
103+
"""
83104

84-
def handle_SystemExit(self, e: SystemExit) -> bool: # noqa: D102
105+
if isinstance(msg, str):
106+
msg = ''.join(msg)
107+
108+
if msg:
109+
click.echo(msg, err=True, color=False)
110+
111+
raise self.exception
112+
113+
def handle_EOFError(self, e: EOFError) -> "NoReturn": # noqa: D102
85114
raise e
86115

87-
def handle_FileNotFoundError(self, e: FileNotFoundError) -> bool: # noqa: D102
116+
def handle_KeyboardInterrupt(self, e: KeyboardInterrupt) -> "NoReturn": # noqa: D102
117+
raise e
88118

89-
# this package
90-
from consolekit.utils import abort
119+
def handle_ClickException(self, e: click.ClickException) -> "NoReturn": # noqa: D102
120+
raise e
91121

92-
raise abort(f"File Not Found: {e}", colour=False)
122+
def handle_Abort(self, e: click.Abort) -> "NoReturn": # noqa: D102
123+
raise e
93124

94-
def handle_FileExistsError(self, e: FileExistsError) -> bool: # noqa: D102
125+
def handle_SystemExit(self, e: SystemExit) -> "NoReturn": # noqa: D102
126+
raise e
95127

96-
# this package
97-
from consolekit.utils import abort
128+
def handle_FileNotFoundError(self, e: FileNotFoundError) -> "NoReturn": # noqa: D102
129+
self.abort(f"File Not Found: {e}")
98130

99-
raise abort(f"File Exists: {e}", colour=False)
131+
def handle_FileExistsError(self, e: FileExistsError) -> "NoReturn": # noqa: D102
132+
self.abort(f"File Exists: {e}")
100133

101134
def handle(self, e: BaseException) -> bool:
102135
"""
@@ -117,7 +150,7 @@ def handle(self, e: BaseException) -> bool:
117150
if hasattr(self, f"handle_{base.__name__}"):
118151
return getattr(self, f"handle_{base.__name__}")(e)
119152

120-
raise abort(f"An error occurred: {e}", colour=False)
153+
self.abort(f"An error occurred: {e}")
121154

122155
@contextlib.contextmanager
123156
def __call__(self):
@@ -171,8 +204,9 @@ def handle_tracebacks(
171204
In either case the program execution will stop on error.
172205
:param cls: The class to use to handle the tracebacks.
173206
174-
.. versionchanged:: 1.0.0 Added the ``cls`` parameter.
207+
:rtype:
175208
209+
.. versionchanged:: 1.0.0 Added the ``cls`` parameter.
176210
.. seealso:: :func:`~.traceback_handler` and :class:`~.TracebackHandler`
177211
"""
178212

tests/test_tracebacks.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
]
2727
)
2828
contextmanagers = pytest.mark.parametrize(
29-
"contextmanager",
29+
"contextmanager, exit_code",
3030
[
31-
pytest.param(handle_tracebacks, id="handle_tracebacks"),
32-
pytest.param(traceback_handler, id="traceback_handler"),
33-
pytest.param(TracebackHandler(), id="TracebackHandler"),
31+
pytest.param(handle_tracebacks, 1, id="handle_tracebacks"),
32+
pytest.param(traceback_handler, 1, id="traceback_handler"),
33+
pytest.param(TracebackHandler(), 1, id="TracebackHandler"),
34+
pytest.param(TracebackHandler(SystemExit(1)), 1, id="TracebackHandler_SystemExit"),
35+
pytest.param(TracebackHandler(SystemExit(2)), 2, id="TracebackHandler_SystemExit_2"),
3436
]
3537
)
3638

@@ -40,6 +42,7 @@
4042
def test_handle_tracebacks(
4143
exception,
4244
contextmanager: Callable[..., ContextManager],
45+
exit_code: int,
4346
file_regression,
4447
cli_runner: CliRunner,
4548
):
@@ -52,7 +55,7 @@ def demo():
5255

5356
result: Result = cli_runner.invoke(demo, catch_exceptions=False)
5457
result.check_stdout(file_regression)
55-
assert result.exit_code == 1
58+
assert result.exit_code == exit_code
5659

5760

5861
@exceptions
@@ -74,6 +77,7 @@ def test_handle_tracebacks_ignored_exceptions_click(
7477
exception: Exception,
7578
contextmanager: Callable[..., ContextManager],
7679
cli_runner: CliRunner,
80+
exit_code: int,
7781
):
7882

7983
@click.command()
@@ -93,6 +97,7 @@ def demo():
9397
def test_handle_tracebacks_ignored_exceptions(
9498
exception: Type[Exception],
9599
contextmanager: Callable[..., ContextManager],
100+
exit_code: int,
96101
):
97102

98103
with pytest.raises(exception): # noqa: PT012
@@ -140,7 +145,8 @@ def test_handle_tracebacks_ignored_click(
140145
advanced_file_regression: AdvancedFileRegressionFixture,
141146
code: int,
142147
cli_runner: CliRunner,
143-
click_version: str
148+
click_version: str,
149+
exit_code: int,
144150
):
145151

146152
@click.command()
@@ -151,6 +157,10 @@ def demo():
151157

152158
result = cli_runner.invoke(demo, catch_exceptions=False)
153159
result.check_stdout(advanced_file_regression)
160+
161+
# if code == 1 and exit_code == 2:
162+
# code = 2
163+
154164
assert result.exit_code == code
155165

156166

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
An error occurred: Something's awry!
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
File Exists: foo.txt
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
File Not Found: foo.txt
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
An error occurred: name 'hello' is not defined
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
An error occurred: invalid syntax
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
An error occurred: Expected type int, got type str
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
An error occurred: 'age' must be >= 0
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
An error occurred: Something's awry!

0 commit comments

Comments
 (0)