summaryrefslogtreecommitdiff
path: root/silx/utils
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2017-08-18 14:48:52 +0200
committerPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2017-08-18 14:48:52 +0200
commitf7bdc2acff3c13a6d632c28c4569690ab106eed7 (patch)
tree9d67cdb7152ee4e711379e03fe0546c7c3b97303 /silx/utils
Import Upstream version 0.5.0+dfsg
Diffstat (limited to 'silx/utils')
-rw-r--r--silx/utils/__init__.py0
-rw-r--r--silx/utils/array_like.py593
-rw-r--r--silx/utils/decorators.py71
-rw-r--r--silx/utils/html.py60
-rw-r--r--silx/utils/launcher.py303
-rw-r--r--silx/utils/setup.py43
-rw-r--r--silx/utils/test/__init__.py43
-rw-r--r--silx/utils/test/test_array_like.py453
-rw-r--r--silx/utils/test/test_html.py61
-rw-r--r--silx/utils/test/test_launcher.py204
-rw-r--r--silx/utils/test/test_launcher_command.py47
-rw-r--r--silx/utils/test/test_weakref.py330
-rw-r--r--silx/utils/weakref.py361
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>")
+ >>> "&lt;html&gt;"
+
+ .. 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("&", "&amp;") # must be done first
+ string = string.replace("<", "&lt;")
+ string = string.replace(">", "&gt;")
+ if quote:
+ string = string.replace("'", "&apos;")
+ string = string.replace("\"", "&quot;")
+ 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("&lt;html&gt;&apos;&quot;", result)
+
+ def testLtAmpGt(self):
+ # '&' have to be escaped first
+ result = html.escape("<&>")
+ self.assertEquals("&lt;&amp;&gt;", result)
+
+ def testNoQuotes(self):
+ result = html.escape("\"m&m's\"", quote=False)
+ self.assertEquals("\"m&amp;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)