# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves <bmr@redhat.com>
#
# boom/report.py - Text reporting
#
# This file is part of the boom project.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions
# of the GNU General Public License v.2.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
"""The Boom reporting module contains a set of classes for creating
simple text based tabular reports for a user-defined set of object
types and fields. No restrictions are placed on the types of object
that can be reported: users of the ``BoomReport`` classes may define
additional object types outside the ``boom`` package and include these
types in reports generated by the module.
The fields displayed in a specific report may be selected from the
available set of fields by specifying a simple comma-separated string
list of field names (in display order). In addition, custom multi-column
sorting is possible using a similar string notation.
The ``BoomReport`` module is closely based on the ``device-mapper``
reporting engine and shares many features and behaviours with device
mapper reports.
"""
from __future__ import print_function
import logging
import sys
from boom import find_minimum_sha_prefix, BOOM_DEBUG_REPORT
_log = logging.getLogger(__name__)
_log.set_debug_mask(BOOM_DEBUG_REPORT)
_log_debug = _log.debug
_log_debug_report = _log.debug_masked
_log_info = _log.info
_log_warn = _log.warning
_log_error = _log.error
_default_columns = 80
REP_NUM = "num"
REP_STR = "str"
REP_SHA = "sha"
_dtypes = [REP_NUM, REP_STR, REP_SHA]
_default_width = 8
ALIGN_LEFT = "left"
ALIGN_RIGHT = "right"
_align_types = [ALIGN_LEFT, ALIGN_RIGHT]
ASCENDING = "ascending"
DESCENDING = "descending"
STANDARD_QUOTE = "'"
STANDARD_PAIR = "="
MIN_SHA_WIDTH = 7
# Python2 vs. Python2 string types
try:
# Py2
string_types = (str, unicode)
except NameError:
# Py3
string_types = str
num_types = (int, float)
[docs]
class BoomReportOpts(object):
"""BoomReportOpts()
Options controlling the formatting and output of a boom report.
"""
columns = 0
headings = True
buffered = True
separator = None
field_name_prefix = None
unquoted = True
aligned = True
columns_as_rows = False
report_file = None
[docs]
def __init__(
self,
columns=_default_columns,
headings=True,
buffered=True,
separator=" ",
field_name_prefix="",
unquoted=True,
aligned=True,
report_file=sys.stdout,
):
"""Initialise BoomReportOpts object.
Initialise a ``BoomReportOpts`` object to control output
of a ``BoomReport``.
:param columns: the number of columns to use for output.
:param headings: a boolean indicating whether to output
column headings for this report.
:param buffered: a boolean indicating whether to buffer
output from this report.
:param report_file: a file to which output will be sent.
:returns: a new ``BoomReportOpts`` object.
:rtype: ``<class BoomReportOpts>``
"""
self.columns = columns
self.headings = headings
self.buffered = buffered
self.separator = separator
self.field_name_prefix = field_name_prefix
self.unquoted = unquoted
self.aligned = aligned
self.report_file = report_file
[docs]
class BoomReportObjType(object):
"""BoomReportObjType()
Class representing a type of object to be reported on.
Instances of ``BoomReportObjType`` must specify an identifier,
a description, and a data function that will return the correct
type of object from a compound object containing data objects
of different types. For reports that use only a single object
type the ``data_fn`` member may be simply ``lambda x: x``.
"""
objtype = -1
desc = ""
prefix = ""
data_fn = None
[docs]
def __init__(self, objtype, desc, prefix, data_fn):
"""Initialise BoomReportObjType.
Initialise a new ``BoomReportObjType`` object with the
specified ``objtype``, ``desc``, optional ``prefix`` and
``data_fn``. The ``objtype`` must be an integer power of two
that is unique within a given report. The ``data_fn`` should
accept an object as its only argument and return an object
of the requested type.
"""
if not objtype or objtype < 0:
raise ValueError("BoomReportObjType objtype cannot be <= 0.")
if not desc:
raise ValueError("BoomReportObjType desc cannot be empty.")
if not data_fn:
raise ValueError("BoomReportObjType requires data_fn.")
self.objtype = objtype
self.desc = desc
self.prefix = prefix
self.data_fn = data_fn
[docs]
class BoomFieldType(object):
"""BoomFieldType()
The ``BoomFieldType`` class describes the properties of a field
available in a ``BoomReport`` instance.
"""
objtype = -1
name = None
head = None
desc = None
width = _default_width
align = None
dtype = None
report_fn = None
[docs]
def __init__(self, objtype, name, head, desc, width, dtype, report_fn, align=None):
"""Initialise new BoomFieldType object.
Initialise a new ``BoomFieldType`` object with the specified
properties.
:param objtype: The numeric object type ID (power of two)
:param name: The field name used to select display fields
:param desc: A human-readable description of the field
:param width: The default (initial) field width
:param dtype: The BoomReport data type of the field
:param report_fn: The field reporting function
:param align: The field alignment value
:returns: A new BoomReportFieldType object
:rtype: BoomReportFieldType
"""
if not objtype:
raise ValueError("'objtype' must be non-zero")
if not name:
raise ValueError("'name' is required")
self.objtype = objtype
self.name = name
self.head = head
self.desc = desc
if dtype not in _dtypes:
raise ValueError("Invalid field dtype: %s " % dtype)
if align and align not in _align_types:
raise ValueError("Invalid field alignment: %s" % align)
self.dtype = dtype
self.report_fn = report_fn
if not align:
if dtype == REP_STR or dtype == REP_SHA:
self.align = ALIGN_LEFT
if dtype == REP_NUM:
self.align = ALIGN_RIGHT
else:
self.align = align
if width < 0:
raise ValueError("Field width cannot be < 0")
self.width = width if width else _default_width
[docs]
class BoomFieldProperties(object):
field_num = None
# sort_posn
initial_width = 0
width = 0
objtype = None
dtype = None
align = None
#
# Field flags
#
hidden = False
implicit = False
sort_key = False
sort_dir = None
compact_one = False # used for implicit fields
compacted = False
sort_posn = None
[docs]
class BoomField(object):
"""BoomField()
A ``BoomField`` represents an instance of a ``BoomFieldType``
including its associated data values.
"""
#: reference to the containing BoomReport
_report = None
#: reference to the BoomFieldProperties describing this field
_props = None
#: The formatted string to be reported for this field.
report_string = None
#: The raw value of this field. Used for sorting.
sort_value = None
[docs]
def __init__(self, report, props):
"""Initialise a new BoomField object.
Initialise a BoomField object and configure the supplied
``report`` and ``props`` attributes.
:param report: The BoomReport that owns this field
:param props: The BoomFieldProperties object for this field
"""
self._report = report
self._props = props
[docs]
def report_str(self, value):
"""Report a string value for this BoomField object.
Set the value for this field to the supplied ``value``.
:param value: The string value to set
:rtype: None
"""
if not isinstance(value, string_types):
raise TypeError("Value for report_str() must be a string type.")
self.set_value(value, sort_value=value)
[docs]
def report_sha(self, value):
"""Report a SHA value for this BoomField object.
Set the value for this field to the supplied ``value``.
:param value: The SHA value to set
:rtype: None
"""
if not isinstance(value, string_types):
raise TypeError("Value for report_sha() must be a string type.")
self.set_value(value, sort_value=value)
[docs]
def report_num(self, value):
"""Report a numeric value for this BoomField object.
Set the value for this field to the supplied ``value``.
:param value: The numeric value to set
:rtype: None
"""
if value is not None and not isinstance(value, num_types):
raise TypeError("Value for report_num() must be a numeric type.")
report_string = str(value)
sort_value = value if value is not None else -1
self.set_value(report_string, sort_value=sort_value)
[docs]
def set_value(self, report_string, sort_value=None):
"""Report an arbitrary value for this BoomField object.
Set the value for this field to the supplied ``value``,
and set the field's ``sort_value`` to the supplied
``sort_value``.
:param report_string: The string value to set
:param sort_value: The sort value
:rtype: None
"""
if report_string is None:
raise ValueError("No value assigned to field.")
self.report_string = report_string
self.sort_value = sort_value if sort_value else report_string
class BoomRow(object):
"""BoomRow()
A class representing a single data row making up a report.
"""
#: the report that this BoomRow belongs to
_report = None
#: the list of report fields in display order
_fields = None
#: fields in sort order
_sort_fields = None
def __init__(self, report):
self._report = report
self._fields = []
def add_field(self, field):
"""Add a field to this BoomRow.
:param field: The field to be added
:rtype: None
"""
self._fields.append(field)
def __none_returning_fn(obj):
"""Dummy data function for special report types.
:returns: None
"""
return None
# Implicit report fields and types
BR_SPECIAL = 0x80000000
_implicit_special_report_types = [
BoomReportObjType(BR_SPECIAL, "Special", "special_", __none_returning_fn)
]
def __no_report_fn(f, d):
"""Dummy report function for special report types.
:returns: None
"""
return
_special_field_help_name = "help"
_implicit_special_report_fields = [
BoomFieldType(
BR_SPECIAL,
_special_field_help_name,
"Help",
"Show help",
8,
REP_STR,
__no_report_fn,
)
]
# BoomReport class
[docs]
class BoomReport(object):
"""BoomReport()
A class representing a configurable text report with multiple
caller-defined fields. An optional title may be provided and he
``fields`` argument must contain a list of ``BoomField`` objects
describing the required report.
"""
report_types = 0
_fields = None
_types = None
_data = None
_rows = None
_keys_count = 0
_field_properties = None
_header_written = False
_field_calc_needed = True
_sort_required = False
_already_reported = False
# Implicit field support
_implicit_types = _implicit_special_report_types
_implicit_fields = _implicit_special_report_fields
private = None
opts = None
def __help_requested(self):
"""Check for presence of 'help' fields in output selection.
Check the fields making up this BoomReport and return True
if any valid 'help' field synonym is present.
:returns: True if help was requested or False otherwise
"""
for fp in self._field_properties:
if fp.implicit:
name = self._implicit_fields[fp.field_num].name
if name == _special_field_help_name:
return True
return False
def __get_longest_field_name_len(self, fields):
"""Find the longest field name length.
:returns: the length of the longest configured field name
"""
max_len = 0
for f in fields:
cur_len = len(f.name)
max_len = cur_len if cur_len > max_len else max_len
for t in self._types:
cur_len = len(t.prefix) + 3
max_len = cur_len if cur_len > max_len else max_len
return max_len
def __display_fields(self, display_field_types):
"""Display report fields help message.
Display a list of valid fields for this ``BoomReport``.
:param fields: The list of fields to display
:param display_field_types: A boolean controlling whether
field types (str, SHA, num)
are included in help output
"""
fields = self._fields
name_len = self.__get_longest_field_name_len(fields)
last_desc = ""
banner = "-" * 79
for f in fields:
t = self.__find_type(f.objtype)
if t:
desc = t.desc
else:
desc = ""
if desc != last_desc:
if len(last_desc):
print(" ")
desc_len = len(desc) + 7
print("%s Fields" % desc)
print("%*.*s" % (desc_len, desc_len, banner))
print(
" %-*s - %s%s%s%s"
% (
name_len,
f.name,
f.desc,
" [" if display_field_types else "",
f.dtype if display_field_types else "",
"]" if display_field_types else "",
)
)
last_desc = desc
def __find_type(self, report_type):
"""Resolve numeric type to corresponding BoomReportObjType.
:param report_type: The numeric report type to look up
:returns: The requested BoomReportObjType.
:raises: ValueError if no matching type was found.
"""
for t in self._implicit_types:
if t.objtype == report_type:
return t
for t in self._types:
if t.objtype == report_type:
return t
raise ValueError("Unknown report object type: %d" % report_type)
def __copy_field(self, field_num, implicit):
"""Copy field definition to BoomFieldProperties
Copy values from a BoomFieldType to BoomFieldProperties.
:param field_num: The number of this field (fields order)
:param implicit: True if this field is implicit, else False
"""
fp = BoomFieldProperties()
fp.field_num = field_num
fp.width = fp.initial_width = self._fields[field_num].width
fp.implicit = implicit
fp.objtype = self.__find_type(self._fields[field_num].objtype)
fp.dtype = self._fields[field_num].dtype
fp.align = self._fields[field_num].align
return fp
def __add_field(self, field_num, implicit):
"""Add a field to this BoomReport.
Add the specified BoomFieldType to this BoomReport and
configure BoomFieldProperties for it.
:param field_num: The number of this field (fields order)
:param implicit: True if this field is implicit, else False
"""
fp = self.__copy_field(field_num, implicit)
if fp.hidden:
self._field_properties.insert(0, fp)
else:
self._field_properties.append(fp)
return fp
def __get_field(self, field_name):
"""Look up a field by name.
Attempt to find the field named in ``field_name`` in this
BoomReport's tables of implicit and user-defined fields,
returning the a ``(field, implicit)`` tuple, where field
contains the requested ``BoomFieldType``, and ``implicit``
is a boolean indicating whether this field is implicit or
not.
:param field_num: The number of this field (fields order)
:param implicit: True if this field is implicit, else False
"""
# FIXME implicit fields
for field in self._implicit_fields:
if field.name == field_name:
return (self._implicit_fields.index(field), True)
for field in self._fields:
if field.name == field_name:
return (self._fields.index(field), False)
raise ValueError("No matching field name: %s" % field_name)
def __field_match(self, field_name, type_only):
"""Attempt to match a field and optionally update report type.
Look up the named field and, if ``type_only`` is True,
update this BoomReport's ``report_types`` mask to include
the field's type identifier. If ``type_only`` is False the
field is also added to this BoomReport's field list.
:param field_name: A string identifying the field
:param type_only: True if this call should only update types
"""
try:
(f, implicit) = self.__get_field(field_name)
if type_only:
if implicit:
self.report_types |= self._implicit_fields[f].objtype
else:
self.report_types |= self._fields[f].objtype
return
return self.__add_field(f, implicit)
except ValueError as e:
# FIXME handle '$PREFIX_all'
# re-raise 'e' if it fails.
raise e
def __parse_fields(self, field_format, type_only):
"""Parse report field list.
Parse ``field_format`` and attempt to match the names of
field names found to registered BoomFieldType fields.
If ``type_only`` is True only the ``report_types`` field
is updated: otherwise the parsed fields are added to the
BoomReport's field list.
:param field_format: The list of fields to parse
:param type_only: True if this call should only update types
"""
for word in field_format.split(","):
# Allow consecutive commas
if not word:
continue
try:
self.__field_match(word, type_only)
except ValueError as e:
self.__display_fields(True)
print("Unrecognised field: %s" % word)
raise e
def __add_sort_key(self, field_num, sort, implicit, type_only):
"""Add a new sort key to this BoomReport
Add the sort key identified by ``field_num`` to this list
of sort keys for this BoomReport.
:param field_num: The field number of the key to add
:param sort: The sort direction for this key
:param implicit: True if field_num is implicit, else False
:param type_only: True if this call should only update types
"""
fields = self._implicit_fields if implicit else self._fields
found = None
for fp in self._field_properties:
if fp.implicit == implicit and fp.field_num == field_num:
found = fp
if not found:
if type_only:
self.report_types |= fields[field_num].objtype
return
else:
found = self.__add_field(field_num, implicit)
if found.sort_key:
_log_info("Ignoring duplicate sort field: %s" % fields[field_num].name)
found.sort_key = True
found.sort_dir = sort
found.sort_posn = self._keys_count
self._keys_count += 1
def __key_match(self, key_name, type_only):
"""Attempt to match a sort key and update report type.
Look up the named sort key and, if ``type_only`` is True,
update this BoomReport's ``report_types`` mask to include
the field's type identifier. If ``type_only`` is False the
field is also added to this BoomReport's field list.
:param field_name: A string identifying the sort key
:param type_only: True if this call should only update types
"""
sort_dir = None
if not key_name:
raise ValueError("Sort key name cannot be empty")
if key_name.startswith("+"):
sort_dir = ASCENDING
key_name = key_name[1:]
elif key_name.startswith("-"):
sort_dir = DESCENDING
key_name = key_name[1:]
else:
sort_dir = ASCENDING
for field in self._implicit_fields:
fields = self._implicit_fields
if field.name == key_name:
return self.__add_sort_key(
fields.index(field), sort_dir, True, type_only
)
for field in self._fields:
fields = self._fields
if field.name == key_name:
return self.__add_sort_key(
fields.index(field), sort_dir, False, type_only
)
raise ValueError("Unknown sort key name: %s" % key_name)
def __parse_keys(self, keys, type_only):
"""Parse report sort key list.
Parse ``keys`` and attempt to match the names of
sort keys found to registered BoomFieldType fields.
If ``type_only`` is True only the ``report_types`` field
is updated: otherwise the parsed fields are added to the
BoomReport's sort key list.
:param field_format: The list of fields to parse
:param type_only: True if this call should only update types
"""
if not keys:
return
for word in keys.split(","):
# Allow consecutive commas
if not word:
continue
try:
self.__key_match(word, type_only)
except ValueError as e:
self.__display_fields(True)
print("Unrecognised field: %s" % word)
raise e
[docs]
def __init__(self, types, fields, output_fields, opts, sort_keys, private):
"""Initialise BoomReport.
Initialise a new ``BoomReport`` object with the specified fields
and output control options.
:param types: List of BoomReportObjType used in this report.
:param fields: A list of ``BoomField`` field descriptions.
:param output_fields: An optional list of output fields to
be rendered by this report.
:param opts: An instance of ``BoomReportOpts`` or None.
:returns: A new report object.
:rtype: ``BoomReport``.
"""
self._fields = fields
self._types = types
self._private = private
if opts.buffered:
self._sort_required = True
self.opts = opts if opts else BoomReportOpts()
self._rows = []
self._field_properties = []
# set field_prefix from type
# canonicalize_field_ids()
if not output_fields:
output_fields = ",".join([field.name for field in fields])
# First pass: set up types
self.__parse_fields(output_fields, 1)
self.__parse_keys(sort_keys, 1)
# Second pass: initialise fields
self.__parse_fields(output_fields, 0)
self.__parse_keys(sort_keys, 0)
if self.__help_requested():
self._already_reported = True
self.__display_fields(display_field_types=True)
print("")
def __recalculate_sha_width(self):
"""Recalculate minimum SHA field widths.
For each REP_SHA field present, recalculate the minimum
field width required to ensure uniqueness of the displayed
values.
:rtype: None
"""
shas = {}
props_map = {}
for row in self._rows:
for field in row._fields:
if self._fields[field._props.field_num].dtype == REP_SHA:
# Use field_num as index to apply check across rows
num = field._props.field_num
if num not in shas:
shas[num] = set()
props_map[num] = field._props
shas[num].add(field.report_string)
for num in shas.keys():
min_prefix = max(MIN_SHA_WIDTH, props_map[num].width)
props_map[num].width = find_minimum_sha_prefix(shas[num], min_prefix)
def __recalculate_fields(self):
"""Recalculate field widths.
For each field, recalculate the minimum field width by
finding the longest ``report_string`` value for that field
and updating the dynamic width stored in the corresponding
``BoomFieldProperties`` object.
:rtype: None
"""
for row in self._rows:
for field in row._fields:
if self._sort_required and field._props.sort_key:
row._sort_fields[field._props.sort_posn] = field
if self._fields[field._props.field_num].dtype == REP_SHA:
continue
field_len = len(field.report_string)
if field_len > field._props.width:
field._props.width = field_len
def __report_headings(self):
"""Output report headings.
Output the column headings for this BoomReport.
:rtype: None
"""
self._header_written = True
if not self.opts.headings:
return
line = ""
props = self._field_properties
for fp in props:
if fp.hidden:
continue
fields = self._fields
heading = fields[fp.field_num].head
headertuple = (fp.width, fp.width, heading)
if self.opts.aligned:
heading = "%-*.*s" % headertuple
line += heading
if props.index(fp) != (len(props) - 1):
line += self.opts.separator
self.opts.report_file.write(line + "\n")
def __row_key_fn(self):
"""Return a Python key function to compare report rows.
The ``cmp`` argument of sorting functions has been removed
in Python 3.x: to maintain similarity with the device-mapper
report library we keep a traditional "cmp"-style function
(that is structured identically to the version in the device
mapper library), and dynamically wrap it in a ``__RowKey``
object to conform to the Python sort key model.
:returns: A __RowKey object wrapping _row_cmp()
:rtype: __RowKey
"""
def _row_cmp(row_a, row_b):
"""Compare two report rows for sorting.
Compare the report rows ``row_a`` and ``row_b`` and
return a "cmp"-style comparison value:
1 if row_a > row_b
0 if row_a == row_b
-1 if row_b < row_a
Note that the actual comparison direction depends on the
field definitions of the fields being compared, since
each sort key defines its own sort order.
:param row_a: The first row to compare
:param row_b: The second row to compare
"""
for cnt in range(0, row_a._report._keys_count):
sfa = row_a._sort_fields[cnt]
sfb = row_b._sort_fields[cnt]
if sfa._props.dtype == REP_NUM:
num_a = sfa.sort_value
num_b = sfb.sort_value
if num_a == num_b:
continue
if sfa._props.sort_dir == ASCENDING:
return 1 if num_a > num_b else -1
else:
return 1 if num_a < num_b else -1
else:
stra = sfa.sort_value
strb = sfb.sort_value
if stra == strb:
continue
if sfa._props.sort_dir == ASCENDING:
return 1 if stra > strb else -1
else:
return 1 if stra < strb else -1
return 0
class __RowKey(object):
"""__RowKey sort wrapper."""
def __init__(self, obj, *args):
"""Initialise a new __RowKey object.
:param obj: The object to be compared
:returns: None
"""
self.obj = obj
def __lt__(self, other):
"""Test if less than.
:param other: The other object to be compared
"""
return _row_cmp(self.obj, other.obj) < 0
def __gt__(self, other):
"""Test if greater than.
:param other: The other object to be compared
"""
return _row_cmp(self.obj, other.obj) > 0
def __eq__(self, other):
"""Test if equal to.
:param other: The other object to be compared
"""
return _row_cmp(self.obj, other.obj) == 0
def __le__(self, other):
"""Test if less than or equal to.
:param other: The other object to be compared
"""
return _row_cmp(self.obj, other.obj) <= 0
def __ge__(self, other):
"""Test if greater than or equal to.
:param other: The other object to be compared
"""
return _row_cmp(self.obj, other.obj) >= 0
def __ne__(self, other):
"""Test if not equal to.
:param other: The other object to be compared
"""
return _row_cmp(self.obj, other.obj) != 0
return __RowKey
[docs]
def _sort_rows(self):
"""Sort the rows of this BoomReport.
Sort this report's rows, according to the configured sort
keys.
:returns: None
"""
self._rows.sort(key=self.__row_key_fn())
[docs]
def report_object(self, obj):
"""Report data for object.
Add a row of data to this ``BoomReport``. The ``data``
argument should be an object of the type understood by this
report's fields. It will be passed in turn to each field to
obtain data for the current row.
:param obj: the object to report on for this row.
"""
if obj is None:
raise ValueError("Cannot report NoneType object.")
if self._already_reported:
return
row = BoomRow(self)
fields = self._fields
if self._sort_required:
row._sort_fields = [-1] * self._keys_count
for fp in self._field_properties:
field = BoomField(self, fp)
data = fp.objtype.data_fn(obj)
if data is None:
raise ValueError(
"No data assigned to field %s" % fields[fp.field_num].name
)
try:
fields[fp.field_num].report_fn(field, data)
except ValueError:
raise ValueError(
"No value assigned to field %s" % fields[fp.field_num].name
)
row.add_field(field)
self._rows.append(row)
if not self.opts.buffered:
return self.report_output()
[docs]
def _output_field(self, field):
"""Output field data.
Generate string data for one field in a report row.
:field: The field to be output
:returns: The output report string for this field
:rtype: str
"""
fields = self._fields
prefix = self.opts.field_name_prefix
quote = "" if self.opts.unquoted else STANDARD_QUOTE
if prefix:
field_name = fields[field._props.field_num].name
prefix += "%s%s%s" % (field_name.upper(), STANDARD_PAIR, STANDARD_QUOTE)
repstr = field.report_string
width = field._props.width
if self.opts.aligned:
align = field._props.align
if not align:
if field._props.dtype == REP_NUM:
align = ALIGN_RIGHT
else:
align = ALIGN_LEFT
reptuple = (width, width, repstr)
if align == ALIGN_LEFT:
repstr = "%-*.*s" % reptuple
else:
repstr = "%*.*s" % reptuple
suffix = quote
return prefix + repstr + suffix
[docs]
def _output_as_rows(self):
"""Output this report in column format.
Output the data contained in this ``BoomReport`` in column
format, one row per line. If column headings have not been
printed already they will be automatically displayed by this
call.
:returns: None
"""
for fp in self._field_properties:
if fp.hidden:
for row in self._rows:
row._fields = row._fields[1:]
fields = self._implicit_fields if fp.implicit else self._fields
line = ""
if self.opts.headings:
line += fields[fp.field_num].head + self.opts.separator
for row in self._rows:
field = row._fields[0]
line += self._output_field(field)
line += self.opts.separator
row._fields = row._fields[1:]
self.opts.report_file.write(line + "\n")
[docs]
def _output_as_columns(self):
"""Output this report in column format.
Output the data contained in this ``BoomReport`` in column
format, one row per line. If column headings have not been
printed already they will be automatically displayed by this
call.
:returns: None
"""
if not self._header_written:
self.__report_headings()
for row in self._rows:
do_field_delim = False
line = ""
for field in row._fields:
if field._props.hidden:
continue
if do_field_delim:
line += self.opts.separator
else:
do_field_delim = True
line += self._output_field(field)
self.opts.report_file.write(line + "\n")
[docs]
def report_output(self):
"""Output report data.
Output this report's data to the configured report file,
using the configured output controls and fields.
On success the number of rows output is returned. On
error an exception is raised.
:returns: the number of rows of output written.
:rtype: ``int``
"""
if self._already_reported:
return
if self._field_calc_needed:
self.__recalculate_sha_width()
self.__recalculate_fields()
if self._sort_required:
self._sort_rows()
if self.opts.columns_as_rows:
return self._output_as_rows()
else:
return self._output_as_columns()
__all__ = [
# Module constants
"REP_NUM",
"REP_STR",
"REP_SHA",
"ALIGN_LEFT",
"ALIGN_RIGHT",
"ASCENDING",
"DESCENDING",
# Report objects
"BoomReportOpts",
"BoomReportObjType",
"BoomField",
"BoomFieldType",
"BoomFieldProperties",
"BoomReport",
]
# vim: set et ts=4 sw=4 :