Skip to content

Commit

Permalink
CRAYSAT-1936: add ability to sort reports by multiple fields
Browse files Browse the repository at this point in the history
  • Loading branch information
haasken-hpe authored and ethanholen-hpe committed Dec 10, 2024
1 parent 84043a3 commit bf10b01
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 38 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.33.3] - 2024-11-26

### Added
- Added the ability to sort reports by multiple fields

## [3.33.2] - 2024-11-26

### Fixed
Expand Down
8 changes: 7 additions & 1 deletion docs/man/_sat-format-opts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ These options govern the format of the output.
Reverses the sorting order.

**--sort-by HEADING**
Sort by the selected heading. Can also accept a 0-based column index.
Sort by the selected heading or comma-separated list of headings.
Can also accept a 0-based column index or comma-separated list
of 0-based column indexes.
E.g. "--sort-by product_name,product_version" will sort
results by product name and then by product version. Can accept a column
name or a 0-based index. Enclose the column name in
double quotes if it contains a space.

**--show-empty**
Show values for columns even if every value is ``EMPTY``. By default,
Expand Down
7 changes: 6 additions & 1 deletion sat/cli/showrev/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,13 @@ def do_showrev(args):
"""
reports = []

# The `showrev` command sets the default of `--sort-by` to None, so we can use that to
# determine if the user explicitly set the value, and use a special default if not.
if args.sort_by is None:
sort_by = ['product_name', 'product_version']
else:
sort_by = args.sort_by
# report formatting
sort_by = args.sort_by
reverse = args.reverse
no_headings = get_config_value('format.no_headings')
no_borders = get_config_value('format.no_borders')
Expand Down
4 changes: 2 additions & 2 deletions sat/cli/showrev/parser.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# MIT License
#
# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP
# (C) Copyright 2019-2020, 2024 Hewlett Packard Enterprise Development LP
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
Expand Down Expand Up @@ -39,7 +39,7 @@ def add_showrev_subparser(subparsers):
None
"""

format_options = sat.parsergroups.create_format_options()
format_options = sat.parsergroups.create_format_options(sort_by_default=None)
filter_options = sat.parsergroups.create_filter_options()

showrev_parser = subparsers.add_parser(
Expand Down
26 changes: 20 additions & 6 deletions sat/parsergroups.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# MIT License
#
# (C) Copyright 2019-2022 Hewlett Packard Enterprise Development LP
# (C) Copyright 2019-2022, 2024 Hewlett Packard Enterprise Development LP
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
Expand Down Expand Up @@ -36,7 +36,16 @@
LOGGER = logging.getLogger(__name__)


def create_format_options():
def create_format_options(sort_by_default='0'):
"""Creates a parser containing options for formatting.
sort_by_default: allows for the value of --sort_by to be set
when calling the method. It is set to 0 or the
first column of data being sorted.
Returns: an ArgumentParser object configured with options and help
text for formatting.
"""
parser = ArgumentParser(add_help=False)

group = parser.add_argument_group(
Expand Down Expand Up @@ -64,10 +73,15 @@ def create_format_options():
default=False, action='store_true')

group.add_argument(
'--sort-by', metavar='FIELD', default=0,
help=('Select which column to sort by. Can accept a column name '
'or a 0-based index. Enclose the column name in double quotes '
'if it contains a space.'))
'--sort-by', default=sort_by_default,
type=lambda v: v.split(','),
help=('Sort by the selected heading or comma-separated list of '
'headings. Can also accept a 0-based column index or '
'comma-separated list of 0-based column indexes.'
'E.g. "--sort-by product_name,product_version" will sort '
'results by product name and then by product version. Can accept a column '
'name or a 0-based index. Enclose the column name in '
'double quotes if it contains a space.'))

