diff options
Diffstat (limited to 'src/silx/gui/data/TextFormatter.py')
-rw-r--r-- | src/silx/gui/data/TextFormatter.py | 386 |
1 files changed, 386 insertions, 0 deletions
diff --git a/src/silx/gui/data/TextFormatter.py b/src/silx/gui/data/TextFormatter.py new file mode 100644 index 0000000..b6baca4 --- /dev/null +++ b/src/silx/gui/data/TextFormatter.py @@ -0,0 +1,386 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2017-2021 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 package provides a class sharred by widget from the +data module to format data as text in the same way.""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "24/07/2018" + +import logging +import numbers + +import numpy + +from silx.gui import qt + +import h5py + + +_logger = logging.getLogger(__name__) + + +class TextFormatter(qt.QObject): + """Formatter to convert data to string. + + The method :meth:`toString` returns a formatted string from an input data + using parameters set to this object. + + It support most python and numpy data, expecting dictionary. Unsupported + data are displayed using the string representation of the object (`str`). + + It provides a set of parameters to custom the formatting of integer and + float values (:meth:`setIntegerFormat`, :meth:`setFloatFormat`). + + It also allows to custom the use of quotes to display text data + (:meth:`setUseQuoteForText`), and custom unit used to display imaginary + numbers (:meth:`setImaginaryUnit`). + + The object emit an event `formatChanged` every time a parametter is + changed. + """ + + formatChanged = qt.Signal() + """Emitted when properties of the formatter change.""" + + def __init__(self, parent=None, formatter=None): + """ + Constructor + + :param qt.QObject parent: Owner of the object + :param TextFormatter formatter: Instantiate this object from the + formatter + """ + qt.QObject.__init__(self, parent) + if formatter is not None: + self.__integerFormat = formatter.integerFormat() + self.__floatFormat = formatter.floatFormat() + self.__useQuoteForText = formatter.useQuoteForText() + self.__imaginaryUnit = formatter.imaginaryUnit() + self.__enumFormat = formatter.enumFormat() + else: + self.__integerFormat = "%d" + self.__floatFormat = "%g" + self.__useQuoteForText = True + self.__imaginaryUnit = u"j" + self.__enumFormat = u"%(name)s(%(value)d)" + + def integerFormat(self): + """Returns the format string controlling how the integer data + are formated by this object. + + This is the C-style format string used by python when formatting + strings with the modulus operator. + + :rtype: str + """ + return self.__integerFormat + + def setIntegerFormat(self, value): + """Set format string controlling how the integer data are + formated by this object. + + :param str value: Format string (e.g. "%d", "%i", "%08i"). + This is the C-style format string used by python when formatting + strings with the modulus operator. + """ + if self.__integerFormat == value: + return + self.__integerFormat = value + self.formatChanged.emit() + + def floatFormat(self): + """Returns the format string controlling how the floating-point data + are formated by this object. + + This is the C-style format string used by python when formatting + strings with the modulus operator. + + :rtype: str + """ + return self.__floatFormat + + def setFloatFormat(self, value): + """Set format string controlling how the floating-point data are + formated by this object. + + :param str value: Format string (e.g. "%.3f", "%d", "%-10.2f", + "%10.3e"). + This is the C-style format string used by python when formatting + strings with the modulus operator. + """ + if self.__floatFormat == value: + return + self.__floatFormat = value + self.formatChanged.emit() + + def useQuoteForText(self): + """Returns true if the string data are formatted using double quotes. + + Else, no quotes are used. + """ + return self.__integerFormat + + def setUseQuoteForText(self, useQuote): + """Set the use of quotes to delimit string data. + + :param bool useQuote: True to use quotes. + """ + if self.__useQuoteForText == useQuote: + return + self.__useQuoteForText = useQuote + self.formatChanged.emit() + + def imaginaryUnit(self): + """Returns the unit display for imaginary numbers. + + :rtype: str + """ + return self.__imaginaryUnit + + def setImaginaryUnit(self, imaginaryUnit): + """Set the unit display for imaginary numbers. + + :param str imaginaryUnit: Unit displayed after imaginary numbers + """ + if self.__imaginaryUnit == imaginaryUnit: + return + self.__imaginaryUnit = imaginaryUnit + self.formatChanged.emit() + + def setEnumFormat(self, value): + """Set format string controlling how the enum data are + formated by this object. + + :param str value: Format string (e.g. "%(name)s(%(value)d)"). + This is the C-style format string used by python when formatting + strings with the modulus operator. + """ + if self.__enumFormat == value: + return + self.__enumFormat = value + self.formatChanged.emit() + + def enumFormat(self): + """Returns the format string controlling how the enum data + are formated by this object. + + This is the C-style format string used by python when formatting + strings with the modulus operator. + + :rtype: str + """ + return self.__enumFormat + + def __formatText(self, text): + if self.__useQuoteForText: + text = "\"%s\"" % text.replace("\\", "\\\\").replace("\"", "\\\"") + return text + + def __formatBinary(self, data): + if isinstance(data, numpy.void): + data = data.item() + if isinstance(data, numpy.ndarray): + # Before numpy 1.15.0 the item API was returning a numpy array + data = data.astype(numpy.uint8) + else: + # Now it is supposed to be a bytes type + pass + data = ["\\x%02X" % d for d in data] + if self.__useQuoteForText: + return "b\"%s\"" % "".join(data) + else: + return "".join(data) + + def __formatSafeAscii(self, data): + data = [chr(d) if (d > 0x20 and d < 0x7F) else "\\x%02X" % d for d in data] + if self.__useQuoteForText: + data = [c if c != '"' else "\\" + c for c in data] + return "b\"%s\"" % "".join(data) + else: + return "".join(data) + + def __formatCharString(self, data): + """Format text of char. + + From the specifications we expect to have ASCII, but we also allow + CP1252 in some ceases as fallback. + + If no encoding fits, it will display a readable ASCII chars, with + escaped chars (using the python syntax) for non decoded characters. + + :param data: A binary string of char expected in ASCII + :rtype: str + """ + try: + text = "%s" % data.decode("ascii") + return self.__formatText(text) + except UnicodeDecodeError: + # Here we can spam errors, this is definitly a badly + # generated file + _logger.error("Invalid ASCII string %s.", data) + if data == b"\xB0": + _logger.error("Fallback using cp1252 encoding") + return self.__formatText(u"\u00B0") + return self.__formatSafeAscii(data) + + def __formatH5pyObject(self, data, dtype): + # That's an HDF5 object + ref = h5py.check_dtype(ref=dtype) + if ref is not None: + if bool(data): + return "REF" + else: + return "NULL_REF" + vlen = h5py.check_dtype(vlen=dtype) + if vlen is not None: + if vlen == str: + # HDF5 UTF8 + # With h5py>=3 reading dataset returns bytes + if isinstance(data, (bytes, numpy.bytes_)): + try: + data = data.decode("utf-8") + except UnicodeDecodeError: + self.__formatSafeAscii(data) + return self.__formatText(data) + elif vlen == bytes: + # HDF5 ASCII + return self.__formatCharString(data) + elif isinstance(vlen, numpy.dtype): + return self.toString(data, vlen) + return None + + def toString(self, data, dtype=None): + """Format a data into a string using formatter options + + :param object data: Data to render + :param dtype: enforce a dtype (mostly used to remember the h5py dtype, + special h5py dtypes are not propagated from array to items) + :rtype: str + """ + if isinstance(data, tuple): + text = [self.toString(d) for d in data] + return "(" + " ".join(text) + ")" + elif isinstance(data, list): + text = [self.toString(d) for d in data] + return "[" + " ".join(text) + "]" + elif isinstance(data, numpy.ndarray): + if dtype is None: + dtype = data.dtype + if data.shape == (): + # it is a scaler + return self.toString(data[()], dtype) + else: + text = [self.toString(d, dtype) for d in data] + return "[" + " ".join(text) + "]" + if dtype is not None and dtype.kind == 'O': + text = self.__formatH5pyObject(data, dtype) + if text is not None: + return text + elif isinstance(data, numpy.void): + if dtype is None: + dtype = data.dtype + if dtype.fields is not None: + text = [] + for index, field in enumerate(dtype.fields.items()): + text.append(field[0] + ":" + self.toString(data[index], field[1][0])) + return "(" + " ".join(text) + ")" + return self.__formatBinary(data) + elif isinstance(data, (numpy.unicode_, str)): + return self.__formatText(data) + elif isinstance(data, (numpy.string_, bytes)): + if dtype is None and hasattr(data, "dtype"): + dtype = data.dtype + if dtype is not None: + # Maybe a sub item from HDF5 + if dtype.kind == 'S': + return self.__formatCharString(data) + elif dtype.kind == 'O': + text = self.__formatH5pyObject(data, dtype) + if text is not None: + return text + try: + # Try ascii/utf-8 + text = "%s" % data.decode("utf-8") + return self.__formatText(text) + except UnicodeDecodeError: + pass + return self.__formatBinary(data) + elif isinstance(data, str): + text = "%s" % data + return self.__formatText(text) + elif isinstance(data, (numpy.integer)): + if dtype is None: + dtype = data.dtype + enumType = h5py.check_dtype(enum=dtype) + if enumType is not None: + for key, value in enumType.items(): + if value == data: + result = {} + result["name"] = key + result["value"] = data + return self.__enumFormat % result + return self.__integerFormat % data + elif isinstance(data, (numbers.Integral)): + return self.__integerFormat % data + elif isinstance(data, (numbers.Real, numpy.floating)): + # It have to be done before complex checking + return self.__floatFormat % data + elif isinstance(data, (numpy.complexfloating, numbers.Complex)): + text = "" + if data.real != 0: + text += self.__floatFormat % data.real + if data.real != 0 and data.imag != 0: + if data.imag < 0: + template = self.__floatFormat + " - " + self.__floatFormat + self.__imaginaryUnit + params = (data.real, -data.imag) + else: + template = self.__floatFormat + " + " + self.__floatFormat + self.__imaginaryUnit + params = (data.real, data.imag) + else: + if data.imag != 0: + template = self.__floatFormat + self.__imaginaryUnit + params = (data.imag) + else: + template = self.__floatFormat + params = (data.real) + return template % params + elif isinstance(data, h5py.h5r.Reference): + dtype = h5py.special_dtype(ref=h5py.Reference) + text = self.__formatH5pyObject(data, dtype) + return text + elif isinstance(data, h5py.h5r.RegionReference): + dtype = h5py.special_dtype(ref=h5py.RegionReference) + text = self.__formatH5pyObject(data, dtype) + return text + elif isinstance(data, numpy.object_) or dtype is not None: + if dtype is None: + dtype = data.dtype + text = self.__formatH5pyObject(data, dtype) + if text is not None: + return text + # That's a numpy object + return str(data) + return str(data) |