diff --git a/lib/vsc/utils/exceptions.py b/lib/vsc/utils/exceptions.py
new file mode 100644
index 00000000..5b27bee0
--- /dev/null
+++ b/lib/vsc/utils/exceptions.py
@@ -0,0 +1,93 @@
+##
+# Copyright 2015-2015 Ghent University
+#
+# This file is part of vsc-base,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://vscentrum.be/nl/en),
+# the Hercules foundation (http://www.herculesstichting.be/in_English)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# http://github.com/hpcugent/vsc-base
+#
+# vsc-base is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Library General Public License as
+# published by the Free Software Foundation, either version 2 of
+# the License, or (at your option) any later version.
+#
+# vsc-base is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Library General Public License for more details.
+#
+# You should have received a copy of the GNU Library General Public License
+# along with vsc-base. If not, see .
+##
+"""
+Module providing custom exceptions.
+
+@author: Kenneth Hoste (Ghent University)
+@author: Riccardo Murri (University of Zurich)
+"""
+import inspect
+import logging
+from vsc.utils import fancylogger
+
+
+def get_callers_logger():
+ """
+ Get logger defined in caller's environment
+ @return: logger instance (or None if none was found)
+ """
+ logger_cls = logging.getLoggerClass()
+ frame = inspect.currentframe()
+ logger = None
+
+ # frame may be None, see https://docs.python.org/2/library/inspect.html#inspect.currentframe
+ if frame is not None:
+ try:
+ # consider calling stack in reverse order, i.e. most inner frame (closest to caller) first
+ for frameinfo in inspect.getouterframes(frame)[::-1]:
+ bindings = inspect.getargvalues(frameinfo[0]).locals
+ for val in bindings.values():
+ if isinstance(val, logger_cls):
+ logger = val
+ break
+ finally:
+ # make very sure that reference to frame object is removed, to avoid reference cycles
+ # see https://docs.python.org/2/library/inspect.html#the-interpreter-stack
+ del frame
+
+ return logger
+
+
+class LoggedException(Exception):
+ """Exception that logs it's message when it is created."""
+
+ # logger module to use (must provide getLogger() function)
+ LOGGER_MODULE = fancylogger
+ # name of logging method to use
+ # must accept an argument of type string, i.e. the log message, and an optional list of formatting arguments
+ LOGGING_METHOD_NAME = 'error'
+
+ def __init__(self, msg, *args, **kwargs):
+ """
+ Constructor.
+ @param msg: exception message
+ @param *args: list of formatting arguments for exception message
+ @param logger: logger to use
+ """
+ # format message with (optional) list of formatting arguments
+ msg = msg % args
+
+ logger = kwargs.get('logger', None)
+ # try to use logger defined in caller's environment
+ if logger is None:
+ logger = get_callers_logger()
+ # search can fail, use root logger as a fallback
+ if logger is None:
+ logger = self.LOGGER_MODULE.getLogger()
+
+ getattr(logger, self.LOGGING_METHOD_NAME)(msg)
+
+ super(LoggedException, self).__init__(msg)
diff --git a/setup.py b/setup.py
index 234dfdeb..630dd01f 100755
--- a/setup.py
+++ b/setup.py
@@ -52,7 +52,7 @@ def remove_bdist_rpm_source_file():
PACKAGE = {
'name': 'vsc-base',
- 'version': '2.0.4',
+ 'version': '2.1.0',
'author': [sdw, jt, ag, kh],
'maintainer': [sdw, jt, ag, kh],
'packages': ['vsc', 'vsc.utils', 'vsc.install'],
diff --git a/test/exceptions.py b/test/exceptions.py
new file mode 100644
index 00000000..43feef3c
--- /dev/null
+++ b/test/exceptions.py
@@ -0,0 +1,145 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2015-2015 Ghent University
+#
+# This file is part of vsc-base,
+# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en),
+# with support of Ghent University (http://ugent.be/hpc),
+# the Flemish Supercomputer Centre (VSC) (https://vscentrum.be/nl/en),
+# the Hercules foundation (http://www.herculesstichting.be/in_English)
+# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en).
+#
+# http://github.com/hpcugent/vsc-base
+#
+# vsc-base is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Library General Public License as
+# published by the Free Software Foundation, either version 2 of
+# the License, or (at your option) any later version.
+#
+# vsc-base is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Library General Public License for more details.
+#
+# You should have received a copy of the GNU Library General Public License
+# along with vsc-base. If not, see .
+#
+"""
+Unit tests for exceptions module.
+
+@author: Kenneth Hoste (Ghent University)
+"""
+import logging
+import os
+import re
+import tempfile
+from unittest import TestLoader, main
+
+from vsc.utils.exceptions import LoggedException, get_callers_logger
+from vsc.utils.fancylogger import getLogger, logToFile, logToScreen, getRootLoggerName, setLogFormat
+from vsc.utils.testing import EnhancedTestCase
+
+
+def raise_loggedexception(msg, *args, **kwargs):
+ """Utility function: just raise a LoggedException."""
+ raise LoggedException(msg, *args, **kwargs)
+
+
+class ExceptionsTest(EnhancedTestCase):
+ """Tests for exceptions module."""
+
+ def test_loggedexception_defaultlogger(self):
+ """Test LoggedException custom exception class."""
+ fd, tmplog = tempfile.mkstemp()
+ os.close(fd)
+
+ # set log format, for each regex searching
+ setLogFormat("%(name)s :: %(message)s")
+
+ # if no logger is available, and no logger is specified, use default 'root' fancylogger
+ logToFile(tmplog, enable=True)
+ self.assertErrorRegex(LoggedException, 'BOOM', raise_loggedexception, 'BOOM')
+ logToFile(tmplog, enable=False)
+
+ log_re = re.compile("^%s :: BOOM$" % getRootLoggerName(), re.M)
+ logtxt = open(tmplog, 'r').read()
+ self.assertTrue(log_re.match(logtxt), "%s matches %s" % (log_re.pattern, logtxt))
+
+ # test formatting of message
+ self.assertErrorRegex(LoggedException, 'BOOMBAF', raise_loggedexception, 'BOOM%s', 'BAF')
+
+ os.remove(tmplog)
+
+ def test_loggedexception_specifiedlogger(self):
+ """Test LoggedException custom exception class."""
+ fd, tmplog = tempfile.mkstemp()
+ os.close(fd)
+
+ # set log format, for each regex searching
+ setLogFormat("%(name)s :: %(message)s")
+
+ logger1 = getLogger('testlogger_one')
+ logger2 = getLogger('testlogger_two')
+
+ # if logger is specified, it should be used
+ logToFile(tmplog, enable=True)
+ self.assertErrorRegex(LoggedException, 'BOOM', raise_loggedexception, 'BOOM', logger=logger1)
+ logToFile(tmplog, enable=False)
+
+ log_re = re.compile("^%s.testlogger_one :: BOOM$" % getRootLoggerName())
+ logtxt = open(tmplog, 'r').read()
+ self.assertTrue(log_re.match(logtxt), "%s matches %s" % (log_re.pattern, logtxt))
+
+ os.remove(tmplog)
+
+ def test_loggedexception_callerlogger(self):
+ """Test LoggedException custom exception class."""
+ fd, tmplog = tempfile.mkstemp()
+ os.close(fd)
+
+ # set log format, for each regex searching
+ setLogFormat("%(name)s :: %(message)s")
+
+ logger = getLogger('testlogger_local')
+
+ # if no logger is specified, logger available in calling context should be used
+ logToFile(tmplog, enable=True)
+ self.assertErrorRegex(LoggedException, 'BOOM', raise_loggedexception, 'BOOM')
+ logToFile(tmplog, enable=False)
+
+ log_re = re.compile("^%s.testlogger_local :: BOOM$" % getRootLoggerName())
+ logtxt = open(tmplog, 'r').read()
+ self.assertTrue(log_re.match(logtxt), "%s matches %s" % (log_re.pattern, logtxt))
+
+ os.remove(tmplog)
+
+ def test_get_callers_logger(self):
+ """Test get_callers_logger function."""
+ # returns None if no logger is available
+ self.assertEqual(get_callers_logger(), None)
+
+ # find defined logger in caller's context
+ logger = getLogger('foo')
+ self.assertEqual(logger, get_callers_logger())
+
+ # also works when logger is 'higher up'
+ class Test(object):
+ """Dummy test class"""
+ def foo(self, logger=None):
+ """Dummy test method, returns logger from calling context."""
+ return get_callers_logger()
+
+ test = Test()
+ self.assertEqual(logger, test.foo())
+
+ # closest logger to caller is preferred
+ logger2 = getLogger(test.__class__.__name__)
+ self.assertEqual(logger2, test.foo(logger=logger2))
+
+def suite():
+ """ returns all the testcases in this module """
+ return TestLoader().loadTestsFromTestCase(ExceptionsTest)
+
+if __name__ == '__main__':
+ """Use this __main__ block to help write and test unittests"""
+ main()
diff --git a/test/missing.py b/test/missing.py
index f536338f..9839c522 100644
--- a/test/missing.py
+++ b/test/missing.py
@@ -273,7 +273,7 @@ def test_modules_in_pkg_path(self):
"""Test modules_in_pkg_path function."""
# real example
import vsc.utils
- vsc_utils_modules = ['__init__', 'affinity', 'asyncprocess', 'daemon', 'dateandtime', 'fancylogger',
+ vsc_utils_modules = ['__init__', 'affinity', 'asyncprocess', 'daemon', 'dateandtime', 'exceptions', 'fancylogger',
'frozendict', 'generaloption', 'mail', 'missing', 'optcomplete', 'patterns', 'rest',
'run', 'testing', 'wrapper']
self.assertEqual(sorted(modules_in_pkg_path(vsc.utils.__path__[0])), vsc_utils_modules)
diff --git a/test/runner.py b/test/runner.py
index fce7e894..ef4e2fa7 100644
--- a/test/runner.py
+++ b/test/runner.py
@@ -6,8 +6,9 @@
import test.asyncprocess as a
import test.dateandtime as td
-import test.generaloption as tg
+import test.exceptions as te
import test.fancylogger as tf
+import test.generaloption as tg
import test.missing as tm
import test.rest as trest
import test.run as trun
@@ -20,7 +21,7 @@
from vsc.utils import fancylogger
fancylogger.logToScreen(enable=False)
-suite = unittest.TestSuite([x.suite() for x in (a, td, tg, tf, tm, trest, trun, tt, topt, wrapt)])
+suite = unittest.TestSuite([x.suite() for x in (a, td, tg, tf, te, tm, trest, trun, tt, topt, wrapt)])
try:
import xmlrunner