From 3a9493f282c283206a2645f4adac3a1bd84c024d Mon Sep 17 00:00:00 2001 From: kschopmeyer Date: Sun, 26 Dec 2021 12:54:15 -0600 Subject: [PATCH] Change output format for instance names table in display_cimobject The original output formatting for instance enumerate --no was useless. It showed 3 4 columns host, ns, class, keybindings where the keybindings was a single string. Modified it to show" host, namespace, class, one column per key which allows the user to compare keys between instances. This does not do anything for associations where the reference properties are keys, however. In the case of enumerates of instances starting from a point above the hierarchy (ex. CIM_ManagedElement) it produces a table for each set of key definitions (i.e. all the instances with the same keys in the same table. This means producing multiple tables. Note also, that the elements in each table are only linked by 1. They are instances of some class in the hierarchy. 2. They have the same keynames. They may be instances that are not strongly related (i.e. from completely different parts of the hiearchy. Fixes the current tests for instance enumerate ... --no --- docs/changes.rst | 3 + pywbemtools/pywbemcli/_display_cimobjects.py | 95 +++++++++++++---- tests/unit/pywbemcli/test_instance_cmds.py | 100 ++++++++++-------- .../unit/pywbemcli/test_subscription_cmds.py | 14 +-- 4 files changed, 140 insertions(+), 72 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 0b382206..992b3e89 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -138,6 +138,9 @@ Released: not yet * Implement an end-end test for the subscription command group. +* Changed output format for table output of instance enumerate --no option to + show each key as a column in the table so that keys are more readable. + **Cleanup:** * Prepared the development environment for having more than one pywbemtools diff --git a/pywbemtools/pywbemcli/_display_cimobjects.py b/pywbemtools/pywbemcli/_display_cimobjects.py index 69df9407..5bc3749e 100644 --- a/pywbemtools/pywbemcli/_display_cimobjects.py +++ b/pywbemtools/pywbemcli/_display_cimobjects.py @@ -31,11 +31,11 @@ CIMError, CIM_ERR_NOT_SUPPORTED from pywbem._nocasedict import NocaseDict -from ._common import sort_cimobjects +from ._common import sort_cimobjects, to_wbem_uri_folded from ._cimvalueformatter import cimvalue_to_fmtd_string from .._utils import get_terminal_width from .._output_formatting import DEFAULT_MAX_CELL_WIDTH, \ - output_format_is_table, format_table, fold_strings, format_keys + output_format_is_table, format_table, fold_strings INT_TYPE_PATTERN = re.compile(r'^[su]int(8|16|32|64)$') @@ -296,30 +296,83 @@ def _display_paths_as_table(objects, table_width, table_format): title = 'Classnames:' headers = ['Class Name'] rows = [[obj] for obj in objects] + elif isinstance(objects[0], CIMClassName): title = 'Classnames' headers = ('host', 'namespace', 'class') rows = [[obj.host, obj.namespace, obj.classname] for obj in objects] + elif isinstance(objects[0], CIMInstanceName): - title = 'InstanceNames: {}'.format(objects[0].classname) - host_hdr = 'host' - ns_hdr = 'namespace' - class_hdr = 'class' - host_hdr_len = len(host_hdr) + 4 - ns_hdr_len = len(ns_hdr) + 3 - class_hdr_len = len(class_hdr) + 3 - headers = (host_hdr, ns_hdr, class_hdr, 'keysbindings') - - host_lens = [len(obj.host) for obj in objects if obj.host] - host_max = max(host_lens) if host_lens else host_hdr_len - ns_lens = [len(obj.namespace) for obj in objects if obj.namespace] - ns_max = max(ns_lens) if ns_lens else ns_hdr_len - class_lens = [len(obj.classname) for obj in objects] - class_max = max(class_lens) if class_lens else class_hdr_len - - max_key_len = (table_width) - (host_max + ns_max + class_max + 3) - rows = [[obj.host, obj.namespace, obj.classname, - format_keys(obj, max_key_len)] for obj in objects] + # Since there are cases where the list has instances with + # different keys (i.e. enumerate CIM_ManagedElement), build + # a dictionary for instances with the same key names. + # Sort into dictionary for each set of key names. If all keys are + # the same, there will be one table. This allows building + # a table for each set of keynames + objs_by_key_set = {} + original_keys = {} + + for instname in objects: + # key_names is a tuple of sorted key names in lower case so + # all instances with the same set of keys ends up in a + # single dictionary item. NOTE: objects must be tuple to be + # a dictionary key, the key. + test_key_names = tuple( + sorted(map(lambda x: x.lower(), instname.keys()))) + try: + objs_by_key_set[test_key_names].append(instname) + except KeyError: + objs_by_key_set[test_key_names] = [instname] + + # Set the original keys into dict to recover correct key + # case later + if test_key_names not in original_keys: + original_keys[test_key_names] = instname.keys() + + # If multiple key_names we create multiple tables and add table + # number to each table title. + table_number = 0 + for key_names, inst_names in objs_by_key_set.items(): + # Build headers for this table with the common elements and key + # names for each key in the object. We use inst_names[0] to + # restore original case to key strings. + inst_keys = original_keys[key_names] + + rows = [] + for instname in inst_names: + # Build header row for this table + row = [instname.host, instname.namespace, + instname.classname] + for key in inst_keys: + if isinstance(instname[key], CIMInstanceName): + # If key is CIMInstanceName, fold the value + row.append(to_wbem_uri_folded( + instname[key], format='standard', max_len=30)) + else: + row.append(instname[key]) + rows.append(row) + + # If multiple tables, number them as hint to reader that there + # are multiples. + if len(objs_by_key_set) > 1: + table_number += 1 + table_number_str = ", (table #{})".format(table_number) + else: + table_number_str = '' + + headers = ['host', 'namespace', 'class'] + \ + ["key=\n{0}".format(kn) for kn in inst_keys] + + title = 'InstanceNames: {0}{1}'.format(inst_names[0].classname, + table_number_str) + + # Generate multiple tables, one for each key_name and + # return local to this scope. + click.echo(format_table(rows, headers, title=title, + table_format=table_format)) + + return # Return to avoid following table output + else: raise click.ClickException("{0} invalid type ({1})for path display". format(objects[0], type(objects[0]))) diff --git a/tests/unit/pywbemcli/test_instance_cmds.py b/tests/unit/pywbemcli/test_instance_cmds.py index 624d5454..54471da4 100644 --- a/tests/unit/pywbemcli/test_instance_cmds.py +++ b/tests/unit/pywbemcli/test_instance_cmds.py @@ -806,35 +806,25 @@ def GET_TEST_PATH_STR(filename): # pylint: disable=invalid-name ['Verify instance command -o grid enumerate CIM_Foo --di --no', {'args': ['enumerate', 'CIM_Foo', '--di', '--no'], - 'general': ['--output-format', 'grid']}, + 'general': ['--output-format', 'table']}, {'stdout': """InstanceNames: CIM_Foo -+--------+-------------+-----------------+-------------------------------+ -| host | namespace | class | keysbindings | -+========+=============+=================+===============================+ -| | root/cimv2 | CIM_Foo | InstanceID="CIM_Foo1" | -+--------+-------------+-----------------+-------------------------------+ -| | root/cimv2 | CIM_Foo | InstanceID="CIM_Foo2" | -+--------+-------------+-----------------+-------------------------------+ -| | root/cimv2 | CIM_Foo | InstanceID="CIM_Foo3" | -+--------+-------------+-----------------+-------------------------------+ -| | root/cimv2 | CIM_Foo | InstanceID="CIM_Foo30" | -+--------+-------------+-----------------+-------------------------------+ -| | root/cimv2 | CIM_Foo | InstanceID="CIM_Foo31" | -+--------+-------------+-----------------+-------------------------------+ -| | root/cimv2 | CIM_Foo_sub | InstanceID="CIM_Foo_sub1" | -+--------+-------------+-----------------+-------------------------------+ -| | root/cimv2 | CIM_Foo_sub | InstanceID="CIM_Foo_sub2" | -+--------+-------------+-----------------+-------------------------------+ -| | root/cimv2 | CIM_Foo_sub | InstanceID="CIM_Foo_sub3" | -+--------+-------------+-----------------+-------------------------------+ -| | root/cimv2 | CIM_Foo_sub | InstanceID="CIM_Foo_sub4" | -+--------+-------------+-----------------+-------------------------------+ -| | root/cimv2 | CIM_Foo_sub_sub | InstanceID="CIM_Foo_sub_sub1" | -+--------+-------------+-----------------+-------------------------------+ -| | root/cimv2 | CIM_Foo_sub_sub | InstanceID="CIM_Foo_sub_sub2" | -+--------+-------------+-----------------+-------------------------------+ -| | root/cimv2 | CIM_Foo_sub_sub | InstanceID="CIM_Foo_sub_sub3" | -+--------+-------------+-----------------+-------------------------------+ ++--------+-------------+-----------------+------------------+ +| host | namespace | class | key= | +| | | | InstanceID | +|--------+-------------+-----------------+------------------| +| | root/cimv2 | CIM_Foo | CIM_Foo1 | +| | root/cimv2 | CIM_Foo | CIM_Foo2 | +| | root/cimv2 | CIM_Foo | CIM_Foo3 | +| | root/cimv2 | CIM_Foo | CIM_Foo30 | +| | root/cimv2 | CIM_Foo | CIM_Foo31 | +| | root/cimv2 | CIM_Foo_sub | CIM_Foo_sub1 | +| | root/cimv2 | CIM_Foo_sub | CIM_Foo_sub2 | +| | root/cimv2 | CIM_Foo_sub | CIM_Foo_sub3 | +| | root/cimv2 | CIM_Foo_sub | CIM_Foo_sub4 | +| | root/cimv2 | CIM_Foo_sub_sub | CIM_Foo_sub_sub1 | +| | root/cimv2 | CIM_Foo_sub_sub | CIM_Foo_sub_sub2 | +| | root/cimv2 | CIM_Foo_sub_sub | CIM_Foo_sub_sub3 | ++--------+-------------+-----------------+------------------+ """, 'test': 'innows'}, SIMPLE_MOCK_FILE, OK], @@ -946,28 +936,48 @@ def GET_TEST_PATH_STR(filename): # pylint: disable=invalid-name {'args': ['enumerate', 'CIM_Foo', '--names-only'], 'general': ['--output-format', 'table']}, {'stdout': """InstanceNames: CIM_Foo -+--------+-------------+-----------------+-------------------------------+ -| host | namespace | class | keysbindings | -|--------+-------------+-----------------+-------------------------------| -| | root/cimv2 | CIM_Foo | InstanceID="CIM_Foo1" | -| | root/cimv2 | CIM_Foo | InstanceID="CIM_Foo2" | -| | root/cimv2 | CIM_Foo | InstanceID="CIM_Foo3" | -| | root/cimv2 | CIM_Foo | InstanceID="CIM_Foo30" | -| | root/cimv2 | CIM_Foo | InstanceID="CIM_Foo31" | -| | root/cimv2 | CIM_Foo_sub | InstanceID="CIM_Foo_sub1" | -| | root/cimv2 | CIM_Foo_sub | InstanceID="CIM_Foo_sub2" | -| | root/cimv2 | CIM_Foo_sub | InstanceID="CIM_Foo_sub3" | -| | root/cimv2 | CIM_Foo_sub | InstanceID="CIM_Foo_sub4" | -| | root/cimv2 | CIM_Foo_sub_sub | InstanceID="CIM_Foo_sub_sub1" | -| | root/cimv2 | CIM_Foo_sub_sub | InstanceID="CIM_Foo_sub_sub2" | -| | root/cimv2 | CIM_Foo_sub_sub | InstanceID="CIM_Foo_sub_sub3" | -+--------+-------------+-----------------+-------------------------------+ - ++--------+-------------+-----------------+------------------+ +| host | namespace | class | key= | +| | | | InstanceID | +|--------+-------------+-----------------+------------------| +| | root/cimv2 | CIM_Foo | CIM_Foo1 | +| | root/cimv2 | CIM_Foo | CIM_Foo2 | +| | root/cimv2 | CIM_Foo | CIM_Foo3 | +| | root/cimv2 | CIM_Foo | CIM_Foo30 | +| | root/cimv2 | CIM_Foo | CIM_Foo31 | +| | root/cimv2 | CIM_Foo_sub | CIM_Foo_sub1 | +| | root/cimv2 | CIM_Foo_sub | CIM_Foo_sub2 | +| | root/cimv2 | CIM_Foo_sub | CIM_Foo_sub3 | +| | root/cimv2 | CIM_Foo_sub | CIM_Foo_sub4 | +| | root/cimv2 | CIM_Foo_sub_sub | CIM_Foo_sub_sub1 | +| | root/cimv2 | CIM_Foo_sub_sub | CIM_Foo_sub_sub2 | +| | root/cimv2 | CIM_Foo_sub_sub | CIM_Foo_sub_sub3 | ++--------+-------------+-----------------+------------------+ """, 'rc': 0, 'test': 'linesnows'}, SIMPLE_MOCK_FILE, OK], + ['Verify command enumerate with ssociation class inst name table output', + {'args': ['enumerate', 'TST_A3', '--names-only'], + 'general': ['--output-format', 'table']}, + {'stdout': """InstanceNames: TST_A3 ++--------+-------------+---------+---------------------+---------------------+---------------------+ +| host | namespace | class | key= | key= | key= | +| | | | Initiator | Target | LogicalUnit | +|--------+-------------+---------+---------------------+---------------------+---------------------| +| | root/cimv2 | TST_A3 | /root/cimv2:TST_EP. | /root/cimv2:TST_EP. | /root/cimv2:TST_LD. | +| | | | InstanceID=1 | InstanceID=2 | InstanceID=3 | +| | root/cimv2 | TST_A3 | /root/cimv2:TST_EP. | /root/cimv2:TST_EP. | /root/cimv2:TST_LD. | +| | | | InstanceID=1 | InstanceID=5 | InstanceID=6 | +| | root/cimv2 | TST_A3 | /root/cimv2:TST_EP. | /root/cimv2:TST_EP. | /root/cimv2:TST_LD. | +| | | | InstanceID=1 | InstanceID=7 | InstanceID=8 | ++--------+-------------+---------+---------------------+---------------------+---------------------+ +""", # noqa: E501 + 'rc': 0, + 'test': 'linesnows'}, + COMPLEX_ASSOC_MODEL, OK], + ['Verify command enumerate with CIM_Foo summary table output', {'args': ['enumerate', 'CIM_Foo', '--summary'], 'general': ['--output-format', 'table']}, diff --git a/tests/unit/pywbemcli/test_subscription_cmds.py b/tests/unit/pywbemcli/test_subscription_cmds.py index aff02b76..dea84e74 100644 --- a/tests/unit/pywbemcli/test_subscription_cmds.py +++ b/tests/unit/pywbemcli/test_subscription_cmds.py @@ -784,8 +784,9 @@ def GET_TEST_PATH_STR(filename): # pylint: disable=invalid-name ' Destination = "http://someone:50000";', ' Protocol = 2;', # Result -o plain subscription list-destinations --names-only - 'host namespace class keysbindings', - 'interop CIM_ListenerDestinationCIMXML', + 'host namespace class key= key= key=', + 'interop CIM_ListenerDestinationCIMXML CIM_ComputerSystem ' + 'MockSystem_WBEMServerTest CIM_ListenerDestinationCIMXML', # Result list '1 CIMInstance(s) returned', '};'], @@ -834,8 +835,8 @@ def GET_TEST_PATH_STR(filename): # pylint: disable=invalid-name '};', '1 CIMInstance(s) returned', # Result from -o plain subscription list-filters --names-only - 'host namespace class keysbindings', - 'interop CIM_IndicationFilter', ], + 'host namespace class key= key= key= key=', + 'interop CIM_IndicationFilter CIM_ComputerSystem MockSystem_WBEMServerTest CIM_IndicationFilter pywbemfilter:defaultpywbemcliSubMgr:ofilter1', ], # noqa: E501 'stderr': [], 'test': 'innows'}, None, OK], @@ -874,8 +875,9 @@ def GET_TEST_PATH_STR(filename): # pylint: disable=invalid-name ' SubscriptionState = 2;', '};', # Limited result -o plain ... list-subscriptions --name-only - # The actual output it to ugly to compare. - 'host namespace class keysbindings', + 'host namespace class key=', + 'Filter Handler', + 'interop CIM_IndicationSubscription /interop:CIM_IndicationFilter. /interop:CIM_ListenerDestinationCIMXML.', # noqa: E501 '};'], 'stderr': [], 'test': 'innows'},