From 270d5ddc31c26b62379e3caa9044dd75ccc71847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Picca=20Fr=C3=A9d=C3=A9ric-Emmanuel?= Date: Sun, 4 Mar 2018 10:20:27 +0100 Subject: New upstream version 0.7.0+dfsg --- silx/utils/array_like.py | 6 +- silx/utils/deprecation.py | 8 +- silx/utils/exceptions.py | 33 +++++ silx/utils/launcher.py | 18 +-- silx/utils/property.py | 52 +++++++ silx/utils/test/test_deprecation.py | 18 +-- silx/utils/test/test_launcher.py | 4 +- silx/utils/testutils.py | 281 ++++++++++++++++++++++++++++++++++++ 8 files changed, 390 insertions(+), 30 deletions(-) create mode 100644 silx/utils/exceptions.py create mode 100644 silx/utils/property.py create mode 100644 silx/utils/testutils.py (limited to 'silx/utils') diff --git a/silx/utils/array_like.py b/silx/utils/array_like.py index f4c85bf..11f531d 100644 --- a/silx/utils/array_like.py +++ b/silx/utils/array_like.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# Copyright (c) 2016-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -300,7 +300,7 @@ class ListOfImages(object): The returned object refers to the same images but with a different :attr:`transposition`. - :param list[int] transposition: List/tuple of dimension numbers in the + :param List[int] transposition: List/tuple of dimension numbers in the wanted order. If ``None`` (default), reverse the dimensions. :return: new :class:`ListOfImages` object @@ -568,7 +568,7 @@ class DatasetView(object): The returned object refers to the same dataset but with a different :attr:`transposition`. - :param list[int] transposition: List of dimension numbers in the wanted order. + :param List[int] transposition: List of dimension numbers in the wanted order. If ``None`` (default), reverse the dimensions. :return: Transposed DatasetView """ diff --git a/silx/utils/deprecation.py b/silx/utils/deprecation.py index f1d2a79..f9ba017 100644 --- a/silx/utils/deprecation.py +++ b/silx/utils/deprecation.py @@ -28,7 +28,7 @@ from __future__ import absolute_import, print_function, division __authors__ = ["Jerome Kieffer", "H. Payno", "P. Knobel"] __license__ = "MIT" -__date__ = "11/09/2017" +__date__ = "26/02/2018" import sys import logging @@ -40,7 +40,7 @@ depreclog = logging.getLogger("silx.DEPRECATION") deprecache = set([]) -def deprecated(func=None, reason=None, replacement=None, since_version=None, only_once=True): +def deprecated(func=None, reason=None, replacement=None, since_version=None, only_once=True, skip_backtrace_count=1): """ Decorator that deprecates the use of a function @@ -52,6 +52,8 @@ def deprecated(func=None, reason=None, replacement=None, since_version=None, onl deprecated (e.g. "0.5.0"). :param bool only_once: If true, the deprecation warning will only be generated one time. Default is true. + :param int skip_backtrace_count: Amount of last backtrace to ignore when + logging the backtrace """ def decorator(func): @functools.wraps(func) @@ -64,7 +66,7 @@ def deprecated(func=None, reason=None, replacement=None, since_version=None, onl replacement=replacement, since_version=since_version, only_once=only_once, - skip_backtrace_count=1) + skip_backtrace_count=skip_backtrace_count) return func(*args, **kwargs) return wrapper if func is not None: diff --git a/silx/utils/exceptions.py b/silx/utils/exceptions.py new file mode 100644 index 0000000..addba89 --- /dev/null +++ b/silx/utils/exceptions.py @@ -0,0 +1,33 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""Bunch of useful exceptions""" + +__authors__ = ["H. Payno"] +__license__ = "MIT" +__date__ = "17/01/2018" + + +class NotEditableError(Exception): + """Exception emitted when try to access to a non editable attribute""" diff --git a/silx/utils/launcher.py b/silx/utils/launcher.py index 8d2c81c..059e990 100644 --- a/silx/utils/launcher.py +++ b/silx/utils/launcher.py @@ -2,7 +2,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2016 European Synchrotron Radiation Facility +# Copyright (c) 2004-2017 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -41,7 +41,7 @@ import logging _logger = logging.getLogger(__name__) -class LauncherCommand(): +class LauncherCommand(object): """Description of a command""" def __init__(self, name, description=None, module_name=None, function=None): @@ -68,17 +68,9 @@ class LauncherCommand(): try: module = importlib.import_module(self.module_name) return module - except ImportError as e: - if "No module name" in e.args[0]: - msg = "Error while reaching module '%s'" - _logger.debug(msg, self.module_name, exc_info=True) - missing_module = e.args[0].split("'")[1] - msg = "Module '%s' is not installed but is mandatory."\ - + " You can install it using \"pip install %s\"." - _logger.error(msg, missing_module, missing_module) - else: - msg = "Error while reaching module '%s'" - _logger.error(msg, self.module_name, exc_info=True) + except ImportError: + msg = "Error while reaching module '%s'" + _logger.error(msg, self.module_name, exc_info=True) return None def get_function(self): diff --git a/silx/utils/property.py b/silx/utils/property.py new file mode 100644 index 0000000..10d5d98 --- /dev/null +++ b/silx/utils/property.py @@ -0,0 +1,52 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""Bunch of useful decorators""" + +from __future__ import absolute_import, print_function, division + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "22/02/2018" + + +class classproperty(property): + """ + Decorator to transform an object method into a class property. + + This code are equivalent, but the second one can be decorated with + deprecation warning for example. + + .. code-block:: python + + class Foo(object): + VALUE = 10 + + class Foo2(object): + @classproperty + def VALUE(self): + return 10 + """ + def __get__(self, cls, owner): + return classmethod(self.fget).__get__(None, owner)() diff --git a/silx/utils/test/test_deprecation.py b/silx/utils/test/test_deprecation.py index b5c5de4..0aa06a0 100644 --- a/silx/utils/test/test_deprecation.py +++ b/silx/utils/test/test_deprecation.py @@ -26,12 +26,12 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "11/09/2017" +__date__ = "17/01/2018" import unittest from .. import deprecation -from silx.test import utils +from silx.utils import testutils class TestDeprecation(unittest.TestCase): @@ -53,22 +53,22 @@ class TestDeprecation(unittest.TestCase): def deprecatedEveryTime(self): pass - @utils.test_logging(deprecation.depreclog.name, warning=1) + @testutils.test_logging(deprecation.depreclog.name, warning=1) def testAnnotationWithoutParam(self): self.deprecatedWithoutParam() - @utils.test_logging(deprecation.depreclog.name, warning=1) + @testutils.test_logging(deprecation.depreclog.name, warning=1) def testAnnotationWithParams(self): self.deprecatedWithParams() - @utils.test_logging(deprecation.depreclog.name, warning=3) + @testutils.test_logging(deprecation.depreclog.name, warning=3) def testLoggedEveryTime(self): """Logged everytime cause it is 3 different locations""" self.deprecatedOnlyOnce() self.deprecatedOnlyOnce() self.deprecatedOnlyOnce() - @utils.test_logging(deprecation.depreclog.name, warning=1) + @testutils.test_logging(deprecation.depreclog.name, warning=1) def testLoggedSingleTime(self): def log(): self.deprecatedOnlyOnce() @@ -76,18 +76,18 @@ class TestDeprecation(unittest.TestCase): log() log() - @utils.test_logging(deprecation.depreclog.name, warning=3) + @testutils.test_logging(deprecation.depreclog.name, warning=3) def testLoggedEveryTime2(self): self.deprecatedEveryTime() self.deprecatedEveryTime() self.deprecatedEveryTime() - @utils.test_logging(deprecation.depreclog.name, warning=1) + @testutils.test_logging(deprecation.depreclog.name, warning=1) def testWarning(self): deprecation.deprecated_warning(type_="t", name="n") def testBacktrace(self): - testLogging = utils.TestLogging(deprecation.depreclog.name) + testLogging = testutils.TestLogging(deprecation.depreclog.name) with testLogging: self.deprecatedEveryTime() message = testLogging.records[0].getMessage() diff --git a/silx/utils/test/test_launcher.py b/silx/utils/test/test_launcher.py index b3b6f98..87b7158 100644 --- a/silx/utils/test/test_launcher.py +++ b/silx/utils/test/test_launcher.py @@ -26,12 +26,12 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "03/04/2017" +__date__ = "17/01/2018" import sys import unittest -from silx.test.utils import ParametricTestCase +from silx.utils.testutils import ParametricTestCase from .. import launcher diff --git a/silx/utils/testutils.py b/silx/utils/testutils.py new file mode 100644 index 0000000..82c2ce3 --- /dev/null +++ b/silx/utils/testutils.py @@ -0,0 +1,281 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""Utilities for writing tests. + +- :class:`ParametricTestCase` provides a :meth:`TestCase.subTest` replacement + for Python < 3.4 +- :class:`TestLogging` with context or the :func:`test_logging` decorator + enables testing the number of logging messages of different levels. +""" + +__authors__ = ["T. Vincent"] +__license__ = "MIT" +__date__ = "26/01/2018" + + +import contextlib +import functools +import logging +import sys +import unittest + +_logger = logging.getLogger(__name__) + + +if sys.hexversion >= 0x030400F0: # Python >= 3.4 + class ParametricTestCase(unittest.TestCase): + pass +else: + class ParametricTestCase(unittest.TestCase): + """TestCase with subTest support for Python < 3.4. + + Add subTest method to support parametric tests. + API is the same, but behavior differs: + If a subTest fails, the following ones are not run. + """ + + _subtest_msg = None # Class attribute to provide a default value + + @contextlib.contextmanager + def subTest(self, msg=None, **params): + """Use as unittest.TestCase.subTest method in Python >= 3.4.""" + # Format arguments as: '[msg] (key=value, ...)' + param_str = ', '.join(['%s=%s' % (k, v) for k, v in params.items()]) + self._subtest_msg = '[%s] (%s)' % (msg or '', param_str) + yield + self._subtest_msg = None + + def shortDescription(self): + short_desc = super(ParametricTestCase, self).shortDescription() + if self._subtest_msg is not None: + # Append subTest message to shortDescription + short_desc = ' '.join( + [msg for msg in (short_desc, self._subtest_msg) if msg]) + + return short_desc if short_desc else None + + +def parameterize(test_case_class, *args, **kwargs): + """Create a suite containing all tests taken from the given + subclass, passing them the parameters. + + .. code-block:: python + + class TestParameterizedCase(unittest.TestCase): + def __init__(self, methodName='runTest', foo=None): + unittest.TestCase.__init__(self, methodName) + self.foo = foo + + def suite(): + testSuite = unittest.TestSuite() + testSuite.addTest(parameterize(TestParameterizedCase, foo=10)) + testSuite.addTest(parameterize(TestParameterizedCase, foo=50)) + return testSuite + """ + test_loader = unittest.TestLoader() + test_names = test_loader.getTestCaseNames(test_case_class) + suite = unittest.TestSuite() + for name in test_names: + suite.addTest(test_case_class(name, *args, **kwargs)) + return suite + + +class TestLogging(logging.Handler): + """Context checking the number of logging messages from a specified Logger. + + It disables propagation of logging message while running. + + This is meant to be used as a with statement, for example: + + >>> with TestLogging(logger, error=2, warning=0): + >>> pass # Run tests here expecting 2 ERROR and no WARNING from logger + ... + + :param logger: Name or instance of the logger to test. + (Default: root logger) + :type logger: str or :class:`logging.Logger` + :param int critical: Expected number of CRITICAL messages. + Default: Do not check. + :param int error: Expected number of ERROR messages. + Default: Do not check. + :param int warning: Expected number of WARNING messages. + Default: Do not check. + :param int info: Expected number of INFO messages. + Default: Do not check. + :param int debug: Expected number of DEBUG messages. + Default: Do not check. + :param int notset: Expected number of NOTSET messages. + Default: Do not check. + :raises RuntimeError: If the message counts are the expected ones. + """ + + def __init__(self, logger=None, critical=None, error=None, + warning=None, info=None, debug=None, notset=None): + if logger is None: + logger = logging.getLogger() + elif not isinstance(logger, logging.Logger): + logger = logging.getLogger(logger) + self.logger = logger + + self.records = [] + + self.count_by_level = { + logging.CRITICAL: critical, + logging.ERROR: error, + logging.WARNING: warning, + logging.INFO: info, + logging.DEBUG: debug, + logging.NOTSET: notset + } + + super(TestLogging, self).__init__() + + def __enter__(self): + """Context (i.e., with) support""" + self.records = [] # Reset recorded LogRecords + self.logger.addHandler(self) + self.logger.propagate = False + # ensure no log message is ignored + self.entry_level = self.logger.level * 1 + self.logger.setLevel(logging.DEBUG) + + def __exit__(self, exc_type, exc_value, traceback): + """Context (i.e., with) support""" + self.logger.removeHandler(self) + self.logger.propagate = True + self.logger.setLevel(self.entry_level) + + for level, expected_count in self.count_by_level.items(): + if expected_count is None: + continue + + # Number of records for the specified level_str + count = len([r for r in self.records if r.levelno == level]) + if count != expected_count: # That's an error + # Resend record logs through logger as they where masked + # to help debug + for record in self.records: + self.logger.handle(record) + raise RuntimeError( + 'Expected %d %s logging messages, got %d' % ( + expected_count, logging.getLevelName(level), count)) + + def emit(self, record): + """Override :meth:`logging.Handler.emit`""" + self.records.append(record) + + +def test_logging(logger=None, critical=None, error=None, + warning=None, info=None, debug=None, notset=None): + """Decorator checking number of logging messages. + + Propagation of logging messages is disabled by this decorator. + + In case the expected number of logging messages is not found, it raises + a RuntimeError. + + >>> class Test(unittest.TestCase): + ... @test_logging('module_logger_name', error=2, warning=0) + ... def test(self): + ... pass # Test expecting 2 ERROR and 0 WARNING messages + + :param logger: Name or instance of the logger to test. + (Default: root logger) + :type logger: str or :class:`logging.Logger` + :param int critical: Expected number of CRITICAL messages. + Default: Do not check. + :param int error: Expected number of ERROR messages. + Default: Do not check. + :param int warning: Expected number of WARNING messages. + Default: Do not check. + :param int info: Expected number of INFO messages. + Default: Do not check. + :param int debug: Expected number of DEBUG messages. + Default: Do not check. + :param int notset: Expected number of NOTSET messages. + Default: Do not check. + """ + def decorator(func): + test_context = TestLogging(logger, critical, error, + warning, info, debug, notset) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + with test_context: + result = func(*args, **kwargs) + return result + return wrapper + return decorator + + +# Simulate missing library context +class EnsureImportError(object): + """This context manager allows to simulate the unavailability + of a library, even if it is actually available. It ensures that + an ImportError is raised if the code inside the context tries to + import the module. + + It can be used to test that a correct fallback library is used, + or that the expected error code is returned. + + Trivial example:: + + from silx.utils.testutils import EnsureImportError + + with EnsureImportError("h5py"): + try: + import h5py + except ImportError: + print("Good") + + .. note:: + + This context manager does not remove the library from the namespace, + if it is already imported. It only ensures that any attempt to import + it again will cause an ImportError to be raised. + """ + def __init__(self, name): + """ + + :param str name: Name of module to be hidden (e.g. "h5py") + """ + self.module_name = name + + def __enter__(self): + """Simulate failed import by setting sys.modules[name]=None""" + if self.module_name not in sys.modules: + self._delete_on_exit = True + self._backup = None + else: + self._delete_on_exit = False + self._backup = sys.modules[self.module_name] + sys.modules[self.module_name] = None + + def __exit__(self, exc_type, exc_val, exc_tb): + """Restore previous state""" + if self._delete_on_exit: + del sys.modules[self.module_name] + else: + sys.modules[self.module_name] = self._backup -- cgit v1.2.3