Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CRAYSAT-1936: add ability to sort reports by multiple fields #295

Merged
merged 1 commit into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
ethanholen-hpe marked this conversation as resolved.
Show resolved Hide resolved
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'):
ethanholen-hpe marked this conversation as resolved.
Show resolved Hide resolved
"""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)

ethanholen-hpe marked this conversation as resolved.
Show resolved Hide resolved
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
Loading