Source code for pyretis.inout.formats.formatter
# -*- coding: utf-8 -*-
# Copyright (c) 2023, PyRETIS Development Team.
# Distributed under the LGPLv2.1+ License. See LICENSE for more info.
"""Module for defining the generic formatting of output data from PyRETIS.
Important classes defined here
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
OutputFormatter (:py:class:`.OutputFormatter`)
A generic class for formatting output from PyRETIS.
PyretisLogFormatter (:py:class:`.PyretisLogFormatter`)
A class representing a formatter for the PyRETIS log file.
Important methods defined here
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
apply_format (:py:func:`.apply_format`)
Apply a format string to a given float value. This method
can be used for formatting text for tables (i.e. if we want
a fixed width).
format_number (:py:func:`.format_number`)
Format a number based on its size.
get_log_formatter (:py:func:`.get_log_formatter`)
Select a formatter for logging based on a given message level.
"""
import logging
from pyretis.inout.fileio import read_some_lines
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
logger.addHandler(logging.NullHandler())
# hard-coded formats to use for Log files:
LOG_FMT = '[%(levelname)s]: %(message)s'
LOG_DEBUG_FMT = ('[%(levelname)s] [%(name)s, %(funcName)s() at'
' line %(lineno)d]: %(message)s')
__all__ = ['OutputFormatter', 'PyretisLogFormatter', 'apply_format',
'format_number', 'get_log_formatter',
'LOG_FMT', 'LOG_DEBUG_FMT']
[docs]def _make_header(labels, width, spacing=1):
"""Format a table header with the given labels.
Parameters
----------
labels : list of strings
The strings to use for the table header.
width : list of ints
The widths to use for the table.
spacing : int
The spacing between the columns in the table
Returns
-------
out : string
A header for the table.
"""
heading = []
for i, col in enumerate(labels):
try:
wid = width[i]
except IndexError:
wid = width[-1]
if i == 0:
fmt = f'# {{:>{wid-2}s}}'
else:
fmt = f'{{:>{wid}s}}'
heading.append(fmt.format(col))
str_white = ' ' * spacing
return str_white.join(heading)
[docs]def apply_format(value, fmt):
"""Apply a format string to a given float value.
Here we check the formatting of a float. We are *forcing* a
*maximum length* on the resulting string. This is to avoid problems
like: '{:7.2f}'.format(12345.7) which returns '12345.70' with a
length 8 > 7. The intended use of this function is to avoid such
problems when we are formatting numbers for tables. Here it is done
by switching to an exponential notation. But note however that this
will have implications for how many decimal places we can show.
Parameters
----------
value : float
The float to format.
fmt : string
The format to use.
Note
----
This function converts numbers to have a fixed length. In some
cases this may reduce the number of significant digits. Remember
to also output your numbers without this format in case a specific
number of significant digits is important!
"""
maxlen = fmt.split(':')[1].split('.')[0]
align = ''
if not maxlen[0].isalnum():
align = maxlen[0]
maxlen = maxlen[1:]
maxlen = int(maxlen)
str_fmt = fmt.format(value)
if len(str_fmt) > maxlen: # switch to exponential:
if value < 0:
deci = maxlen - 7
else:
deci = maxlen - 6
new_fmt = f'{{:{align}{maxlen}.{deci}e}}'
return new_fmt.format(value)
return str_fmt
[docs]def format_number(number, minf, maxf, fmtf='{0:<16.9f}', fmte='{0:<16.9e}'):
"""Format a number based on its size.
Parameters
----------
number : float
The number to format.
minf : float
If the number is smaller than `minf` then apply the
format with scientific notation.
maxf : float
If the number is greater than `maxf` then apply the
format with scientific notation.
fmtf : string, optional
Format to use for floats.
fmte : string, optional
Format to use for scientific notation.
Returns
-------
out : string
The formatted number.
"""
if minf <= number <= maxf:
return fmtf.format(number)
return fmte.format(number)
[docs]class OutputFormatter:
"""A generic class for formatting output from PyRETIS.
Attributes
----------
name : string
A string which identifies the formatter.
header : string
A header (or table heading) with information about the
output data.
print_header : boolean
Determines if we are to print the header or not. This is useful
for classes making use of the formatter to determine if they
should write out the header or not.
"""
_FMT = '{}'
[docs] def __init__(self, name, header=None):
"""Initialise the formatter.
Parameters
----------
name : string
A string which identifies the output type of this formatter.
header : dict, optional
The header for the output data
"""
self.name = name
self._header = None
self.print_header = True
if header is not None:
if 'width' in header and 'labels' in header:
self._header = _make_header(header['labels'],
header['width'],
spacing=header.get('spacing', 1))
else:
self._header = header.get('text', None)
else:
self.print_header = False
@property
def header(self):
"""Define the header as a property."""
return self._header
@header.setter
def header(self, value):
"""Set the header."""
self._header = value
[docs] def format(self, step, data):
"""Use the formatter to generate output.
Parameters
----------
step : integer
This is assumed to be the current step number for
generating the output.
data : list, dict or similar
This is the data we are to format. Here we assume that
this is something we can iterate over.
"""
out = [f'{step}']
for i in data:
out.append(self._FMT.format(i))
yield ' '.join(out)
[docs] @staticmethod
def parse(line):
"""Parse formatted data.
This method is intended to be the "inverse" of the :py:meth:`.format`
method. In this particular case, we assume that we read floats from
columns in a file. One input line corresponds to a "row" of data.
Parameters
----------
line : string
The string we will parse.
Returns
-------
out : list of floats
The parsed input data.
"""
return [int(col) if i == 0 else
float(col) for i, col in enumerate(line.split())]
[docs] def load(self, filename):
"""Read generic data from a file.
Since this class defines how the data is formatted it is also
convenient to have methods for reading the data defined here.
This method will read entire blocks of data from a file into
memory. This will be slow for large files and this method
could be converted to also yield the individual "rows" of
the blocks, rather than the full blocks themselves.
Parameters
----------
filename : string
The path/file name of the file we want to open.
Yields
------
data : list of tuples of int
This is the data contained in the file. The columns are the
step number, interface number and direction.
See Also
--------
:py:func:`.read_some_lines`.
"""
for blocks in read_some_lines(filename, self.parse):
data_dict = {'comment': blocks['comment'],
'data': blocks['data']}
yield data_dict
[docs]def get_log_formatter(level):
"""Select a log format based on a given level.
Here, it is just used to get a slightly more verbose format for
the debug level.
Parameters
----------
level : integer
This integer defines the log level.
Returns
-------
out : object like :py:class:`logging.Formatter`
An object that can be used as a formatter for a logger.
"""
if level <= logging.DEBUG:
return PyretisLogFormatter(LOG_DEBUG_FMT)
return PyretisLogFormatter(LOG_FMT)
[docs]class PyretisLogFormatter(logging.Formatter): # pragma: no cover
"""Hard-coded formatter for the PyRETIS log file.
This formatter will just adjust multi-line messages to have some
indentation.
"""
[docs] def format(self, record):
"""Apply the PyRETIS log format."""
out = logging.Formatter.format(self, record)
if '\n' in out:
heading, _ = out.split(record.message)
if len(heading) < 12:
out = out.replace('\n', '\n' + ' ' * len(heading))
else:
out = out.replace('\n', '\n' + ' ' * 4)
return out