Skip to content

Commit 4da77c4

Browse files
authored
Merge pull request #1067 from python-cmd2/exception_passthrough
Added cmd2.exceptions.PassThroughException
2 parents 6b14aa6 + 7024aa0 commit 4da77c4

File tree

6 files changed

+60
-3
lines changed

6 files changed

+60
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* Added support for custom tab completion and up-arrow input history to `cmd2.Cmd2.read_input`.
1919
See [read_input.py](https://github.com/python-cmd2/cmd2/blob/master/examples/read_input.py)
2020
for an example.
21+
* Added `cmd2.exceptions.PassThroughException` to raise unhandled command exceptions instead of printing them.
2122

2223
## 1.5.0 (January 31, 2021)
2324
* Bug Fixes

cmd2/cmd2.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
CompletionError,
9696
EmbeddedConsoleExit,
9797
EmptyStatement,
98+
PassThroughException,
9899
RedirectionError,
99100
SkipPostcommandHooks,
100101
)
@@ -2259,6 +2260,8 @@ def onecmd_plus_hooks(
22592260
raise ex
22602261
except SystemExit:
22612262
stop = True
2263+
except PassThroughException as ex:
2264+
raise ex.wrapped_ex
22622265
except Exception as ex:
22632266
self.pexcept(ex)
22642267
finally:
@@ -2269,6 +2272,8 @@ def onecmd_plus_hooks(
22692272
raise ex
22702273
except SystemExit:
22712274
stop = True
2275+
except PassThroughException as ex:
2276+
raise ex.wrapped_ex
22722277
except Exception as ex:
22732278
self.pexcept(ex)
22742279

cmd2/exceptions.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,21 @@ def __init__(self, *args, apply_style: bool = True):
6262
super().__init__(*args)
6363

6464

65+
class PassThroughException(Exception):
66+
"""
67+
Normally all unhandled exceptions raised during commands get printed to the user.
68+
This class is used to wrap an exception that should be raised instead of printed.
69+
"""
70+
71+
def __init__(self, *args, wrapped_ex: BaseException):
72+
"""
73+
Initializer for PassThroughException
74+
:param wrapped_ex: the exception that will be raised
75+
"""
76+
self.wrapped_ex = wrapped_ex
77+
super().__init__(*args)
78+
79+
6580
############################################################################################################
6681
# The following exceptions are NOT part of the public API and are intended for internal use only.
6782
############################################################################################################

docs/api/exceptions.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ Custom cmd2 exceptions
1515

1616
.. autoclass:: cmd2.exceptions.CompletionError
1717
:members:
18+
19+
.. autoclass:: cmd2.exceptions.PassThroughException
20+
:members:

tests/test_cmd2.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ def hook(self: cmd2.Cmd, data: plugin.CommandFinalizationData) -> plugin.Command
534534

535535

536536
def test_system_exit_in_command(base_app, capsys):
537-
"""Test raising SystemExit from a command"""
537+
"""Test raising SystemExit in a command"""
538538
import types
539539

540540
def do_system_exit(self, _):
@@ -546,6 +546,21 @@ def do_system_exit(self, _):
546546
assert stop
547547

548548

549+
def test_passthrough_exception_in_command(base_app):
550+
"""Test raising a PassThroughException in a command"""
551+
import types
552+
553+
def do_passthrough(self, _):
554+
wrapped_ex = OSError("Pass me up")
555+
raise exceptions.PassThroughException(wrapped_ex=wrapped_ex)
556+
557+
setattr(base_app, 'do_passthrough', types.MethodType(do_passthrough, base_app))
558+
559+
with pytest.raises(OSError) as excinfo:
560+
base_app.onecmd_plus_hooks('passthrough')
561+
assert 'Pass me up' in str(excinfo.value)
562+
563+
549564
def test_output_redirection(base_app):
550565
fd, filename = tempfile.mkstemp(prefix='cmd2_test', suffix='.txt')
551566
os.close(fd)

tests/test_plugin.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,14 @@ def cmdfinalization_hook_keyboard_interrupt(
238238
self.called_cmdfinalization += 1
239239
raise KeyboardInterrupt
240240

241+
def cmdfinalization_hook_passthrough_exception(
242+
self, data: cmd2.plugin.CommandFinalizationData
243+
) -> cmd2.plugin.CommandFinalizationData:
244+
"""A command finalization hook which raises a PassThroughException"""
245+
self.called_cmdfinalization += 1
246+
wrapped_ex = OSError("Pass me up")
247+
raise exceptions.PassThroughException(wrapped_ex=wrapped_ex)
248+
241249
def cmdfinalization_hook_not_enough_parameters(self) -> plugin.CommandFinalizationData:
242250
"""A command finalization hook with no parameters."""
243251
pass
@@ -916,15 +924,15 @@ def test_cmdfinalization_hook_exception(capsys):
916924
assert app.called_cmdfinalization == 1
917925

918926

919-
def test_cmdfinalization_hook_system_exit(capsys):
927+
def test_cmdfinalization_hook_system_exit():
920928
app = PluggedApp()
921929
app.register_cmdfinalization_hook(app.cmdfinalization_hook_system_exit)
922930
stop = app.onecmd_plus_hooks('say hello')
923931
assert stop
924932
assert app.called_cmdfinalization == 1
925933

926934

927-
def test_cmdfinalization_hook_keyboard_interrupt(capsys):
935+
def test_cmdfinalization_hook_keyboard_interrupt():
928936
app = PluggedApp()
929937
app.register_cmdfinalization_hook(app.cmdfinalization_hook_keyboard_interrupt)
930938

@@ -947,6 +955,16 @@ def test_cmdfinalization_hook_keyboard_interrupt(capsys):
947955
assert app.called_cmdfinalization == 1
948956

949957

958+
def test_cmdfinalization_hook_passthrough_exception():
959+
app = PluggedApp()
960+
app.register_cmdfinalization_hook(app.cmdfinalization_hook_passthrough_exception)
961+
962+
with pytest.raises(OSError) as excinfo:
963+
app.onecmd_plus_hooks('say hello')
964+
assert 'Pass me up' in str(excinfo.value)
965+
assert app.called_cmdfinalization == 1
966+
967+
950968
def test_skip_postcmd_hooks(capsys):
951969
app = PluggedApp()
952970
app.register_postcmd_hook(app.postcmd_hook)

0 commit comments

Comments
 (0)