summaryrefslogtreecommitdiff
path: root/silx/utils
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@debian.org>2018-03-04 10:20:27 +0100
committerPicca Frédéric-Emmanuel <picca@debian.org>2018-03-04 10:20:27 +0100
commit270d5ddc31c26b62379e3caa9044dd75ccc71847 (patch)
tree55c5bfc851dfce7172d335cd2405b214323e3caf /silx/utils
parente19c96eff0c310c06c4f268c8b80cb33bd08996f (diff)
New upstream version 0.7.0+dfsg
Diffstat (limited to 'silx/utils')
-rw-r--r--silx/utils/array_like.py6
-rw-r--r--silx/utils/deprecation.py8
-rw-r--r--silx/utils/exceptions.py33
-rw-r--r--silx/utils/launcher.py18
-rw-r--r--silx/utils/property.py52
-rw-r--r--silx/utils/test/test_deprecation.py18
-rw-r--r--silx/utils/test/test_launcher.py4
-rw-r--r--silx/utils/testutils.py281
8 files changed, 390 insertions, 30 deletions
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