diff --git a/pywbemtools/pywbemcli/_cmd_class.py b/pywbemtools/pywbemcli/_cmd_class.py index b973328d..17e8b0c1 100644 --- a/pywbemtools/pywbemcli/_cmd_class.py +++ b/pywbemtools/pywbemcli/_cmd_class.py @@ -711,9 +711,11 @@ def cmd_class_get(context, classname, options): the class. If the class cannot be found, the server returns a CIMError exception. """ - - format_group = get_format_group(context, options) - output_format = validate_output_format(context.output_format, format_group) + # TODO what is this + #format_group = get_format_group(context, options) + #output_format = validate_output_format(context.output_format, format_group) + output_format = validate_output_format(context.output_format, ['CIM', + 'TABLE']) try: result_class = context.conn.GetClass( @@ -745,7 +747,8 @@ def cmd_class_enumerate(context, classname, options): Enumerate the classes returning a list of classes from the WBEM server. That match the qualifier filter options """ - format_group = get_format_group(context, options) + #format_group = get_format_group(context, options) + format_group = ['CIM', 'TABLE', 'TEXT'] output_format = validate_output_format(context.output_format, format_group) try: diff --git a/pywbemtools/pywbemcli/_common.py b/pywbemtools/pywbemcli/_common.py index 1ca56d9f..c8cdc78d 100644 --- a/pywbemtools/pywbemcli/_common.py +++ b/pywbemtools/pywbemcli/_common.py @@ -1155,11 +1155,15 @@ def column_is_empty(rows, column): return False return True - # Remove empty rows + # Validate row lengths are the same as header length len_hdr = len(headers) for row in rows: - assert len(row) == len_hdr, "row: {}\nhdrs: {}". \ - format(row, headers) + if len(row) != len_hdr: + import pdb; pdb.set_trace() + assert len(row) == len_hdr, "Header len and row len mismatch:" \ + "row_len {} len hdr {}\n" \ + "row: {}\nhdrs: {}". \ + format(len(row), len_hdr, row, headers) for column in range(len(headers) - 1, -1, -1): if column_is_empty(rows, column): if isinstance(headers, tuple): diff --git a/pywbemtools/pywbemcli/_display_cimobjects.py b/pywbemtools/pywbemcli/_display_cimobjects.py index b28ecd1f..1813ca29 100644 --- a/pywbemtools/pywbemcli/_display_cimobjects.py +++ b/pywbemtools/pywbemcli/_display_cimobjects.py @@ -26,6 +26,7 @@ from pydicti import odicti import six import click +from nocaselist import NocaseList from nocasedict import NocaseDict from pywbem import CIMInstanceName, CIMInstance, CIMClass, \ @@ -33,14 +34,16 @@ CIMError, CIM_ERR_NOT_SUPPORTED from ._common import format_table, fold_strings, DEFAULT_MAX_CELL_WIDTH, \ - output_format_is_table, sort_cimobjects, format_keys + output_format_is_table, sort_cimobjects, format_keys, \ + hide_empty_columns -from .config import USE_TERMINAL_WIDTH, DEFAULT_TABLE_WIDTH +from .config import DEFAULT_TABLE_WIDTH, USE_TERMINAL_WIDTH from ._cimvalueformatter import cimvalue_to_fmtd_string INT_TYPE_PATTERN = re.compile(r'^[su]int(8|16|32|64)$') + #################################################################### # # Display of CIM objects. @@ -189,7 +192,8 @@ def _display_objects_as_table(objects, output_format, context=None): _display_instances_as_table(objects, table_width, output_format, context=context) elif isinstance(objects[0], CIMClass): - _display_classes_as_table(objects, table_width, output_format) + _display_classes_as_table(objects, table_width, output_format, + context=context) elif isinstance(objects[0], CIMQualifierDeclaration): _display_qual_decls_as_table(objects, table_width, output_format) elif isinstance(objects[0], (CIMClassName, CIMInstanceName, @@ -258,17 +262,387 @@ def display_cim_objects_summary(context, objects, output_format): click.echo('0 objects returned') -def _display_classes_as_table(classes, table_width, table_format): +class TableCell(object): """ - TODO: Future extend to display classes as a table, showing the - properties for each class. This will display the properties that exist in - subclasses. The temp output - so we could create the function is to just output as mof + Defines a single cell of data for a table. This data may be of any + python type. + The data can be manipulated including folding it as a string into multiple + lines and computing the length and width of the data. """ - # pylint: disable=unused-argument + def __init__(self, data, max_width=None, break_long_words=False, + break_on_hyphens=False, fold_list_items=False, separator=', ', + initial_indent='', subsequent_indent=''): + """ + Capture the data for the cell and optionally fold it immediatly if + the max_width parameter is defined. + """ + self._data = data + # TODO: Handle lists including cvt to strings. + # optional. If max_width on constructor fold immediatly + if data and max_width: + self.fold(max_width, break_long_words, break_on_hyphens, + fold_list_items, initial_indent, subsequent_indent) + + @property + def data(self): + """ + Returns the string representation of the data including any folding + """ + return self._data + + @property + def width(self): + """ + Get the maximum length between EOL characters which is the the maximum + display width of this cell. That is the cell width. If the cell data is + not a string, return the length of the string representation of the + cell. + + Parameters: + cell (:term:`string` or int or bool or float): + String that may contain EOL characters. The width is defined as the + maximum number of characters on a single line in the string + + Returns: + Integer defining the width of the cell where width is the maximum + number of characters on a single line + """ + if self._data is None: + return 0 + #if isinstance(self._data, (list, tuple)) + # return ','.join(str(item) for item in self._data) + # The following are all one line per cell. + if isinstance(self._data, (six.integer_types, float, bool)): + return len(str(self._data)) + + assert isinstance(self._data, six.string_types) + lines = self._data.split("\n") + return len(max(lines, key=len)) + + @property + def length(self): + """ + Return the length of the string value of the item + """ + if self._data is None: + return 0 + if isinstance(self.data, six.string_types): + return len(self._data) + else: + return len(str(self._data)) + + def __str__(self): + return self.data + + def __repr__(self): + return "TableCell {}".self.format(self.data) + + def fold(self, max_width, break_long_words=False, + break_on_hyphens=False, fold_list_items=False, separator=', ', + initial_indent='', subsequent_indent=''): + """ + Fold the data based on the max_length attribute. + """ + if isinstance(self._data, (six.string_types, list, tuple)): + self._data = fold_strings(self._data, max_width, + break_long_words=break_long_words, + break_on_hyphens=break_on_hyphens, + fold_list_items=fold_list_items, + separator=separator, + initial_indent=initial_indent, + subsequent_indent=subsequent_indent) + else: + assert False, "Fold failed bad type {}".format(type(self._data)) + + +class TableColumn(object): + """ + Represents a single column in a table. Made up of 0 or more + TableCell objects + """ + def __init__(self, cells): + """ + """ + if isinstance(cells, (tuple, list)): + self._column = cells + else: + self._column = [cells] + for item in cells: + assert isinstance(item, TableCell) + + #def __str__(self): + # return ", ".format(self._column) + + @property + def data(self): + """ + Returns the cells of the row possibly modified by fold, etc. + """ + return [cell.data for cell in self._column] + + def __repr__(self): + return ", ".join([str(item) for item in self._column]) + + def widths(self): + """ + Returns a list of the width of each cell entry in the column + """ + return [cell.width for cell in self._column] + + def max_width(self): + """ + Return the maximum cell width in a column + Parameters: + col (list/tuple of ints, floats, strings) + + Returns: + integer defining the maximum with of a cell in the column + """ + + assert isinstance(self._column, (list, tuple)) + + return max([cell.width for cell in self._column]) + + def fold(self, max_width, break_long_words=False, + break_on_hyphens=False, fold_list_items=False, separator=', ', + initial_indent='', subsequent_indent=''): + """ + Fold the cells in the column + """ + for cell in self._column: + fold_strings(self, max_width, break_long_words=break_long_words, + break_on_hyphens=break_on_hyphens, + fold_list_items=fold_list_items, separator=separator, + initial_indent=initial_indent, + subsequent_indent=subsequent_indent) + + +def _build_class_as_table(klass, table_width, table_format, context): + """ + + Parameters: + + klass (): + table_width + table_format - for class_ in classes: - click.echo(class_.tomof()) + Returns: + + Raises: TODO + """ + def build_qualifiers_cell(obj, width, exclude=[]): + """ + Build a multiline string for the names and values of the qualifiers + defined in qualifiers. Each qualifier name and value is in mof format + on one or more lines. + + Parameters: + + obj : + The CIM object from which the qualifiers are to be extracted + + width TODO + + exclude (NocaseList) + """ + qualifiers = obj.qualifiers.values() + qualifier_entries = [] + if not qualifiers: + return None + for qualifier in qualifiers: + #if qualifier.name in exclude: + # continue + if 'description' == qualifier.name.lower(): + continue + qualifier_entries.append( + qualifier.tomof(indent=0, maxline=width)) + + return TableCell("\n".join(qualifier_entries)) + + def get_classorigin(obj, classname): + """ + Return the class origin classname if class)origin exists + """ + # TODO: We need option to show all classnames + if obj.class_origin != classname: + return obj.class_origin + + return None + + def build_description_cell(obj, max_width): + """ + Get the description from the qualifiers attached to obj. Returns + the description value or None if there is no description + """ + if 'Description' in obj.qualifiers: + description = TableCell(obj.qualifiers['Description'].value) + description.fold(max_width) + return description + return TableCell(None) + + def build_type_cell(obj): + """ + Build a string that defines the object type, arrayness and if it + is a reference type, the Reference class. The object type string is the + actual pywbem string for that type except for reference which returns + "REF". + + If embedded_data is set, this is added to the type string as + EMB() + + Returns "", or "[]" or "[int]" and if reference + type "reference_class" + + """ + if obj.is_array: + array_size = str(obj.array_size) if obj.array_size else "" + array = "[{0}]".format(array_size) + else: + array = '' + if obj.embedded_object: + embed_object = "\nEMB(object.embedded_object)" + else: + embed_object = "" + if obj.type == 'reference': + return TableCell("{}{}({}{})".format('REF', array, + obj.reference_class, + embed_object)) + return TableCell("{}{}{}".format(obj.type, array, embed_object)) + + def build_parameters_subtable(parameters, width, table_format): + """ + Build a subtable of the parameters for a method + """ + qualifier_exclude_list = NocaseList(["Description"]) + rows = [] + if not parameters: + return None + headers = ["Name", "Type", "Value", "Description", "Qualifiers"] + + for param in parameters: + name_cell = TableCell(param.name) + type_and_array_cell = build_type_cell(param) + # TODO format value + value_cell = TableCell(param.value) + description_cell = build_description_cell(param, 40) + param_qualifiers_cell = build_qualifiers_cell( + param, width, exclude=qualifier_exclude_list) + + row = [name_cell.data, type_and_array_cell.data, + value_cell.data, description_cell.data, + param_qualifiers_cell.data] + rows.append(row) + + hide_empty_columns(headers, rows) + return format_table(rows, headers, table_format=table_format) + subtable_width = table_width - 12 + + # Build class subtable + subclasses_cell_width = 30 + subclasses = context.conn.EnumerateClassNames(ClassName=klass.path) + subclasses_cell = TableCell(subclasses, + max_width=subclasses_cell_width, + break_long_words=False, + break_on_hyphens=False, + fold_list_items=True) + + # Create class qualifiers subtable + qualifier_exclude_list = NocaseList([]) + qual_cell_width = subtable_width - 16 if subtable_width > 90 else 12 + qualifiers_cell = build_qualifiers_cell(klass, + qual_cell_width, + exclude=qualifier_exclude_list) + + class_header = ['Superclass', 'Subclasses', 'Description', 'Qualifiers'] + + superclass_cell = TableCell(klass.superclass) + other_cells_width = superclass_cell.width + qualifiers_cell.width + \ + subclasses_cell.width + 12 + description_width = max([subtable_width - other_cells_width, 34]) + description_cell = build_description_cell(klass, description_width) + class_row = [superclass_cell.data, subclasses_cell.data, + description_cell.data, qualifiers_cell.data] + + class_subtable = format_table([class_row], class_header, + table_format=table_format, + hide_empty_cols=True) + + # Build property subtable + property_rows = [] + headers = ["Property\nName", "Type", "Default\nValue", "Description", + "Class Origin", "Embedded Obj", "Qualifiers"] + for property in klass.properties.values(): + qualifier_width = subtable_width - (8 + 50) + qualifiers_cell = build_qualifiers_cell( + property, qualifier_width, exclude=qualifier_exclude_list) + + type_and_array_cell = build_type_cell(property) + + class_origin_cell = TableCell(get_classorigin(property, + klass.classname)) + description_cell = build_description_cell(property, 50) + + row = [property.name, type_and_array_cell.data, property.value, + description_cell.data, class_origin_cell.data, + property.embedded_object, qualifiers_cell.data] + property_rows.append(row) + + property_subtable = format_table(property_rows, headers, + table_format=table_format, + hide_empty_cols=True) + + # Build method subtable and parameter subtable for each method + # Format methods and a subtable for parameters. + method_rows = [] + headers = ["MethodName\nRtnType", "Class\nOrigin", "Description", + "Qualifiers", "Parameters"] + for method in klass.methods.values(): + method_name = "{}({})".format(method.name, method.return_type) + name_cell = TableCell(method_name) + + method_qualifiers_cell = build_qualifiers_cell( + method, 12, exclude=qualifier_exclude_list) + + class_origin_cell = TableCell(get_classorigin(method, klass.classname)) + description_cell = build_description_cell(method, 30) + + parameters_subtable_width = max(subtable_width - 80, 40) + parameters_subtable = build_parameters_subtable( + method.parameters.values(), parameters_subtable_width, table_format) + + + + row = [name_cell.data, class_origin_cell.data, + description_cell.data, + method_qualifiers_cell.data, + parameters_subtable] + + method_rows.append(row) + methods_subtable = format_table(method_rows, headers, + table_format=table_format, + hide_empty_cols=True) + + overall_rows = [[class_subtable], [property_subtable], [methods_subtable]] + + overall_headers = [klass.classname] + table = format_table(overall_rows, overall_headers, + table_format=table_format, + hide_empty_cols=True) + return table + + +# TODO: We are inconsistent with context as optional or required +def _display_classes_as_table(classes, table_width, table_format, context=None): + """ + Display one or more CIM class objects as a table. + """ + # pylint: disable=unused-argument + for klass in classes: + class_table = _build_class_as_table(klass, + table_width, + table_format, + context) + click.echo(class_table) def _display_paths_as_table(objects, table_width, table_format): diff --git a/tests/unit/test_display_cimobjects.py b/tests/unit/test_display_cimobjects.py index 8c40eeed..170764a7 100644 --- a/tests/unit/test_display_cimobjects.py +++ b/tests/unit/test_display_cimobjects.py @@ -37,7 +37,8 @@ from tests.unit.pytest_extensions import simplified_test_function from pywbemtools.pywbemcli._display_cimobjects import \ - _format_instances_as_rows, _display_instances_as_table + _format_instances_as_rows, _display_instances_as_table, \ + TableCell, TableColumn OK = True # mark tests OK when they execute correctly RUN = True # Mark OK = False and current test case being created RUN @@ -552,3 +553,168 @@ def test_display_instances_as_table( "Expected:\n" \ "{}\n" \ "End\n".format(desc, stdout, exp_stdout) + + +# Testcases for _cell_width() + + # Each list item is a testcase tuple with these items: + # * desc: Short testcase description. + # * kwargs: Keyword arguments for the test function: + # * input: Multiline string or integer or float or bool. + # * exp_rtn: width of cell with maximum length. + # * exp_exc_types: Expected exception type(s), or None. + # * exp_rtn: Expected warning type(s), or None. + # * condition: Boolean condition for testcase to run, or 'pdb' for debugger + + +TESTCASES_CELL_WIDTH = [ + ( + "Verify cell width with two line string", + dict( + input="1\ntwo", + exp_rtn=[3, 5], + ), + None, None, True, ), + ( + "Verify cell width with single string", + dict( + input="two", + exp_rtn=[3, 3], + ), + None, None, True), + ( + "Verify cell width with multiline string", + dict( + input="two\nthree\nthis is long string", + exp_rtn=[19, 29], + ), + None, None, True), + + ( + "Verify cell width with empty string", + dict( + input="", + exp_rtn=[0, 0], + ), + None, None, True), + + ( + "Verify cell width with None", + dict( + input=None, + exp_rtn=[0, 0], + ), + None, None, True), + + ( + "Verify cell width with integer", + dict( + input=1, + exp_rtn=[1, 1], + ), + None, None, True), + + ( + "Verify cell width with bool", + dict( + input=False, + exp_rtn=[5, 5], + ), + None, None, True), +] + + +@pytest.mark.parametrize( + "desc, kwargs, exp_exc_types, exp_warn_types, condition", + TESTCASES_CELL_WIDTH) +@simplified_test_function +def test_table_cell(testcase, input, exp_rtn): + """ + Test the output of the common _format_instances_as_rows() function + """ + # The code to be tested + cell = TableCell(input) + cell_width = cell.width + cell_length = cell.length + + # Ensure that exceptions raised in the remainder of this function + # are not mistaken as expected exceptions + assert testcase.exp_exc_types is None + + assert cell_width == exp_rtn[0] + assert cell_length == exp_rtn[1] + assert input == cell.data + +# Testcases for _max_cell_width_in_col() + + # Each list item is a testcase tuple with these items: + # * desc: Short testcase description. + # * kwargs: Keyword arguments for the test function: + # * input: list of multiline items representing cells in column + # * exp_rtn: length of cell with maximum width. + # * exp_exc_types: Expected exception type(s), or None. + # * exp_rtn: Expected warning type(s), or None. + # * condition: Boolean condition for testcase to run, or 'pdb' for debugger + + +TESTCASES_TABLECOLUMN = [ + ( + "Verify maximum line with multiple cells", + dict( + input=["1\ntwo", "", "blahblah", "this\nis\nme"], + exp_rtn=8, + ), + None, None, True, ), + ( + "Verify maximum list with one cell", + dict( + input=["two"], + exp_rtn=3, + ), + None, None, True, ), + ( + "Verify maximum cell with one cell in col", + dict( + input=["two\nthree\nthis is long string"], + exp_rtn=19, + ), + None, None, True, ), + + ( + "Verify maximum cell size with multiple lines of integers", + dict( + input=[1, 2, 9999], + exp_rtn=4, + ), + None, None, True, ), + + ( + "Verify maximum cell size with multiple bools", + dict( + input=[True, False, None, True], + exp_rtn=5, + ), + None, None, True, ), +] + + +@pytest.mark.parametrize( + "desc, kwargs, exp_exc_types, exp_warn_types, condition", + TESTCASES_TABLECOLUMN) +@simplified_test_function +def test_TableColumn(testcase, input, exp_rtn): + """ + Test the output of the common max_col_width functionble + """ + + + inputs = [TableCell(item) for item in input] + cols = TableColumn(inputs) + act_max_width = cols.max_width() + + # Ensure that exceptions raised in the remainder of this function + # are not mistaken as expected exceptions + assert testcase.exp_exc_types is None + + assert act_max_width == exp_rtn + assert input == cols.data