diff --git a/src/ctapipe/exceptions.py b/src/ctapipe/exceptions.py index 77ec1bd276a..47e2d7d2eda 100644 --- a/src/ctapipe/exceptions.py +++ b/src/ctapipe/exceptions.py @@ -1,3 +1,12 @@ +__all__ = [ + "CTAPipeException", + "TooFewEvents", + "OptionalDependencyMissing", + "InputMissing", + "MockOptionalDecorator", +] + + class CTAPipeException(Exception): pass @@ -17,3 +26,46 @@ def __init__(self, module): class InputMissing(ValueError): """Raised in case an input was not specified.""" + + +class MockOptionalDecorator: + """A decorator that can be used in-place of an imported decorator. + + Will throw the corresponding OptionalDependencyMissing exception when + the decorated function is called. + + Examples + -------- + You might want to use this class for optional dependencies that provide + decorators. With decorators, it is hard to defer import of the dependency + to runtime. See this example of how one might make numba with njit optional: + + from unittest import MagicMock + from ctapipe.exceptions import MockOptionalDecorator + try: + import numba + except ModuleNotFoundError: + numba = MagicMock() + numba.njit = MockOptionalDecorator("numba") + + @numba.njit(cache=True) + def example(x): + return 5 * x + """ + + def __init__(self, module): + self.module = module + + def __call__(self, *args, **kwargs): + def _raise(*args, **kwargs): + raise OptionalDependencyMissing(self.module) + + # decorator called as @decorator without arguments + if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): + return _raise + + # decorator called as @decorator(*args, **kwargs) + def wrapper(func): + return _raise + + return wrapper diff --git a/src/ctapipe/tests/test_exceptions.py b/src/ctapipe/tests/test_exceptions.py new file mode 100644 index 00000000000..3d846ecb03e --- /dev/null +++ b/src/ctapipe/tests/test_exceptions.py @@ -0,0 +1,35 @@ +from unittest.mock import MagicMock + +import pytest + + +def test_optional_decorator_no_args(): + from ctapipe.exceptions import MockOptionalDecorator, OptionalDependencyMissing + + dummy_opt_module = MagicMock() + dummy_opt_module.some_decorator = MockOptionalDecorator("dummy") + + # defining the function should not throw + @dummy_opt_module.some_decorator + def func(): + pass + + with pytest.raises(OptionalDependencyMissing, match="'dummy' is required"): + # calling the function should raise + func() + + +def test_optional_decorator_with_args(): + from ctapipe.exceptions import MockOptionalDecorator, OptionalDependencyMissing + + dummy_opt_module = MagicMock() + dummy_opt_module.some_decorator = MockOptionalDecorator("dummy") + + # defining the function should not throw + @dummy_opt_module.some_decorator(foo=5) + def func(): + pass + + with pytest.raises(OptionalDependencyMissing, match="'dummy' is required"): + # calling the function should raise + func()