summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--cmd2/cmd2.py5
-rw-r--r--cmd2/exceptions.py15
-rw-r--r--docs/api/exceptions.rst3
-rwxr-xr-xtests/test_cmd2.py17
-rw-r--r--tests/test_plugin.py22
6 files changed, 60 insertions, 3 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index afed0b53..1e049bc9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 0b198111..d01dfaa0 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -95,6 +95,7 @@ from .exceptions import (
CompletionError,
EmbeddedConsoleExit,
EmptyStatement,
+ PassThroughException,
RedirectionError,
SkipPostcommandHooks,
)
@@ -2259,6 +2260,8 @@ class Cmd(cmd.Cmd):
raise ex
except SystemExit:
stop = True
+ except PassThroughException as ex:
+ raise ex.wrapped_ex
except Exception as ex:
self.pexcept(ex)
finally:
@@ -2269,6 +2272,8 @@ class Cmd(cmd.Cmd):
raise ex
except SystemExit:
stop = True
+ except PassThroughException as ex:
+ raise ex.wrapped_ex
except Exception as ex:
self.pexcept(ex)
diff --git a/cmd2/exceptions.py b/cmd2/exceptions.py
index 39b7e333..b45167d1 100644
--- a/cmd2/exceptions.py
+++ b/cmd2/exceptions.py
@@ -62,6 +62,21 @@ class CompletionError(Exception):
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.
############################################################################################################
diff --git a/docs/api/exceptions.rst b/docs/api/exceptions.rst
index 98afa97a..efa26957 100644
--- a/docs/api/exceptions.rst
+++ b/docs/api/exceptions.rst
@@ -15,3 +15,6 @@ Custom cmd2 exceptions
.. autoclass:: cmd2.exceptions.CompletionError
:members:
+
+.. autoclass:: cmd2.exceptions.PassThroughException
+ :members:
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 0c3333c1..b4b13945 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -534,7 +534,7 @@ def test_in_script(request):
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, _):
@@ -546,6 +546,21 @@ def test_system_exit_in_command(base_app, capsys):
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)
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 97c046a2..1e12d655 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -238,6 +238,14 @@ class Plugin:
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
@@ -916,7 +924,7 @@ 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')
@@ -924,7 +932,7 @@ def test_cmdfinalization_hook_system_exit(capsys):
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)
@@ -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)