Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* Added support for custom tab completion and up-arrow input history to `cmd2.Cmd2.read_input`.
See [read_input.py](https://github.com/python-cmd2/cmd2/blob/master/examples/read_input.py)
for an example.
* Added `cmd2.exceptions.PassThroughException` to raise unhandled command exceptions instead of printing them.

## 1.5.0 (January 31, 2021)
* Bug Fixes
Expand Down
5 changes: 5 additions & 0 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
CompletionError,
EmbeddedConsoleExit,
EmptyStatement,
PassThroughException,
RedirectionError,
SkipPostcommandHooks,
)
Expand Down Expand Up @@ -2259,6 +2260,8 @@ def onecmd_plus_hooks(
raise ex
except SystemExit:
stop = True
except PassThroughException as ex:
raise ex.wrapped_ex
except Exception as ex:
self.pexcept(ex)
finally:
Expand All @@ -2269,6 +2272,8 @@ def onecmd_plus_hooks(
raise ex
except SystemExit:
stop = True
except PassThroughException as ex:
raise ex.wrapped_ex
except Exception as ex:
self.pexcept(ex)

Expand Down
15 changes: 15 additions & 0 deletions cmd2/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ def __init__(self, *args, apply_style: bool = True):
super().__init__(*args)


class PassThroughException(Exception):
"""
Normally all unhandled exceptions raised during commands get printed to the user.
This class is used to wrap an exception that should be raised instead of printed.
"""

def __init__(self, *args, wrapped_ex: BaseException):
"""
Initializer for PassThroughException
:param wrapped_ex: the exception that will be raised
"""
self.wrapped_ex = wrapped_ex
super().__init__(*args)


############################################################################################################
# The following exceptions are NOT part of the public API and are intended for internal use only.
############################################################################################################
Expand Down
3 changes: 3 additions & 0 deletions docs/api/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ Custom cmd2 exceptions

.. autoclass:: cmd2.exceptions.CompletionError
:members:

.. autoclass:: cmd2.exceptions.PassThroughException
:members:
17 changes: 16 additions & 1 deletion tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ def hook(self: cmd2.Cmd, data: plugin.CommandFinalizationData) -> plugin.Command


def test_system_exit_in_command(base_app, capsys):
"""Test raising SystemExit from a command"""
"""Test raising SystemExit in a command"""
import types

def do_system_exit(self, _):
Expand All @@ -546,6 +546,21 @@ def do_system_exit(self, _):
assert stop


def test_passthrough_exception_in_command(base_app):
"""Test raising a PassThroughException in a command"""
import types

def do_passthrough(self, _):
wrapped_ex = OSError("Pass me up")
raise exceptions.PassThroughException(wrapped_ex=wrapped_ex)

setattr(base_app, 'do_passthrough', types.MethodType(do_passthrough, base_app))

with pytest.raises(OSError) as excinfo:
base_app.onecmd_plus_hooks('passthrough')
assert 'Pass me up' in str(excinfo.value)


def test_output_redirection(base_app):
fd, filename = tempfile.mkstemp(prefix='cmd2_test', suffix='.txt')
os.close(fd)
Expand Down
22 changes: 20 additions & 2 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,14 @@ def cmdfinalization_hook_keyboard_interrupt(
self.called_cmdfinalization += 1
raise KeyboardInterrupt

def cmdfinalization_hook_passthrough_exception(
self, data: cmd2.plugin.CommandFinalizationData
) -> cmd2.plugin.CommandFinalizationData:
"""A command finalization hook which raises a PassThroughException"""
self.called_cmdfinalization += 1
wrapped_ex = OSError("Pass me up")
raise exceptions.PassThroughException(wrapped_ex=wrapped_ex)

def cmdfinalization_hook_not_enough_parameters(self) -> plugin.CommandFinalizationData:
"""A command finalization hook with no parameters."""
pass
Expand Down Expand Up @@ -916,15 +924,15 @@ def test_cmdfinalization_hook_exception(capsys):
assert app.called_cmdfinalization == 1


def test_cmdfinalization_hook_system_exit(capsys):
def test_cmdfinalization_hook_system_exit():
app = PluggedApp()
app.register_cmdfinalization_hook(app.cmdfinalization_hook_system_exit)
stop = app.onecmd_plus_hooks('say hello')
assert stop
assert app.called_cmdfinalization == 1


def test_cmdfinalization_hook_keyboard_interrupt(capsys):
def test_cmdfinalization_hook_keyboard_interrupt():
app = PluggedApp()
app.register_cmdfinalization_hook(app.cmdfinalization_hook_keyboard_interrupt)

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


def test_cmdfinalization_hook_passthrough_exception():
app = PluggedApp()
app.register_cmdfinalization_hook(app.cmdfinalization_hook_passthrough_exception)

with pytest.raises(OSError) as excinfo:
app.onecmd_plus_hooks('say hello')
assert 'Pass me up' in str(excinfo.value)
assert app.called_cmdfinalization == 1


def test_skip_postcmd_hooks(capsys):
app = PluggedApp()
app.register_postcmd_hook(app.postcmd_hook)
Expand Down