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