diff options
Diffstat (limited to 'silx/utils')
-rw-r--r-- | silx/utils/__init__.py | 0 | ||||
-rw-r--r-- | silx/utils/array_like.py | 593 | ||||
-rw-r--r-- | silx/utils/decorators.py | 71 | ||||
-rw-r--r-- | silx/utils/html.py | 60 | ||||
-rw-r--r-- | silx/utils/launcher.py | 303 | ||||
-rw-r--r-- | silx/utils/setup.py | 43 | ||||
-rw-r--r-- | silx/utils/test/__init__.py | 43 | ||||
-rw-r--r-- | silx/utils/test/test_array_like.py | 453 | ||||
-rw-r--r-- | silx/utils/test/test_html.py | 61 | ||||
-rw-r--r-- | silx/utils/test/test_launcher.py | 204 | ||||
-rw-r--r-- | silx/utils/test/test_launcher_command.py | 47 | ||||
-rw-r--r-- | silx/utils/test/test_weakref.py | 330 | ||||
-rw-r--r-- | silx/utils/weakref.py | 361 |
13 files changed, 2569 insertions, 0 deletions
diff --git a/silx/utils/__init__.py b/silx/utils/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/silx/utils/__init__.py diff --git a/silx/utils/array_like.py b/silx/utils/array_like.py new file mode 100644 index 0000000..f4c85bf --- /dev/null +++ b/silx/utils/array_like.py @@ -0,0 +1,593 @@ +# 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. +# +# ###########################################################################*/ +"""Functions and classes for array-like objects, implementing common numpy +array features for datasets or nested sequences, while trying to avoid copying +data. + +Classes: + + - :class:`DatasetView`: Similar to a numpy view, to access + a h5py dataset as if it was transposed, without casting it into a + numpy array (this lets h5py handle reading the data from the + file into memory, as needed). + - :class:`ListOfImages`: Similar to a numpy view, to access + a list of 2D numpy arrays as if it was a 3D array (possibly transposed), + without casting it into a numpy array. + +Functions: + + - :func:`is_array` + - :func:`is_list_of_arrays` + - :func:`is_nested_sequence` + - :func:`get_shape` + - :func:`get_dtype` + - :func:`get_concatenated_dtype` + +""" + +from __future__ import absolute_import, print_function, division +import numpy +import sys +from silx.third_party import six + +__authors__ = ["P. Knobel"] +__license__ = "MIT" +__date__ = "26/04/2017" + + +def is_array(obj): + """Return True if object implements necessary attributes to be + considered similar to a numpy array. + + Attributes needed are "shape", "dtype", "__getitem__" + and "__array__". + + :param obj: Array-like object (numpy array, h5py dataset...) + :return: boolean + """ + # add more required attribute if necessary + for attr in ("shape", "dtype", "__array__", "__getitem__"): + if not hasattr(obj, attr): + return False + return True + + +def is_list_of_arrays(obj): + """Return True if object is a sequence of numpy arrays, + e.g. a list of images as 2D arrays. + + :param obj: list of arrays + :return: boolean""" + # object must not be a numpy array + if is_array(obj): + return False + + # object must have a __len__ method + if not hasattr(obj, "__len__"): + return False + + # all elements in sequence must be arrays + for arr in obj: + if not is_array(arr): + return False + + return True + + +def is_nested_sequence(obj): + """Return True if object is a nested sequence. + + A simple 1D sequence is considered to be a nested sequence. + + Numpy arrays and h5py datasets are not considered to be nested sequences. + + To test if an object is a nested sequence in a more general sense, + including arrays and datasets, use:: + + is_nested_sequence(obj) or is_array(obj) + + :param obj: nested sequence (numpy array, h5py dataset...) + :return: boolean""" + # object must not be a numpy array + if is_array(obj): + return False + + if not hasattr(obj, "__len__"): + return False + + # obj must not be a list of (lists of) numpy arrays + subsequence = obj + while hasattr(subsequence, "__len__"): + if is_array(subsequence): + return False + # strings cause infinite loops + if isinstance(subsequence, six.string_types + (six.binary_type, )): + return True + subsequence = subsequence[0] + + # object has __len__ and is not an array + return True + + +def get_shape(array_like): + """Return shape of an array like object. + + In case the object is a nested sequence but not an array or dataset + (list of lists, tuples...), the size of each dimension is assumed to be + uniform, and is deduced from the length of the first sequence. + + :param array_like: Array like object: numpy array, hdf5 dataset, + multi-dimensional sequence + :return: Shape of array, as a tuple of integers + """ + if hasattr(array_like, "shape"): + return array_like.shape + + shape = [] + subsequence = array_like + while hasattr(subsequence, "__len__"): + shape.append(len(subsequence)) + # strings cause infinite loops + if isinstance(subsequence, six.string_types + (six.binary_type, )): + break + subsequence = subsequence[0] + + return tuple(shape) + + +def get_dtype(array_like): + """Return dtype of an array like object. + + In the case of a nested sequence, the type of the first value + is inspected. + + :param array_like: Array like object: numpy array, hdf5 dataset, + multi-dimensional nested sequence + :return: numpy dtype of object + """ + if hasattr(array_like, "dtype"): + return array_like.dtype + + subsequence = array_like + while hasattr(subsequence, "__len__"): + # strings cause infinite loops + if isinstance(subsequence, six.string_types + (six.binary_type, )): + break + subsequence = subsequence[0] + + return numpy.dtype(type(subsequence)) + + +def get_concatenated_dtype(arrays): + """Return dtype of array resulting of concatenation + of a list of arrays (without actually concatenating + them). + + :param arrays: list of numpy arrays + :return: resulting dtype after concatenating arrays + """ + dtypes = {a.dtype for a in arrays} + dummy = [] + for dt in dtypes: + dummy.append(numpy.zeros((1, 1), dtype=dt)) + return numpy.array(dummy).dtype + + +class ListOfImages(object): + """This class provides a way to access values and slices in a stack of + images stored as a list of 2D numpy arrays, without creating a 3D numpy + array first. + + A transposition can be specified, as a 3-tuple of dimensions in the wanted + order. For example, to transpose from ``xyz`` ``(0, 1, 2)`` into ``yzx``, + the transposition tuple is ``(1, 2, 0)`` + + All the 2D arrays in the list must have the same shape. + + The global dtype of the stack of images is the one that would be obtained + by casting the list of 2D arrays into a 3D numpy array. + + :param images: list of 2D numpy arrays, or :class:`ListOfImages` object + :param transposition: Tuple of dimension numbers in the wanted order + """ + def __init__(self, images, transposition=None): + """ + + """ + super(ListOfImages, self).__init__() + + # if images is a ListOfImages instance, get the underlying data + # as a list of 2D arrays + if isinstance(images, ListOfImages): + images = images.images + + # test stack of images is as expected + assert is_list_of_arrays(images), \ + "Image stack must be a list of arrays" + image0_shape = images[0].shape + for image in images: + assert image.ndim == 2, \ + "Images must be 2D numpy arrays" + assert image.shape == image0_shape, \ + "All images must have the same shape" + + self.images = images + """List of images""" + + self.shape = (len(images), ) + image0_shape + """Tuple of array dimensions""" + self.dtype = get_concatenated_dtype(images) + """Data-type of the global array""" + self.ndim = 3 + """Number of array dimensions""" + + self.size = len(images) * image0_shape[0] * image0_shape[1] + """Number of elements in the array.""" + + self.transposition = list(range(self.ndim)) + """List of dimension indices, in an order depending on the + specified transposition. By default this is simply + [0, ..., self.ndim], but it can be changed by specifying a different + ``transposition`` parameter at initialization. + + Use :meth:`transpose`, to create a new :class:`ListOfImages` + with a different :attr:`transposition`. + """ + + if transposition is not None: + assert len(transposition) == self.ndim + assert set(transposition) == set(list(range(self.ndim))), \ + "Transposition must be a sequence containing all dimensions" + self.transposition = transposition + self.__sort_shape() + + def __sort_shape(self): + """Sort shape in the order defined in :attr:`transposition` + """ + new_shape = tuple(self.shape[dim] for dim in self.transposition) + self.shape = new_shape + + def __sort_indices(self, indices): + """Return array indices sorted in the order needed + to access data in the original non-transposed images. + + :param indices: Tuple of ndim indices, in the order needed + to access the transposed view + :return: Sorted tuple of indices, to access original data + """ + assert len(indices) == self.ndim + sorted_indices = tuple(idx for (_, idx) in + sorted(zip(self.transposition, indices))) + return sorted_indices + + def __array__(self, dtype=None): + """Cast the images into a numpy array, and return it. + + If a transposition has been done on this images, return + a transposed view of a numpy array.""" + return numpy.transpose(numpy.array(self.images, dtype=dtype), + self.transposition) + + def __len__(self): + return self.shape[0] + + def transpose(self, transposition=None): + """Return a re-ordered (dimensions permutated) + :class:`ListOfImages`. + + 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 + wanted order. + If ``None`` (default), reverse the dimensions. + :return: new :class:`ListOfImages` object + """ + # by default, reverse the dimensions + if transposition is None: + transposition = list(reversed(self.transposition)) + + # If this ListOfImages is already transposed, sort new transposition + # relative to old transposition + elif list(self.transposition) != list(range(self.ndim)): + transposition = [self.transposition[i] for i in transposition] + + return ListOfImages(self.images, + transposition) + + @property + def T(self): + """ + Same as self.transpose() + + :return: DatasetView with dimensions reversed.""" + return self.transpose() + + def __getitem__(self, item): + """Handle a subset of numpy indexing with regards to the dimension + order as specified in :attr:`transposition` + + Following features are **not supported**: + + - fancy indexing using numpy arrays + - using ellipsis objects + + :param item: Index + :return: value or slice as a numpy array + """ + # 1-D slicing -> n-D slicing (n=1) + if not hasattr(item, "__len__"): + # first dimension index is given + item = [item] + # following dimensions are indexed with : (all elements) + item += [slice(None) for _i in range(self.ndim - 1)] + + # n-dimensional slicing + if len(item) != self.ndim: + raise IndexError( + "N-dim slicing requires a tuple of N indices/slices. " + + "Needed dimensions: %d" % self.ndim) + + # get list of indices sorted in the original images order + sorted_indices = self.__sort_indices(item) + list_idx, array_idx = sorted_indices[0], sorted_indices[1:] + + images_selection = self.images[list_idx] + + # now we must transpose the output data + output_dimensions = [] + frozen_dimensions = [] + for i, idx in enumerate(item): + # slices and sequences + if not isinstance(idx, int): + output_dimensions.append(self.transposition[i]) + # regular integer index + else: + # whenever a dimension is fixed (indexed by an integer) + # the number of output dimension is reduced + frozen_dimensions.append(self.transposition[i]) + + # decrement output dimensions that are above frozen dimensions + for frozen_dim in reversed(sorted(frozen_dimensions)): + for i, out_dim in enumerate(output_dimensions): + if out_dim > frozen_dim: + output_dimensions[i] -= 1 + + assert (len(output_dimensions) + len(frozen_dimensions)) == self.ndim + assert set(output_dimensions) == set(range(len(output_dimensions))) + + # single list elements selected + if isinstance(images_selection, numpy.ndarray): + return numpy.transpose(images_selection[array_idx], + axes=output_dimensions) + # muliple list elements selected + else: + # apply selection first + output_stack = [] + for img in images_selection: + output_stack.append(img[array_idx]) + # then cast into a numpy array, and transpose + return numpy.transpose(numpy.array(output_stack), + axes=output_dimensions) + + def min(self): + """ + :return: Global minimum value + """ + min_value = self.images[0].min() + if len(self.images) > 1: + for img in self.images[1:]: + min_value = min(min_value, img.min()) + return min_value + + def max(self): + """ + :return: Global maximum value + """ + max_value = self.images[0].max() + if len(self.images) > 1: + for img in self.images[1:]: + max_value = max(max_value, img.max()) + return max_value + + +class DatasetView(object): + """This class provides a way to transpose a dataset without + casting it into a numpy array. This way, the dataset in a file need not + necessarily be integrally read into memory to view it in a different + transposition. + + .. note:: + The performances depend a lot on the way the dataset was written + to file. Depending on the chunking strategy, reading a complete 2D slice + in an unfavorable direction may still require the entire dataset to + be read from disk. + + :param dataset: h5py dataset + :param transposition: List of dimensions sorted in the order of + transposition (relative to the original h5py dataset) + """ + def __init__(self, dataset, transposition=None): + """ + + """ + super(DatasetView, self).__init__() + self.dataset = dataset + """original dataset""" + + self.shape = dataset.shape + """Tuple of array dimensions""" + self.dtype = dataset.dtype + """Data-type of the array’s element""" + self.ndim = len(dataset.shape) + """Number of array dimensions""" + + size = 0 + if self.ndim: + size = 1 + for dimsize in self.shape: + size *= dimsize + self.size = size + """Number of elements in the array.""" + + self.transposition = list(range(self.ndim)) + """List of dimension indices, in an order depending on the + specified transposition. By default this is simply + [0, ..., self.ndim], but it can be changed by specifying a different + `transposition` parameter at initialization. + + Use :meth:`transpose`, to create a new :class:`DatasetView` + with a different :attr:`transposition`. + """ + + if transposition is not None: + assert len(transposition) == self.ndim + assert set(transposition) == set(list(range(self.ndim))), \ + "Transposition must be a list containing all dimensions" + self.transposition = transposition + self.__sort_shape() + + def __sort_shape(self): + """Sort shape in the order defined in :attr:`transposition` + """ + new_shape = tuple(self.shape[dim] for dim in self.transposition) + self.shape = new_shape + + def __sort_indices(self, indices): + """Return array indices sorted in the order needed + to access data in the original non-transposed dataset. + + :param indices: Tuple of ndim indices, in the order needed + to access the view + :return: Sorted tuple of indices, to access original data + """ + assert len(indices) == self.ndim + sorted_indices = tuple(idx for (_, idx) in + sorted(zip(self.transposition, indices))) + return sorted_indices + + def __getitem__(self, item): + """Handle fancy indexing with regards to the dimension order as + specified in :attr:`transposition` + + The supported fancy-indexing syntax is explained at + http://docs.h5py.org/en/latest/high/dataset.html#fancy-indexing. + + Additional restrictions exist if the data has been transposed: + + - numpy boolean array indexing is not supported + - ellipsis objects are not supported + + :param item: Index, possibly fancy index (must be supported by h5py) + :return: Sliced numpy array or numpy scalar + """ + # no transposition, let the original dataset handle indexing + if self.transposition == list(range(self.ndim)): + return self.dataset[item] + + # 1-D slicing: create a list of indices to switch to n-D slicing + if not hasattr(item, "__len__"): + # first dimension index (list index) is given + item = [item] + # following dimensions are indexed with slices representing all elements + item += [slice(None) for _i in range(self.ndim - 1)] + + # n-dimensional slicing + if len(item) != self.ndim: + raise IndexError( + "N-dim slicing requires a tuple of N indices/slices. " + + "Needed dimensions: %d" % self.ndim) + + # get list of indices sorted in the original dataset order + sorted_indices = self.__sort_indices(item) + + output_data_not_transposed = self.dataset[sorted_indices] + + # now we must transpose the output data + output_dimensions = [] + frozen_dimensions = [] + for i, idx in enumerate(item): + # slices and sequences + if not isinstance(idx, int): + output_dimensions.append(self.transposition[i]) + # regular integer index + else: + # whenever a dimension is fixed (indexed by an integer) + # the number of output dimension is reduced + frozen_dimensions.append(self.transposition[i]) + + # decrement output dimensions that are above frozen dimensions + for frozen_dim in reversed(sorted(frozen_dimensions)): + for i, out_dim in enumerate(output_dimensions): + if out_dim > frozen_dim: + output_dimensions[i] -= 1 + + assert (len(output_dimensions) + len(frozen_dimensions)) == self.ndim + assert set(output_dimensions) == set(range(len(output_dimensions))) + + return numpy.transpose(output_data_not_transposed, + axes=output_dimensions) + + def __array__(self, dtype=None): + """Cast the dataset into a numpy array, and return it. + + If a transposition has been done on this dataset, return + a transposed view of a numpy array.""" + return numpy.transpose(numpy.array(self.dataset, dtype=dtype), + self.transposition) + + def __len__(self): + return self.shape[0] + + def transpose(self, transposition=None): + """Return a re-ordered (dimensions permutated) + :class:`DatasetView`. + + 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. + If ``None`` (default), reverse the dimensions. + :return: Transposed DatasetView + """ + # by default, reverse the dimensions + if transposition is None: + transposition = list(reversed(self.transposition)) + + # If this DatasetView is already transposed, sort new transposition + # relative to old transposition + elif list(self.transposition) != list(range(self.ndim)): + transposition = [self.transposition[i] for i in transposition] + + return DatasetView(self.dataset, + transposition) + + @property + def T(self): + """ + Same as self.transpose() + + :return: DatasetView with dimensions reversed.""" + return self.transpose() diff --git a/silx/utils/decorators.py b/silx/utils/decorators.py new file mode 100644 index 0000000..ff70e38 --- /dev/null +++ b/silx/utils/decorators.py @@ -0,0 +1,71 @@ +# 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__ = ["Jerome Kieffer"] +__license__ = "MIT" +__date__ = "01/03/2017" + +import os +import sys +import traceback +import logging +import functools + + +depreclog = logging.getLogger("DEPRECATION") + + +def deprecated(func=None, reason=None, replacement=None, since_version=None): + """ + Decorator that deprecates the use of a function + + :param str reason: Reason for deprecating this function + (e.g. "feature no longer provided", + :param str replacement: Name of replacement function (if the reason for + deprecating was to rename the function) + :param str since_version: First *silx* version for which the function was + deprecated (e.g. "0.5.0"). + """ + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + name = func.func_name if sys.version_info[0] < 3 else func.__name__ + msg = "%s is deprecated" + if since_version is not None: + msg += " since silx version %s" % since_version + msg += "!" + if reason is not None: + msg += " Reason: %s." % reason + if replacement is not None: + msg += " Use '%s' instead." % replacement + depreclog.warning(msg + " %s", name, os.linesep.join([""] + traceback.format_stack()[:-1])) + return func(*args, **kwargs) + return wrapper + if func is not None: + return decorator(func) + return decorator diff --git a/silx/utils/html.py b/silx/utils/html.py new file mode 100644 index 0000000..aab25f2 --- /dev/null +++ b/silx/utils/html.py @@ -0,0 +1,60 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +"""Utils function relative to HTML +""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "19/09/2016" + + +def escape(string, quote=True): + """Returns a string where HTML metacharacters are properly escaped. + + Compatibility layer to avoid incompatibilities between Python versions, + Qt versions and Qt bindings. + + >>> import silx.utils.html + >>> silx.utils.html.escape("<html>") + >>> "<html>" + + .. note:: Since Python 3.3 you can use the `html` module. For previous + version, it is provided by `sgi` module. + .. note:: Qt4 provides it with `Qt.escape` while Qt5 provide it with + `QString.toHtmlEscaped`. But `QString` is not exposed by `PyQt` or + `PySide`. + + :param str string: Human readable string. + :param bool quote: Escape quote and double quotes (default behaviour). + :returns: Valid HTML syntax to display the input string. + :rtype: str + """ + string = string.replace("&", "&") # must be done first + string = string.replace("<", "<") + string = string.replace(">", ">") + if quote: + string = string.replace("'", "'") + string = string.replace("\"", """) + return string diff --git a/silx/utils/launcher.py b/silx/utils/launcher.py new file mode 100644 index 0000000..8d2c81c --- /dev/null +++ b/silx/utils/launcher.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2004-2016 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. +# +# ###########################################################################*/ +"""This module define silx application available throug the silx launcher. +""" + +__authors__ = ["P. Knobel", "V. Valls"] +__license__ = "MIT" +__date__ = "03/04/2017" + + +import sys +import importlib +import contextlib +import argparse +import logging + + +_logger = logging.getLogger(__name__) + + +class LauncherCommand(): + """Description of a command""" + + def __init__(self, name, description=None, module_name=None, function=None): + """ + Constructor + + :param str name: Name of the command + :param str description: Description of the command + :param str module_name: Module name to execute + :param callable function: Python function to execute + """ + self.name = name + self.module_name = module_name + if description is None: + description = "A command" + self.description = description + self.function = function + + def get_module(self): + """Returns the python module to execute. If any. + + :rtype: module + """ + 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) + return None + + def get_function(self): + """Returns the main function to execute. + + :rtype: callable + """ + if self.function is not None: + return self.function + else: + module = self.get_module() + if module is None: + _logger.error("Imposible to load module name '%s'" % self.module_name) + return None + + # reach the 'main' function + if not hasattr(module, "main"): + raise TypeError("Module excpect to have a 'main' function") + else: + main = getattr(module, "main") + return main + + @contextlib.contextmanager + def get_env(self, argv): + """Fix the environement before and after executing the command. + + :param list argv: The list of arguments (the first one is the name of + the application and command) + :rtype: int + """ + + # fix the context + old_argv = sys.argv + sys.argv = argv + + try: + yield + finally: + # clean up the context + sys.argv = old_argv + + def execute(self, argv): + """Execute the command. + + :param list[str] argv: The list of arguments (the first one is the + name of the application and command) + :rtype: int + :returns: The execution status + """ + with self.get_env(argv): + func = self.get_function() + if func is None: + _logger.error("Imposible to execute the command '%s'" % self.name) + return -1 + try: + status = func(argv) + except SystemExit as e: + # ArgumentParser likes to finish with a sys.exit + status = e.args[0] + return status + + +class Launcher(object): + """ + Manage launch of module. + + Provides an API to describe available commands and feature to display help + and execute the commands. + """ + + def __init__(self, + prog=None, + usage=None, + description=None, + epilog=None, + version=None): + """ + :param str prog: Name of the program. If it is not defined it uses the + first argument of `sys.argv` + :param str usage: Custom the string explaining the usage. Else it is + autogenerated. + :param str description: Description of the application displayed after the + usage. + :param str epilog: Custom the string displayed at the end of the help. + :param str version: Define the version of the application. + """ + if prog is None: + prog = sys.argv[0] + self.prog = prog + self.usage = usage + self.description = description + self.epilog = epilog + self.version = version + self._commands = {} + + help_command = LauncherCommand( + "help", + description="Show help of the following command", + function=self.execute_help) + self.add_command(command=help_command) + + def add_command(self, name=None, module_name=None, description=None, command=None): + """ + Add a command to the launcher. + + See also `LauncherCommand`. + + :param str name: Name of the command + :param str module_name: Module to execute + :param str description: Description of the command + :param LauncherCommand command: A `LauncherCommand` + """ + if command is not None: + assert(name is None and module_name is None and description is None) + else: + command = LauncherCommand( + name=name, + description=description, + module_name=module_name) + self._commands[command.name] = command + + def print_help(self): + """Print the help to stdout. + """ + usage = self.usage + if usage is None: + usage = "usage: {0.prog} [--version|--help] <command> [<args>]" + description = self.description + epilog = self.epilog + if epilog is None: + epilog = "See '{0.prog} help <command>' to read about a specific subcommand" + + print(usage.format(self)) + print("") + if description is not None: + print(description) + print("") + print("The {0.prog} commands are:".format(self)) + commands = sorted(self._commands.keys()) + for command in commands: + command = self._commands[command] + print(" {:10s} {:s}".format(command.name, command.description)) + print("") + print(epilog.format(self)) + + def execute_help(self, argv): + """Execute the help command. + + :param list[str] argv: The list of arguments (the first one is the + name of the application with the help command) + :rtype: int + :returns: The execution status + """ + description = "Display help information about %s" % self.prog + parser = argparse.ArgumentParser(description=description) + parser.add_argument( + 'command', + default=None, + nargs=argparse.OPTIONAL, + help='Command in which aving help') + + try: + options = parser.parse_args(argv[1:]) + except SystemExit as e: + # ArgumentParser likes to finish with a sys.exit + return e.args[0] + + command_name = options.command + if command_name is None: + self.print_help() + return 0 + + if command_name not in self._commands: + print("Unknown command: %s", command_name) + self.print_help() + return -1 + + command = self._commands[command_name] + prog = "%s %s" % (self.prog, command.name) + return command.execute([prog, "--help"]) + + def execute(self, argv=None): + """Execute the launcher. + + :param list[str] argv: The list of arguments (the first one is the + name of the application) + :rtype: int + :returns: The execution status + """ + if argv is None: + argv = sys.argv + + if len(argv) <= 1: + self.print_help() + return 0 + + command_name = argv[1] + + if command_name == "--version": + print("%s version %s" % (self.prog, str(self.version))) + return 0 + + if command_name in ["--help", "-h"]: + # Special help command + if len(argv) == 2: + self.print_help() + return 0 + else: + command_name = argv[2] + command_argv = argv[2:] + ["--help"] + command_argv[0] = "%s %s" % (self.prog, command_argv[0]) + else: + command_argv = argv[1:] + command_argv[0] = "%s %s" % (self.prog, command_argv[0]) + + if command_name not in self._commands: + print("Unknown command: %s" % command_name) + self.print_help() + return -1 + + command = self._commands[command_name] + return command.execute(command_argv) diff --git a/silx/utils/setup.py b/silx/utils/setup.py new file mode 100644 index 0000000..1f3e09a --- /dev/null +++ b/silx/utils/setup.py @@ -0,0 +1,43 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "24/08/2016" + + +from numpy.distutils.misc_util import Configuration + + +def configuration(parent_package='', top_path=None): + config = Configuration('utils', parent_package, top_path) + config.add_subpackage('test') + + return config + + +if __name__ == "__main__": + from numpy.distutils.core import setup + + setup(configuration=configuration) diff --git a/silx/utils/test/__init__.py b/silx/utils/test/__init__.py new file mode 100644 index 0000000..68e7412 --- /dev/null +++ b/silx/utils/test/__init__.py @@ -0,0 +1,43 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +__authors__ = ["T. Vincent", "P. Knobel"] +__license__ = "MIT" +__date__ = "03/04/2017" + + +import unittest +from .test_weakref import suite as test_weakref_suite +from .test_html import suite as test_html_suite +from .test_array_like import suite as test_array_like_suite +from .test_launcher import suite as test_launcher_suite + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest(test_weakref_suite()) + test_suite.addTest(test_html_suite()) + test_suite.addTest(test_array_like_suite()) + test_suite.addTest(test_launcher_suite()) + return test_suite diff --git a/silx/utils/test/test_array_like.py b/silx/utils/test/test_array_like.py new file mode 100644 index 0000000..7cd004c --- /dev/null +++ b/silx/utils/test/test_array_like.py @@ -0,0 +1,453 @@ +# 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. +# +# ###########################################################################*/ +"""Tests for array_like module""" + +__authors__ = ["P. Knobel"] +__license__ = "MIT" +__date__ = "09/01/2017" + +try: + import h5py +except ImportError: + h5py = None + +import numpy +import os +import tempfile +import unittest + +from ..array_like import DatasetView, ListOfImages +from ..array_like import get_dtype, get_concatenated_dtype, get_shape,\ + is_array, is_nested_sequence, is_list_of_arrays + + +@unittest.skipIf(h5py is None, + "h5py is needed to test DatasetView") +class TestTransposedDatasetView(unittest.TestCase): + + def setUp(self): + # dataset attributes + self.ndim = 3 + self.original_shape = (5, 10, 20) + self.size = 1 + for dim in self.original_shape: + self.size *= dim + + self.volume = numpy.arange(self.size).reshape(self.original_shape) + + self.tempdir = tempfile.mkdtemp() + self.h5_fname = os.path.join(self.tempdir, "tempfile.h5") + with h5py.File(self.h5_fname, "w") as f: + f["volume"] = self.volume + + self.h5f = h5py.File(self.h5_fname, "r") + + self.all_permutations = [(0, 1, 2), (0, 2, 1), (1, 0, 2), (1, 2, 0), + (2, 0, 1), (2, 1, 0)] + + def tearDown(self): + self.h5f.close() + os.unlink(self.h5_fname) + os.rmdir(self.tempdir) + + def _testSize(self, obj): + """These assertions apply to all following test cases""" + self.assertEqual(obj.ndim, self.ndim) + self.assertEqual(obj.size, self.size) + size_from_shape = 1 + for dim in obj.shape: + size_from_shape *= dim + self.assertEqual(size_from_shape, self.size) + + for dim in self.original_shape: + self.assertIn(dim, obj.shape) + + def testNoTransposition(self): + """no transposition (transposition = (0, 1, 2))""" + a = DatasetView(self.h5f["volume"]) + + self.assertEqual(a.shape, self.original_shape) + self._testSize(a) + + # reversing the dimensions twice results in no change + rtrans = list(reversed(range(self.ndim))) + self.assertTrue(numpy.array_equal( + a, + a.transpose(rtrans).transpose(rtrans))) + + for i in range(a.shape[0]): + for j in range(a.shape[1]): + for k in range(a.shape[2]): + self.assertEqual(self.h5f["volume"][i, j, k], + a[i, j, k]) + + def _testTransposition(self, transposition): + """test transposed dataset + + :param tuple transposition: List of dimensions (0... n-1) sorted + in the desired order + """ + a = DatasetView(self.h5f["volume"], + transposition=transposition) + self._testSize(a) + + # sort shape of transposed object, to hopefully find the original shape + sorted_shape = tuple(dim_size for (_, dim_size) in + sorted(zip(transposition, a.shape))) + self.assertEqual(sorted_shape, self.original_shape) + + a_as_array = numpy.array(self.h5f["volume"]).transpose(transposition) + + # test the __array__ method + self.assertTrue(numpy.array_equal( + numpy.array(a), + a_as_array)) + + # test slicing + for selection in [(2, slice(None), slice(None)), + (slice(None), 1, slice(0, 8)), + (slice(0, 3), slice(None), 3), + (1, 3, slice(None)), + (slice(None), 2, 1), + (4, slice(1, 9, 2), 2)]: + self.assertIsInstance(a[selection], numpy.ndarray) + self.assertTrue(numpy.array_equal( + a[selection], + a_as_array[selection])) + + # test the DatasetView.__getitem__ for single values + # (step adjusted to test at least 3 indices in each dimension) + for i in range(0, a.shape[0], a.shape[0] // 3): + for j in range(0, a.shape[1], a.shape[1] // 3): + for k in range(0, a.shape[2], a.shape[2] // 3): + sorted_indices = tuple(idx for (_, idx) in + sorted(zip(transposition, [i, j, k]))) + viewed_value = a[i, j, k] + corresponding_original_value = self.h5f["volume"][sorted_indices] + self.assertEqual(viewed_value, + corresponding_original_value) + + # reversing the dimensions twice results in no change + rtrans = list(reversed(range(self.ndim))) + self.assertTrue(numpy.array_equal( + a, + a.transpose(rtrans).transpose(rtrans))) + + # test .T property + self.assertTrue(numpy.array_equal( + a.T, + a.transpose(rtrans))) + + def testTransposition012(self): + """transposition = (0, 1, 2) + (should be the same as testNoTransposition)""" + self._testTransposition((0, 1, 2)) + + def testTransposition021(self): + """transposition = (0, 2, 1)""" + self._testTransposition((0, 2, 1)) + + def testTransposition102(self): + """transposition = (1, 0, 2)""" + self._testTransposition((1, 0, 2)) + + def testTransposition120(self): + """transposition = (1, 2, 0)""" + self._testTransposition((1, 2, 0)) + + def testTransposition201(self): + """transposition = (2, 0, 1)""" + self._testTransposition((2, 0, 1)) + + def testTransposition210(self): + """transposition = (2, 1, 0)""" + self._testTransposition((2, 1, 0)) + + def testAllDoubleTranspositions(self): + for trans1 in self.all_permutations: + for trans2 in self.all_permutations: + self._testDoubleTransposition(trans1, trans2) + + def _testDoubleTransposition(self, transposition1, transposition2): + a = DatasetView(self.h5f["volume"], + transposition=transposition1).transpose(transposition2) + + b = self.volume.transpose(transposition1).transpose(transposition2) + + self.assertTrue(numpy.array_equal(a, b), + "failed with double transposition %s %s" % (transposition1, transposition2)) + + def test1DIndex(self): + a = DatasetView(self.h5f["volume"]) + self.assertTrue(numpy.array_equal(self.volume[1], + a[1])) + + b = DatasetView(self.h5f["volume"], transposition=(1, 0, 2)) + self.assertTrue(numpy.array_equal(self.volume[:, 1, :], + b[1])) + + +class TestTransposedListOfImages(unittest.TestCase): + def setUp(self): + # images attributes + self.ndim = 3 + self.original_shape = (5, 10, 20) + self.size = 1 + for dim in self.original_shape: + self.size *= dim + + volume = numpy.arange(self.size).reshape(self.original_shape) + + self.images = [] + for i in range(self.original_shape[0]): + self.images.append( + volume[i]) + + self.images_as_3D_array = numpy.array(self.images) + + self.all_permutations = [(0, 1, 2), (0, 2, 1), (1, 0, 2), (1, 2, 0), + (2, 0, 1), (2, 1, 0)] + + def tearDown(self): + pass + + def _testSize(self, obj): + """These assertions apply to all following test cases""" + self.assertEqual(obj.ndim, self.ndim) + self.assertEqual(obj.size, self.size) + size_from_shape = 1 + for dim in obj.shape: + size_from_shape *= dim + self.assertEqual(size_from_shape, self.size) + + for dim in self.original_shape: + self.assertIn(dim, obj.shape) + + def testNoTransposition(self): + """no transposition (transposition = (0, 1, 2))""" + a = ListOfImages(self.images) + + self.assertEqual(a.shape, self.original_shape) + self._testSize(a) + + for i in range(a.shape[0]): + for j in range(a.shape[1]): + for k in range(a.shape[2]): + self.assertEqual(self.images[i][j, k], + a[i, j, k]) + + # reversing the dimensions twice results in no change + rtrans = list(reversed(range(self.ndim))) + self.assertTrue(numpy.array_equal( + a, + a.transpose(rtrans).transpose(rtrans))) + + # test .T property + self.assertTrue(numpy.array_equal( + a.T, + a.transpose(rtrans))) + + def _testTransposition(self, transposition): + """test transposed dataset + + :param tuple transposition: List of dimensions (0... n-1) sorted + in the desired order + """ + a = ListOfImages(self.images, + transposition=transposition) + self._testSize(a) + + # sort shape of transposed object, to hopefully find the original shape + sorted_shape = tuple(dim_size for (_, dim_size) in + sorted(zip(transposition, a.shape))) + self.assertEqual(sorted_shape, self.original_shape) + + a_as_array = numpy.array(self.images).transpose(transposition) + + # test the DatasetView.__array__ method + self.assertTrue(numpy.array_equal( + numpy.array(a), + a_as_array)) + + # test slicing + for selection in [(2, slice(None), slice(None)), + (slice(None), 1, slice(0, 8)), + (slice(0, 3), slice(None), 3), + (1, 3, slice(None)), + (slice(None), 2, 1), + (4, slice(1, 9, 2), 2)]: + self.assertIsInstance(a[selection], numpy.ndarray) + self.assertTrue(numpy.array_equal( + a[selection], + a_as_array[selection])) + + # test the DatasetView.__getitem__ for single values + # (step adjusted to test at least 3 indices in each dimension) + for i in range(0, a.shape[0], a.shape[0] // 3): + for j in range(0, a.shape[1], a.shape[1] // 3): + for k in range(0, a.shape[2], a.shape[2] // 3): + viewed_value = a[i, j, k] + sorted_indices = tuple(idx for (_, idx) in + sorted(zip(transposition, [i, j, k]))) + corresponding_original_value = self.images[sorted_indices[0]][sorted_indices[1:]] + self.assertEqual(viewed_value, + corresponding_original_value) + + # reversing the dimensions twice results in no change + rtrans = list(reversed(range(self.ndim))) + self.assertTrue(numpy.array_equal( + a, + a.transpose(rtrans).transpose(rtrans))) + + # test .T property + self.assertTrue(numpy.array_equal( + a.T, + a.transpose(rtrans))) + + def _testDoubleTransposition(self, transposition1, transposition2): + a = ListOfImages(self.images, + transposition=transposition1).transpose(transposition2) + + b = self.images_as_3D_array.transpose(transposition1).transpose(transposition2) + + self.assertTrue(numpy.array_equal(a, b), + "failed with double transposition %s %s" % (transposition1, transposition2)) + + def testTransposition012(self): + """transposition = (0, 1, 2) + (should be the same as testNoTransposition)""" + self._testTransposition((0, 1, 2)) + + def testTransposition021(self): + """transposition = (0, 2, 1)""" + self._testTransposition((0, 2, 1)) + + def testTransposition102(self): + """transposition = (1, 0, 2)""" + self._testTransposition((1, 0, 2)) + + def testTransposition120(self): + """transposition = (1, 2, 0)""" + self._testTransposition((1, 2, 0)) + + def testTransposition201(self): + """transposition = (2, 0, 1)""" + self._testTransposition((2, 0, 1)) + + def testTransposition210(self): + """transposition = (2, 1, 0)""" + self._testTransposition((2, 1, 0)) + + def testAllDoubleTranspositions(self): + for trans1 in self.all_permutations: + for trans2 in self.all_permutations: + self._testDoubleTransposition(trans1, trans2) + + def test1DIndex(self): + a = ListOfImages(self.images) + self.assertTrue(numpy.array_equal(self.images[1], + a[1])) + + b = ListOfImages(self.images, transposition=(1, 0, 2)) + self.assertTrue(numpy.array_equal(self.images_as_3D_array[:, 1, :], + b[1])) + + +class TestFunctions(unittest.TestCase): + """Test functions to guess the dtype and shape of an array_like + object""" + def testListOfLists(self): + l = [[0, 1, 2], [2, 3, 4]] + self.assertEqual(get_dtype(l), + numpy.dtype(int)) + self.assertEqual(get_shape(l), + (2, 3)) + self.assertTrue(is_nested_sequence(l)) + self.assertFalse(is_array(l)) + self.assertFalse(is_list_of_arrays(l)) + + l = [[0., 1.], [2., 3.]] + self.assertEqual(get_dtype(l), + numpy.dtype(float)) + self.assertEqual(get_shape(l), + (2, 2)) + self.assertTrue(is_nested_sequence(l)) + self.assertFalse(is_array(l)) + self.assertFalse(is_list_of_arrays(l)) + + # concatenated dtype of int and float + l = [numpy.array([[0, 1, 2], [2, 3, 4]]), + numpy.array([[0., 1., 2.], [2., 3., 4.]])] + + self.assertEqual(get_concatenated_dtype(l), + numpy.array(l).dtype) + self.assertEqual(get_shape(l), + (2, 2, 3)) + self.assertFalse(is_nested_sequence(l)) + self.assertFalse(is_array(l)) + self.assertTrue(is_list_of_arrays(l)) + + def testNumpyArray(self): + a = numpy.array([[0, 1], [2, 3]]) + self.assertEqual(get_dtype(a), + a.dtype) + self.assertFalse(is_nested_sequence(a)) + self.assertTrue(is_array(a)) + self.assertFalse(is_list_of_arrays(a)) + + @unittest.skipIf(h5py is None, + "h5py is needed for this test") + def testH5pyDataset(self): + a = numpy.array([[0, 1], [2, 3]]) + + tempdir = tempfile.mkdtemp() + h5_fname = os.path.join(tempdir, "tempfile.h5") + with h5py.File(h5_fname, "w") as h5f: + h5f["dataset"] = a + d = h5f["dataset"] + + self.assertEqual(get_dtype(d), + numpy.dtype(int)) + self.assertFalse(is_nested_sequence(d)) + self.assertTrue(is_array(d)) + self.assertFalse(is_list_of_arrays(d)) + + os.unlink(h5_fname) + os.rmdir(tempdir) + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestTransposedDatasetView)) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestTransposedListOfImages)) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestFunctions)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/utils/test/test_html.py b/silx/utils/test/test_html.py new file mode 100644 index 0000000..2d0387b --- /dev/null +++ b/silx/utils/test/test_html.py @@ -0,0 +1,61 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +"""Tests for html module""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "19/09/2016" + + +import unittest +from .. import html + + +class TestHtml(unittest.TestCase): + """Tests for html module.""" + + def testLtGt(self): + result = html.escape("<html>'\"") + self.assertEquals("<html>'"", result) + + def testLtAmpGt(self): + # '&' have to be escaped first + result = html.escape("<&>") + self.assertEquals("<&>", result) + + def testNoQuotes(self): + result = html.escape("\"m&m's\"", quote=False) + self.assertEquals("\"m&m's\"", result) + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestHtml)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/utils/test/test_launcher.py b/silx/utils/test/test_launcher.py new file mode 100644 index 0000000..b3b6f98 --- /dev/null +++ b/silx/utils/test/test_launcher.py @@ -0,0 +1,204 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +"""Tests for html module""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "03/04/2017" + + +import sys +import unittest +from silx.test.utils import ParametricTestCase +from .. import launcher + + +class CallbackMock(): + + def __init__(self, result=None): + self._execute_count = 0 + self._execute_argv = None + self._result = result + + def execute(self, argv): + self._execute_count = self._execute_count + 1 + self._execute_argv = argv + return self._result + + def __call__(self, argv): + return self.execute(argv) + + +class TestLauncherCommand(unittest.TestCase): + """Tests for launcher class.""" + + def testEnv(self): + command = launcher.LauncherCommand("foo") + old = sys.argv + params = ["foo", "bar"] + with command.get_env(params): + self.assertEqual(params, sys.argv) + self.assertEqual(sys.argv, old) + + def testEnvWhileException(self): + command = launcher.LauncherCommand("foo") + old = sys.argv + params = ["foo", "bar"] + try: + with command.get_env(params): + raise RuntimeError() + except RuntimeError: + pass + self.assertEqual(sys.argv, old) + + def testExecute(self): + params = ["foo", "bar"] + callback = CallbackMock(result=42) + command = launcher.LauncherCommand("foo", function=callback) + status = command.execute(params) + self.assertEqual(callback._execute_count, 1) + self.assertEqual(callback._execute_argv, params) + self.assertEqual(status, 42) + + +class TestModuleCommand(ParametricTestCase): + + def setUp(self): + module_name = "silx.utils.test.test_launcher_command" + command = launcher.LauncherCommand("foo", module_name=module_name) + self.command = command + + def testHelp(self): + status = self.command.execute(["--help"]) + self.assertEqual(status, 0) + + def testException(self): + try: + self.command.execute(["exception"]) + self.fail() + except RuntimeError: + pass + + def testCall(self): + status = self.command.execute([]) + self.assertEqual(status, 0) + + def testError(self): + status = self.command.execute(["error"]) + self.assertEqual(status, -1) + + +class TestLauncher(ParametricTestCase): + """Tests for launcher class.""" + + def testCallCommand(self): + callback = CallbackMock(result=42) + runner = launcher.Launcher(prog="prog") + command = launcher.LauncherCommand("foo", function=callback) + runner.add_command(command=command) + status = runner.execute(["prog", "foo", "param1", "param2"]) + self.assertEquals(status, 42) + self.assertEquals(callback._execute_argv, ["prog foo", "param1", "param2"]) + self.assertEquals(callback._execute_count, 1) + + def testAddCommand(self): + runner = launcher.Launcher(prog="prog") + module_name = "silx.utils.test.test_launcher_command" + runner.add_command("foo", module_name=module_name) + status = runner.execute(["prog", "foo"]) + self.assertEquals(status, 0) + + def testCallHelpOnCommand(self): + callback = CallbackMock(result=42) + runner = launcher.Launcher(prog="prog") + command = launcher.LauncherCommand("foo", function=callback) + runner.add_command(command=command) + status = runner.execute(["prog", "--help", "foo"]) + self.assertEquals(status, 42) + self.assertEquals(callback._execute_argv, ["prog foo", "--help"]) + self.assertEquals(callback._execute_count, 1) + + def testCallHelpOnCommand2(self): + callback = CallbackMock(result=42) + runner = launcher.Launcher(prog="prog") + command = launcher.LauncherCommand("foo", function=callback) + runner.add_command(command=command) + status = runner.execute(["prog", "help", "foo"]) + self.assertEquals(status, 42) + self.assertEquals(callback._execute_argv, ["prog foo", "--help"]) + self.assertEquals(callback._execute_count, 1) + + def testCallHelpOnUnknownCommand(self): + callback = CallbackMock(result=42) + runner = launcher.Launcher(prog="prog") + command = launcher.LauncherCommand("foo", function=callback) + runner.add_command(command=command) + status = runner.execute(["prog", "help", "foo2"]) + self.assertEquals(status, -1) + + def testNotAvailableCommand(self): + callback = CallbackMock(result=42) + runner = launcher.Launcher(prog="prog") + command = launcher.LauncherCommand("foo", function=callback) + runner.add_command(command=command) + status = runner.execute(["prog", "foo2", "param1", "param2"]) + self.assertEquals(status, -1) + self.assertEquals(callback._execute_count, 0) + + def testCallHelp(self): + callback = CallbackMock(result=42) + runner = launcher.Launcher(prog="prog") + command = launcher.LauncherCommand("foo", function=callback) + runner.add_command(command=command) + status = runner.execute(["prog", "help"]) + self.assertEquals(status, 0) + self.assertEquals(callback._execute_count, 0) + + def testCommonCommands(self): + runner = launcher.Launcher() + tests = [ + ["prog"], + ["prog", "--help"], + ["prog", "--version"], + ["prog", "help", "--help"], + ["prog", "help", "help"], + ] + for arguments in tests: + with self.subTest(args=tests): + status = runner.execute(arguments) + self.assertEquals(status, 0) + + +def suite(): + loader = unittest.defaultTestLoader.loadTestsFromTestCase + test_suite = unittest.TestSuite() + test_suite.addTest(loader(TestLauncherCommand)) + test_suite.addTest(loader(TestLauncher)) + test_suite.addTest(loader(TestModuleCommand)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/utils/test/test_launcher_command.py b/silx/utils/test/test_launcher_command.py new file mode 100644 index 0000000..ccf4601 --- /dev/null +++ b/silx/utils/test/test_launcher_command.py @@ -0,0 +1,47 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +"""Tests for html module""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "03/04/2017" + + +import sys + + +def main(argv): + + if "--help" in argv: + # Common behaviour of ArgumentParser + sys.exit(0) + + if "exception" in argv: + raise RuntimeError("Simulated exception") + + if "error" in argv: + return -1 + + return 0 diff --git a/silx/utils/test/test_weakref.py b/silx/utils/test/test_weakref.py new file mode 100644 index 0000000..7175863 --- /dev/null +++ b/silx/utils/test/test_weakref.py @@ -0,0 +1,330 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +"""Tests for weakref module""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "15/09/2016" + + +import unittest +from .. import weakref + + +class Dummy(object): + """Dummy class to use it as geanie pig""" + def inc(self, a): + return a + 1 + + def __lt__(self, other): + return True + + +def dummy_inc(a): + """Dummy function to use it as geanie pig""" + return a + 1 + + +class TestWeakMethod(unittest.TestCase): + """Tests for weakref.WeakMethod""" + + def testMethod(self): + dummy = Dummy() + callable_ = weakref.WeakMethod(dummy.inc) + self.assertEquals(callable_()(10), 11) + + def testMethodWithDeadObject(self): + dummy = Dummy() + callable_ = weakref.WeakMethod(dummy.inc) + dummy = None + self.assertIsNone(callable_()) + + def testMethodWithDeadFunction(self): + dummy = Dummy() + dummy.inc2 = lambda self, a: a + 1 + callable_ = weakref.WeakMethod(dummy.inc2) + dummy.inc2 = None + self.assertIsNone(callable_()) + + def testFunction(self): + callable_ = weakref.WeakMethod(dummy_inc) + self.assertEquals(callable_()(10), 11) + + def testDeadFunction(self): + def inc(a): + return a + 1 + callable_ = weakref.WeakMethod(inc) + inc = None + self.assertIsNone(callable_()) + + def testLambda(self): + store = lambda a: a + 1 # noqa: E731 + callable_ = weakref.WeakMethod(store) + self.assertEquals(callable_()(10), 11) + + def testDeadLambda(self): + callable_ = weakref.WeakMethod(lambda a: a + 1) + self.assertIsNone(callable_()) + + def testCallbackOnDeadObject(self): + self.__count = 0 + + def callback(ref): + self.__count += 1 + self.assertIs(callable_, ref) + dummy = Dummy() + callable_ = weakref.WeakMethod(dummy.inc, callback) + dummy = None + self.assertEquals(self.__count, 1) + + def testCallbackOnDeadMethod(self): + self.__count = 0 + + def callback(ref): + self.__count += 1 + self.assertIs(callable_, ref) + dummy = Dummy() + dummy.inc2 = lambda self, a: a + 1 + callable_ = weakref.WeakMethod(dummy.inc2, callback) + dummy.inc2 = None + self.assertEquals(self.__count, 1) + + def testCallbackOnDeadFunction(self): + self.__count = 0 + + def callback(ref): + self.__count += 1 + self.assertIs(callable_, ref) + store = lambda a: a + 1 # noqa: E731 + callable_ = weakref.WeakMethod(store, callback) + store = None + self.assertEquals(self.__count, 1) + + def testEquals(self): + dummy = Dummy() + callable1 = weakref.WeakMethod(dummy.inc) + callable2 = weakref.WeakMethod(dummy.inc) + self.assertEquals(callable1, callable2) + + def testInSet(self): + callable_set = set([]) + dummy = Dummy() + callable_set.add(weakref.WeakMethod(dummy.inc)) + callable_ = weakref.WeakMethod(dummy.inc) + self.assertIn(callable_, callable_set) + + def testInDict(self): + callable_dict = {} + dummy = Dummy() + callable_dict[weakref.WeakMethod(dummy.inc)] = 10 + callable_ = weakref.WeakMethod(dummy.inc) + self.assertEquals(callable_dict.get(callable_), 10) + + +class TestWeakMethodProxy(unittest.TestCase): + + def testMethod(self): + dummy = Dummy() + callable_ = weakref.WeakMethodProxy(dummy.inc) + self.assertEquals(callable_(10), 11) + + def testMethodWithDeadObject(self): + dummy = Dummy() + method = weakref.WeakMethodProxy(dummy.inc) + dummy = None + self.assertRaises(ReferenceError, method, 9) + + +class TestWeakList(unittest.TestCase): + """Tests for weakref.WeakList""" + + def setUp(self): + self.list = weakref.WeakList() + self.object1 = Dummy() + self.object2 = Dummy() + self.list.append(self.object1) + self.list.append(self.object2) + + def testAppend(self): + obj = Dummy() + self.list.append(obj) + self.assertEquals(len(self.list), 3) + obj = None + self.assertEquals(len(self.list), 2) + + def testRemove(self): + self.list.remove(self.object1) + self.assertEquals(len(self.list), 1) + + def testPop(self): + obj = self.list.pop(0) + self.assertIs(obj, self.object1) + self.assertEquals(len(self.list), 1) + + def testGetItem(self): + self.assertIs(self.object1, self.list[0]) + + def testGetItemSlice(self): + objects = self.list[:] + self.assertEquals(len(objects), 2) + self.assertIs(self.object1, objects[0]) + self.assertIs(self.object2, objects[1]) + + def testIter(self): + obj_list = list(self.list) + self.assertEquals(len(obj_list), 2) + self.assertIs(self.object1, obj_list[0]) + + def testLen(self): + self.assertEquals(len(self.list), 2) + + def testSetItem(self): + obj = Dummy() + self.list[0] = obj + self.assertIsNot(self.object1, self.list[0]) + obj = None + self.assertEquals(len(self.list), 1) + + def testSetItemSlice(self): + obj = Dummy() + self.list[:] = [obj, obj] + self.assertEquals(len(self.list), 2) + self.assertIs(obj, self.list[0]) + self.assertIs(obj, self.list[1]) + obj = None + self.assertEquals(len(self.list), 0) + + def testDelItem(self): + del self.list[0] + self.assertEquals(len(self.list), 1) + self.assertIs(self.object2, self.list[0]) + + def testDelItemSlice(self): + del self.list[:] + self.assertEquals(len(self.list), 0) + + def testContains(self): + self.assertIn(self.object1, self.list) + + def testAdd(self): + others = [Dummy()] + l = self.list + others + self.assertIs(l[0], self.object1) + self.assertEquals(len(l), 3) + others = None + self.assertEquals(len(l), 2) + + def testExtend(self): + others = [Dummy()] + self.list.extend(others) + self.assertIs(self.list[0], self.object1) + self.assertEquals(len(self.list), 3) + others = None + self.assertEquals(len(self.list), 2) + + def testIadd(self): + others = [Dummy()] + self.list += others + self.assertIs(self.list[0], self.object1) + self.assertEquals(len(self.list), 3) + others = None + self.assertEquals(len(self.list), 2) + + def testMul(self): + l = self.list * 2 + self.assertIs(l[0], self.object1) + self.assertEquals(len(l), 4) + self.object1 = None + self.assertEquals(len(l), 2) + self.assertIs(l[0], self.object2) + self.assertIs(l[1], self.object2) + + def testImul(self): + self.list *= 2 + self.assertIs(self.list[0], self.object1) + self.assertEquals(len(self.list), 4) + self.object1 = None + self.assertEquals(len(self.list), 2) + self.assertIs(self.list[0], self.object2) + self.assertIs(self.list[1], self.object2) + + def testCount(self): + self.list.append(self.object2) + self.assertEquals(self.list.count(self.object1), 1) + self.assertEquals(self.list.count(self.object2), 2) + + def testIndex(self): + self.assertEquals(self.list.index(self.object1), 0) + self.assertEquals(self.list.index(self.object2), 1) + + def testInsert(self): + obj = Dummy() + self.list.insert(1, obj) + self.assertEquals(len(self.list), 3) + self.assertIs(self.list[1], obj) + obj = None + self.assertEquals(len(self.list), 2) + + def testReverse(self): + self.list.reverse() + self.assertEquals(len(self.list), 2) + self.assertIs(self.list[0], self.object2) + self.assertIs(self.list[1], self.object1) + + def testReverted(self): + new_list = reversed(self.list) + self.assertEquals(len(new_list), 2) + self.assertIs(self.list[1], self.object2) + self.assertIs(self.list[0], self.object1) + self.assertIs(new_list[0], self.object2) + self.assertIs(new_list[1], self.object1) + self.object1 = None + self.assertEquals(len(new_list), 1) + + def testStr(self): + self.assertNotEquals(self.list.__str__(), "[]") + + def testRepr(self): + self.assertNotEquals(self.list.__repr__(), "[]") + + def testSort(self): + # only a coverage + self.list.sort() + self.assertEquals(len(self.list), 2) + + +def suite(): + test_suite = unittest.TestSuite() + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestWeakMethod)) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestWeakMethodProxy)) + test_suite.addTest( + unittest.defaultTestLoader.loadTestsFromTestCase(TestWeakList)) + return test_suite + + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/silx/utils/weakref.py b/silx/utils/weakref.py new file mode 100644 index 0000000..42d7392 --- /dev/null +++ b/silx/utils/weakref.py @@ -0,0 +1,361 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +"""Weakref utils for compatibility between Python 2 and Python 3 or for +extended features. +""" +from __future__ import absolute_import + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "15/09/2016" + + +import weakref +import types +import inspect + + +def ref(object, callback=None): + """Returns a weak reference to object. The original object can be retrieved + by calling the reference object if the referent is still alive. If the + referent is no longer alive, calling the reference object will cause None + to be returned. + + The signature is the same as the standard `weakref` library, but it returns + `WeakMethod` if the object is a bound method. + + :param object: An object + :param func callback: If provided, and the returned weakref object is + still alive, the callback will be called when the object is about to + be finalized. The weak reference object will be passed as the only + parameter to the callback. Then the referent will no longer be + available. + :return: A weak reference to the object + """ + if inspect.ismethod(object): + return WeakMethod(object, callback) + else: + return weakref.ref(object, callback) + + +def proxy(object, callback=None): + """Return a proxy to object which uses a weak reference. This supports use + of the proxy in most contexts instead of requiring the explicit + dereferencing used with weak reference objects. + + The signature is the same as the standard `weakref` library, but it returns + `WeakMethodProxy` if the object is a bound method. + + :param object: An object + :param func callback: If provided, and the returned weakref object is + still alive, the callback will be called when the object is about to + be finalized. The weak reference object will be passed as the only + parameter to the callback. Then the referent will no longer be + available. + :return: A proxy to a weak reference of the object + """ + if inspect.ismethod(object): + return WeakMethodProxy(object, callback) + else: + return weakref.proxy(object, callback) + + +class WeakMethod(object): + """Wraps a callable object like a function or a bound method. + Feature callback when the object is about to be finalized. + Provids the same interface as a normal weak reference. + """ + + def __init__(self, function, callback=None): + """ + Constructor + :param function: Function/method to be called + :param callback: If callback is provided and not None, + and the returned weakref object is still alive, the + callback will be called when the object is about to + be finalized; the weak reference object will be passed + as the only parameter to the callback; the referent will + no longer be available + """ + self.__callback = callback + + if inspect.ismethod(function): + # it is a bound method + self.__obj = weakref.ref(function.__self__, self.__call_callback) + self.__method = weakref.ref(function.__func__, self.__call_callback) + else: + self.__obj = None + self.__method = weakref.ref(function, self.__call_callback) + + def __call_callback(self, ref): + """Called when the object is about to be finalized""" + if not self.is_alive(): + return + self.__obj = None + self.__method = None + if self.__callback is not None: + self.__callback(self) + + def __call__(self): + """Return a callable function or None if the WeakMethod is dead.""" + if self.__obj is not None: + method = self.__method() + obj = self.__obj() + if method is None or obj is None: + return None + return types.MethodType(method, obj) + elif self.__method is not None: + return self.__method() + else: + return None + + def is_alive(self): + """True if the WeakMethod is still alive""" + return self.__method is not None + + def __eq__(self, other): + """Check it another obect is equal to this. + + :param object other: Object to compare with + """ + if isinstance(other, WeakMethod): + if not self.is_alive(): + return False + return self.__obj == other.__obj and self.__method == other.__method + return False + + def __ne__(self, other): + """Check it another obect is not equal to this. + + :param object other: Object to compare with + """ + if isinstance(other, WeakMethod): + if not self.is_alive(): + return False + return self.__obj != other.__obj or self.__method != other.__method + return True + + def __hash__(self): + """Returns the hash for the object.""" + return self.__obj.__hash__() ^ self.__method.__hash__() + + +class WeakMethodProxy(WeakMethod): + """Wraps a callable object like a function or a bound method + with a weakref proxy. + """ + def __call__(self, *args, **kwargs): + """Dereference the method and call it if the method is still alive. + Else raises an ReferenceError. + + :raises: ReferenceError, if the method is not alive + """ + fn = super(WeakMethodProxy, self).__call__() + if fn is None: + raise ReferenceError("weakly-referenced object no longer exists") + return fn(*args, **kwargs) + + +class WeakList(list): + """Manage a list of weaked references. + When an object is dead, the list is flaged as invalid. + If expected the list is cleaned up to remove dead objects. + """ + + def __init__(self, enumerator=()): + """Create a WeakList + + :param iterator enumerator: A list of object to initialize the + list + """ + list.__init__(self) + self.__list = [] + self.__is_valid = True + for obj in enumerator: + self.append(obj) + + def __invalidate(self, ref): + """Flag the list as invalidated. The list contains dead references.""" + self.__is_valid = False + + def __create_ref(self, obj): + """Create a weakref from an object. It uses the `ref` module function. + """ + return ref(obj, self.__invalidate) + + def __clean(self): + """Clean the list from dead references""" + if self.__is_valid: + return + self.__list = [ref for ref in self.__list if ref() is not None] + self.__is_valid = True + + def __iter__(self): + """Iterate over objects of the list""" + for ref in self.__list: + obj = ref() + if obj is not None: + yield obj + + def __len__(self): + """Count item on the list""" + self.__clean() + return len(self.__list) + + def __getitem__(self, key): + """Returns the object at the requested index + + :param key: Indexes to get + :type key: int or slice + """ + self.__clean() + data = self.__list[key] + if isinstance(data, list): + result = [ref() for ref in data] + else: + result = data() + return result + + def __setitem__(self, key, obj): + """Set an item at an index + + :param key: Indexes to set + :type key: int or slice + """ + self.__clean() + if isinstance(key, slice): + objs = [self.__create_ref(o) for o in obj] + self.__list[key] = objs + else: + obj_ref = self.__create_ref(obj) + self.__list[key] = obj_ref + + def __delitem__(self, key): + """Delete an Indexes item of this list + + :param key: Index to delete + :type key: int or slice + """ + self.__clean() + del self.__list[key] + + def __delslice__(self, i, j): + """Looks to be used in Python 2.7""" + self.__delitem__(slice(i, j, None)) + + def __setslice__(self, i, j, sequence): + """Looks to be used in Python 2.7""" + self.__setitem__(slice(i, j, None), sequence) + + def __getslice__(self, i, j): + """Looks to be used in Python 2.7""" + return self.__getitem__(slice(i, j, None)) + + def __reversed__(self): + """Returns a copy of the reverted list""" + reversed_list = reversed(list(self)) + return WeakList(reversed_list) + + def __contains__(self, obj): + """Returns true if the object is in the list""" + ref = self.__create_ref(obj) + return ref in self.__list + + def __add__(self, other): + """Returns a WeakList containing this list an the other""" + l = WeakList(self) + l.extend(other) + return l + + def __iadd__(self, other): + """Add objects to this list inplace""" + self.extend(other) + return self + + def __mul__(self, n): + """Returns a WeakList containing n-duplication object of this list""" + return WeakList(list(self) * n) + + def __imul__(self, n): + """N-duplication of the objects to this list inplace""" + self.__list *= n + return self + + def append(self, obj): + """Add an object at the end of the list""" + ref = self.__create_ref(obj) + self.__list.append(ref) + + def count(self, obj): + """Returns the number of occurencies of an object""" + ref = self.__create_ref(obj) + return self.__list.count(ref) + + def extend(self, other): + """Append the list with all objects from another list""" + for obj in other: + self.append(obj) + + def index(self, obj): + """Returns the index of an object""" + ref = self.__create_ref(obj) + return self.__list.index(ref) + + def insert(self, index, obj): + """Insert an object at the requested index""" + ref = self.__create_ref(obj) + self.__list.insert(index, ref) + + def pop(self, index): + """Remove and return an object at the requested index""" + self.__clean() + obj = self.__list.pop(index)() + return obj + + def remove(self, obj): + """Remove an object from the list""" + ref = self.__create_ref(obj) + self.__list.remove(ref) + + def reverse(self): + """Reverse the list inplace""" + self.__list.reverse() + + def sort(self, key=None, reverse=False): + """Sort the list inplace. + Not very efficient. + """ + sorted_list = list(self) + sorted_list.sort(key=key, reverse=reverse) + self.__list = [] + self.extend(sorted_list) + + def __str__(self): + unref_list = list(self) + return "WeakList(%s)" % str(unref_list) + + def __repr__(self): + unref_list = list(self) + return "WeakList(%s)" % repr(unref_list) |