Skip to content

Commit

Permalink
Add '--sort-ascending', '--sort-descending' parameters
Browse files Browse the repository at this point in the history
Allow users to reverse sorting direction.

Change-Id: Iecd539139c5a7ce4abaaee2ff5632a2459437d51
Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
  • Loading branch information
stephenfin committed Jan 29, 2021
1 parent c1c9910 commit 7798cb2
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 21 deletions.
67 changes: 46 additions & 21 deletions cliff/lister.py
Expand Up @@ -10,17 +10,16 @@
# License for the specific language governing permissions and limitations
# under the License.

"""Application base class for providing a list of data as output.
"""
"""Application base class for providing a list of data as output."""

import abc
import logging

from . import display


class Lister(display.DisplayCommandBase, metaclass=abc.ABCMeta):
"""Command base class for providing a list of data as output.
"""
"""Command base class for providing a list of data as output."""

log = logging.getLogger(__name__)

Expand All @@ -37,13 +36,16 @@ def need_sort_by_cliff(self):
"""Whether sort procedure is performed by cliff itself.
Should be overridden (return False) when there is a need to implement
custom sorting procedure or data is already sorted."""
custom sorting procedure or data is already sorted.
"""
return True

@abc.abstractmethod
def take_action(self, parsed_args):
"""Return a tuple containing the column names and an iterable
containing the data to be listed.
"""Run command.
Return a tuple containing the column names and an iterable containing
the data to be listed.
"""

def get_parser(self, prog_name):
Expand All @@ -55,16 +57,36 @@ def get_parser(self, prog_name):
default=[],
dest='sort_columns',
metavar='SORT_COLUMN',
help=("specify the column(s) to sort the data (columns specified "
"first have a priority, non-existing columns are ignored), "
"can be repeated")
help=(
'specify the column(s) to sort the data (columns specified '
'first have a priority, non-existing columns are ignored), '
'can be repeated'
),
)
sort_dir_group = group.add_mutually_exclusive_group()
sort_dir_group.add_argument(
'--sort-ascending',
action='store_const',
dest='sort_direction',
const='asc',
help=('sort the column(s) in ascending order'),
)
sort_dir_group.add_argument(
'--sort-descending',
action='store_const',
dest='sort_direction',
const='desc',
help=('sort the column(s) in descending order'),
)
return parser

def produce_output(self, parsed_args, column_names, data):
if parsed_args.sort_columns and self.need_sort_by_cliff:
indexes = [column_names.index(c) for c in parsed_args.sort_columns
if c in column_names]
indexes = [
column_names.index(c) for c in parsed_args.sort_columns
if c in column_names
]
reverse = parsed_args.sort_direction == 'desc'
for index in indexes[::-1]:
try:
# We need to handle unset values (i.e. None) so we sort on
Expand All @@ -76,6 +98,7 @@ def produce_output(self, parsed_args, column_names, data):
# the same, i.e. both None or both not-None
data = sorted(
data, key=lambda k: (k[index] is None, k[index]),
reverse=reverse,
)
except TypeError:
# Simply log and then ignore this; sorting is best effort
Expand All @@ -84,18 +107,20 @@ def produce_output(self, parsed_args, column_names, data):
parsed_args.sort_columns[index],
)

(columns_to_include, selector) = self._generate_columns_and_selector(
parsed_args, column_names)
columns_to_include, selector = self._generate_columns_and_selector(
parsed_args, column_names,
)
if selector:
# Generator expression to only return the parts of a row
# of data that the user has expressed interest in
# seeing. We have to convert the compress() output to a
# list so the table formatter can ask for its length.
data = (list(self._compress_iterable(row, selector))
for row in data)
self.formatter.emit_list(columns_to_include,
data,
self.app.stdout,
parsed_args,
)
data = (
list(self._compress_iterable(row, selector)) for row in data
)

self.formatter.emit_list(
columns_to_include, data, self.app.stdout, parsed_args,
)

return 0
15 changes: 15 additions & 0 deletions cliff/tests/test_lister.py
Expand Up @@ -106,6 +106,21 @@ def test_sort_by_column_cliff_side_procedure(self):
data = list(args[1])
self.assertEqual([['a', 'A'], ['c', 'A'], ['b', 'B']], data)

def test_sort_by_column_reverse_order(self):
test_lister = ExerciseLister(mock.Mock(), [])
parsed_args = mock.Mock()
parsed_args.columns = ('Col1', 'Col2')
parsed_args.formatter = 'test'
parsed_args.sort_columns = ['Col2', 'Col1']
parsed_args.sort_direction = 'desc'

test_lister.run(parsed_args)

f = test_lister._formatter_plugins['test']
args = f.args[0]
data = list(args[1])
self.assertEqual([['b', 'B'], ['c', 'A'], ['a', 'A']], data)

def test_sort_by_column_data_already_sorted(self):
test_lister = ExerciseListerCustomSort(mock.Mock(), [])
parsed_args = mock.Mock()
Expand Down
21 changes: 21 additions & 0 deletions releasenotes/notes/add-Lister-sort-direction-5f34dba3c9743572.yaml
@@ -0,0 +1,21 @@
---
features:
- |
The ``cliff.lister.Lister`` base class now implements ``--sort-ascending``
and ``--sort-descending`` options, which can be used to configure the sort
direction. For example::
$ hello-world list-users --sort-column email --sort-descending
+----------------+-----------------------------+
| Name | Email |
+----------------+-----------------------------+
| Charles Xavier | therealcharliex@example.com |
| Jim Hendrix | jim@example.com |
| John Doe | doe.john@example.com |
| Alice Baker | abaker@example.com |
+----------------+-----------------------------+
upgrade:
- |
``cliff.lister.Lister`` implementations that override the
``need_sort_by_cliff`` property should now consider the
``--sort-ascending`` and ``--sort-descending`` options.

0 comments on commit 7798cb2

Please sign in to comment.