Skip to content

Commit

Permalink
Update exception handling in tools
Browse files Browse the repository at this point in the history
- Remove SystemExit handling in `Tool.run()`
- Add a possibility to register custom exceptions
  and handle them in `Tool.run()` with the preservation of the exit code.
  • Loading branch information
mexanick committed Jul 17, 2024
1 parent 2fec8ca commit 5dd363d
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 26 deletions.
11 changes: 0 additions & 11 deletions src/ctapipe/core/tests/test_run_tool.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import sys
from subprocess import CalledProcessError

import pytest
Expand All @@ -17,15 +16,5 @@ def start(self):
ret = run_tool(ErrorTool(), ["--non-existing-alias"], raises=False)
assert ret == 2

class SysExitTool(Tool):
def setup(self):
pass

def start(self):
sys.exit(4)

ret = run_tool(SysExitTool(), raises=False)
assert ret == 4

with pytest.raises(CalledProcessError):
run_tool(ErrorTool(), ["--non-existing-alias"], raises=True)
29 changes: 29 additions & 0 deletions src/ctapipe/core/tests/test_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,13 +381,42 @@ class ToolBad(Tool):
def start(self):
raise ValueError("1 does not equal 0.")

class CustomErrorNoExitCode(Exception):
pass

class CustomErrorWithExitCode(Exception):
exit_code = 42

class ToolCustomExceptionNoExitCode(Tool):
name = "CustomException"
description = "This tool raises a custom exception without an exit code."
custom_exceptions = (CustomErrorNoExitCode,)

def start(self):
raise CustomErrorNoExitCode("This is a custom exception.")

class ToolCustomExceptionWithExitCode(Tool):
name = "CustomException"
description = "This tool raises a custom exception with a custom exit code."
custom_exceptions = (CustomErrorWithExitCode,)

def start(self):
raise CustomErrorWithExitCode("This is a custom exception.")

assert run_tool(ToolGood(), raises=True) == 0

assert run_tool(ToolBad(), raises=False) == 1

assert run_tool(ToolCustomExceptionNoExitCode(), raises=False) == 1

assert run_tool(ToolCustomExceptionWithExitCode(), raises=False) == 42

with pytest.raises(ValueError):
run_tool(ToolBad(), raises=True)

with pytest.raises(CustomErrorNoExitCode):
run_tool(ToolCustomExceptionNoExitCode(), raises=True)


def test_exit_stack():
"""Test that components that are context managers are properly handled"""
Expand Down
34 changes: 19 additions & 15 deletions src/ctapipe/core/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ class MyTool(Tool):
# Which classes are registered for configuration
classes = [MyComponent, AdvancedComponent, SecondaryMyComponent]
# Tuple with the custom exceptions the tool can raise (optional)
# If the exception has an exit_code attribute, it will be used
custom_exceptions = (MyCustomError, MyOtherError)
# local configuration parameters
iterations = Integer(5,help="Number of times to run",
allow_none=False).tag(config=True)
Expand Down Expand Up @@ -181,6 +185,8 @@ def main():

provenance_log = Path(directory_ok=False).tag(config=True)

custom_exceptions = ()

@default("provenance_log")
def _default_provenance_log(self):
return self.name + ".provenance.log"
Expand Down Expand Up @@ -443,6 +449,19 @@ def run(self, argv=None, raises=False):
Provenance().finish_activity(
activity_name=self.name, status="interrupted", exit_code=exit_status
)
except self.custom_exceptions as err:
self.log.exception("Caught custom exception: %s", err)
if hasattr(err, "exit_code"):
exit_status = err.exit_code
else:
exit_status = 1
Provenance().finish_activity(
activity_name=self.name,
status="error",
exit_code=exit_status,
)
if raises:
raise
except Exception as err:
self.log.exception("Caught unexpected exception: %s", err)
exit_status = 1 # any other error
Expand All @@ -451,21 +470,6 @@ def run(self, argv=None, raises=False):
)
if raises:
raise
except SystemExit as err:
exit_status = err.code
# Do nothing if SystemExit was called with the exit code 0 (e.g. with -h option)
if exit_status != 0:
if raises:
raise # do not re-intercept in tests
else:
self.log.exception(
"Caught SystemExit with exit code %s", exit_status
)
Provenance().finish_activity(
activity_name=self.name,
status="error",
exit_code=exit_status,
)
finally:
if not {"-h", "--help", "--help-all"}.intersection(self.argv):
self.write_provenance()
Expand Down

0 comments on commit 5dd363d

Please sign in to comment.