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'},