group.add_argument(
'--show-empty',
Expand Down
57 changes: 33 additions & 24 deletions sat/report.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# MIT License
#
# (C) Copyright 2019-2023 Hewlett Packard Enterprise Development LP
# (C) Copyright 2019-2024 Hewlett Packard Enterprise Development LP
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
Expand Down Expand Up @@ -93,9 +93,8 @@ def __init__(self, headings, title=None,
Args:
headings: Headings for the table's columns.
title: Title for the table
sort_by: Sort the output by the desired column when printing
in tabular format. Can be the name of a heading, or a 0-based
index.
sort_by: List of columns to sort by when printing in tabular format.
Each column can be the name of a heading, or a 0-based index.
reverse: If True, then reverse the sorting order.
no_headings: If True, then omit the title block and column
headings from the display.
Expand Down Expand Up @@ -157,21 +156,30 @@ def __init__(self, headings, title=None,
LOGGER.warning("See the man page for this subcommand for further details on filter syntax.")
sys.exit(1)

# find the heading to sort on
if sort_by is not None:
warn_str = "Element '%s' is not in %s. Output will be unsorted."
try:
self.sort_by = int(self.sort_by)
self.sort_by = self.headings[self.sort_by]
except IndexError:
# sort_by is out of range.
LOGGER.warning(warn_str, sort_by, self.headings)
# find the heading(s) to sort on
if self.sort_by is not None:
if not isinstance(self.sort_by, list):
self.sort_by = [self.sort_by]
warn_str = "Element '%s' is not in %s. Output will be unsorted on that element."
valid_sort_by = []
for i in range(len(self.sort_by)):
try:
index = int(self.sort_by[i])
valid_sort_by.append(self.headings[index])
except IndexError:
# sort_by is out of range.
LOGGER.warning(warn_str, self.sort_by[i], self.headings)
except ValueError:
# sort_by is not an int.
if match_query_key(sort_by[i], self.headings):
valid_sort_by.append(match_query_key(self.sort_by[i], headings))
else:
LOGGER.warning(warn_str, self.sort_by[i], self.headings)

self.sort_by = valid_sort_by

if self.sort_by == []:
self.sort_by = None
except ValueError:
# sort_by is not an int.
self.sort_by = match_query_key(self.sort_by, headings)
if not self.sort_by:
LOGGER.warning(warn_str, sort_by, self.headings)

if display_headings is not None:
self.display_headings = []
Expand Down Expand Up @@ -290,12 +298,13 @@ def sort_data(self):
If `self.sort_by` is None, no sorting is done.
"""
if self.sort_by is not None:
try:
self.data.sort(key=lambda d: d[self.sort_by], reverse=self.reverse)
except TypeError:
LOGGER.info("Converting all values of '%s' field to str "
"to allow sorting.", self.sort_by)
self.data.sort(key=lambda d: str(d[self.sort_by]), reverse=self.reverse)
for element in reversed(self.sort_by):
try:
self.data.sort(key=lambda d: d[element], reverse=self.reverse)
except TypeError:
LOGGER.info("Converting all values of '%s' field to str "
"to allow sorting.", self.sort_by)
self.data.sort(key=lambda d: str(d[element]), reverse=self.reverse)

def remove_empty_and_missing(self, data_rows):
"""Removes columns which have only EMPTY_VALUE or MISSING_VALUE.
Expand Down
48 changes: 44 additions & 4 deletions tests/test_report.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# MIT License
#
# (C) Copyright 2019-2023 Hewlett Packard Enterprise Development LP
# (C) Copyright 2019-2024 Hewlett Packard Enterprise Development LP
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
Expand Down Expand Up @@ -390,6 +390,33 @@ def test_sorted_print(self):
for expected, actual in zip(self.entries, pt_s):
self.assertEqual(expected, actual)

def test_sorted_list_print(self):
"""The internal PT should be sorted on the first column and then the second column
"""
report = Report(self.headings, sort_by=[0, 1])

report.add_rows(self.out_of_order)
pt_s = get_report_printed_list(report)

for expected, actual in zip(self.entries, pt_s):
self.assertEqual(expected, actual)

def test_sorted_list_with_matches_print(self):
"""Test sort on first and second column when elements are equal in first column
"""
e1 = ['alice', 'mars', 'red']
e2 = ['bob', 'venus', 'blue']
e3 = ['charlie', 'earth', 'purple']
e4 = ['alice', 'earth', 'green']
out_of_order = [e1, e3, e2, e4]
expected_order = [e4, e1, e2, e3]
report = Report(self.headings, sort_by=[0, 1])
report.add_rows(out_of_order)

pt_s = get_report_printed_list(report)

self.assertEqual(expected_order, pt_s)

def test_sorted_yaml(self):
"""The YAML output list should be sorted as well.
"""
Expand Down Expand Up @@ -467,6 +494,19 @@ def test_sort_heading_invalid(self):
for expected, actual in zip(self.out_of_order, pt_s):
self.assertEqual(expected, actual)

def test_sort_heading_invalid_multiple(self):
"""The PT should default to not sorting if the heading is not present.
"""
report = Report(self.headings, sort_by=['does-not-exist', 'does-not-exist-two'])

self.assertEqual(None, report.sort_by)

report.add_rows(self.out_of_order)
pt_s = get_report_printed_list(report)

for expected, actual in zip(self.out_of_order, pt_s):
self.assertEqual(expected, actual)

def test_sort_heading_idx_invalid(self):
"""The PT should not sort if the idx is out-of-range.
"""
Expand All @@ -485,7 +525,7 @@ def test_sort_heading_abbreviation(self):
"""
report = Report(self.headings, sort_by='pl')

self.assertEqual('place', report.sort_by)
self.assertEqual(['place'], report.sort_by)

report.add_rows(self.out_of_order)
pt_s = get_report_printed_list(report)
Expand All @@ -497,7 +537,7 @@ def test_sort_unique_subsequence(self):
"""The report should sort on a unique subsequence.
"""
report = Report(self.headings, sort_by='clr')
self.assertEqual('color', report.sort_by)
self.assertEqual(['color'], report.sort_by)

report.add_rows(self.out_of_order)
pt_s = get_report_printed_list(report)
Expand All @@ -509,7 +549,7 @@ def test_sort_ambiguous_subsequence(self):
"""The report should sort on the first match.
"""
report = Report(self.headings, sort_by='ae')
self.assertEqual('name', report.sort_by)
self.assertEqual(['name'], report.sort_by)

report.add_rows(self.out_of_order)
pt_s = get_report_printed_list(report)
Expand Down

0 comments on commit bf10b01

Please sign in to comment.