Merge "Add sort support to image list"
This commit is contained in:
commit
6525c065a4
@ -141,6 +141,7 @@ List available images
|
|||||||
[--public | --private | --shared]
|
[--public | --private | --shared]
|
||||||
[--property <key=value>]
|
[--property <key=value>]
|
||||||
[--long]
|
[--long]
|
||||||
|
[--sort <key>[:<direction>]]
|
||||||
|
|
||||||
.. option:: --public
|
.. option:: --public
|
||||||
|
|
||||||
@ -164,6 +165,11 @@ List available images
|
|||||||
|
|
||||||
List additional fields in output
|
List additional fields in output
|
||||||
|
|
||||||
|
.. option:: --sort <key>[:<direction>]
|
||||||
|
|
||||||
|
Sort output by selected keys and directions(asc or desc) (default: asc),
|
||||||
|
multiple keys and directions can be specified separated by comma
|
||||||
|
|
||||||
image save
|
image save
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
@ -122,6 +122,17 @@ def format_list(data):
|
|||||||
return ', '.join(sorted(data))
|
return ', '.join(sorted(data))
|
||||||
|
|
||||||
|
|
||||||
|
def get_field(item, field):
|
||||||
|
try:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
return item[field]
|
||||||
|
else:
|
||||||
|
return getattr(item, field)
|
||||||
|
except Exception:
|
||||||
|
msg = "Resource doesn't have field %s" % field
|
||||||
|
raise exceptions.CommandError(msg)
|
||||||
|
|
||||||
|
|
||||||
def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
|
def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
|
||||||
"""Return a tuple containing the item properties.
|
"""Return a tuple containing the item properties.
|
||||||
|
|
||||||
@ -170,6 +181,35 @@ def get_dict_properties(item, fields, mixed_case_fields=[], formatters={}):
|
|||||||
return tuple(row)
|
return tuple(row)
|
||||||
|
|
||||||
|
|
||||||
|
def sort_items(items, sort_str):
|
||||||
|
"""Sort items based on sort keys and sort directions given by sort_str.
|
||||||
|
|
||||||
|
:param items: a list or generator object of items
|
||||||
|
:param sort_str: a string defining the sort rules, the format is
|
||||||
|
'<key1>:[direction1],<key2>:[direction2]...', direction can be 'asc'
|
||||||
|
for ascending or 'desc' for descending, if direction is not given,
|
||||||
|
it's ascending by default
|
||||||
|
:return: sorted items
|
||||||
|
"""
|
||||||
|
if not sort_str:
|
||||||
|
return items
|
||||||
|
# items may be a generator object, transform it to a list
|
||||||
|
items = list(items)
|
||||||
|
sort_keys = sort_str.strip().split(',')
|
||||||
|
for sort_key in reversed(sort_keys):
|
||||||
|
reverse = False
|
||||||
|
if ':' in sort_key:
|
||||||
|
sort_key, direction = sort_key.split(':', 1)
|
||||||
|
if direction not in ['asc', 'desc']:
|
||||||
|
msg = "Specify sort direction by asc or desc"
|
||||||
|
raise exceptions.CommandError(msg)
|
||||||
|
if direction == 'desc':
|
||||||
|
reverse = True
|
||||||
|
items.sort(key=lambda item: get_field(item, sort_key),
|
||||||
|
reverse=reverse)
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
def string_to_bool(arg):
|
def string_to_bool(arg):
|
||||||
return arg.strip().lower() in ('t', 'true', 'yes', '1')
|
return arg.strip().lower() in ('t', 'true', 'yes', '1')
|
||||||
|
|
||||||
|
@ -355,6 +355,13 @@ class ListImage(lister.Lister):
|
|||||||
metavar="<size>",
|
metavar="<size>",
|
||||||
help=argparse.SUPPRESS,
|
help=argparse.SUPPRESS,
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--sort',
|
||||||
|
metavar="<key>[:<direction>]",
|
||||||
|
help="Sort output by selected keys and directions(asc or desc) "
|
||||||
|
"(default: asc), multiple keys and directions can be "
|
||||||
|
"specified separated by comma",
|
||||||
|
)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def take_action(self, parsed_args):
|
def take_action(self, parsed_args):
|
||||||
@ -409,6 +416,9 @@ class ListImage(lister.Lister):
|
|||||||
value=value,
|
value=value,
|
||||||
property_field='properties',
|
property_field='properties',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data = utils.sort_items(data, parsed_args.sort)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
column_headers,
|
column_headers,
|
||||||
(utils.get_dict_properties(
|
(utils.get_dict_properties(
|
||||||
|
@ -105,6 +105,13 @@ class ListImage(lister.Lister):
|
|||||||
metavar="<size>",
|
metavar="<size>",
|
||||||
help=argparse.SUPPRESS,
|
help=argparse.SUPPRESS,
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--sort',
|
||||||
|
metavar="<key>[:<direction>]",
|
||||||
|
help="Sort output by selected keys and directions(asc or desc) "
|
||||||
|
"(default: asc), multiple keys and directions can be "
|
||||||
|
"specified separated by comma",
|
||||||
|
)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def take_action(self, parsed_args):
|
def take_action(self, parsed_args):
|
||||||
@ -160,6 +167,9 @@ class ListImage(lister.Lister):
|
|||||||
value=value,
|
value=value,
|
||||||
property_field='properties',
|
property_field='properties',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data = utils.sort_items(data, parsed_args.sort)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
column_headers,
|
column_headers,
|
||||||
(utils.get_dict_properties(
|
(utils.get_dict_properties(
|
||||||
|
@ -58,6 +58,68 @@ class TestUtils(test_utils.TestCase):
|
|||||||
utils.get_password,
|
utils.get_password,
|
||||||
mock_stdin)
|
mock_stdin)
|
||||||
|
|
||||||
|
def get_test_items(self):
|
||||||
|
item1 = {'a': 1, 'b': 2}
|
||||||
|
item2 = {'a': 1, 'b': 3}
|
||||||
|
item3 = {'a': 2, 'b': 2}
|
||||||
|
item4 = {'a': 2, 'b': 1}
|
||||||
|
return [item1, item2, item3, item4]
|
||||||
|
|
||||||
|
def test_sort_items_with_one_key(self):
|
||||||
|
items = self.get_test_items()
|
||||||
|
sort_str = 'b'
|
||||||
|
expect_items = [items[3], items[0], items[2], items[1]]
|
||||||
|
self.assertEqual(expect_items, utils.sort_items(items, sort_str))
|
||||||
|
|
||||||
|
def test_sort_items_with_multiple_keys(self):
|
||||||
|
items = self.get_test_items()
|
||||||
|
sort_str = 'a,b'
|
||||||
|
expect_items = [items[0], items[1], items[3], items[2]]
|
||||||
|
self.assertEqual(expect_items, utils.sort_items(items, sort_str))
|
||||||
|
|
||||||
|
def test_sort_items_all_with_direction(self):
|
||||||
|
items = self.get_test_items()
|
||||||
|
sort_str = 'a:desc,b:desc'
|
||||||
|
expect_items = [items[2], items[3], items[1], items[0]]
|
||||||
|
self.assertEqual(expect_items, utils.sort_items(items, sort_str))
|
||||||
|
|
||||||
|
def test_sort_items_some_with_direction(self):
|
||||||
|
items = self.get_test_items()
|
||||||
|
sort_str = 'a,b:desc'
|
||||||
|
expect_items = [items[1], items[0], items[2], items[3]]
|
||||||
|
self.assertEqual(expect_items, utils.sort_items(items, sort_str))
|
||||||
|
|
||||||
|
def test_sort_items_with_object(self):
|
||||||
|
item1 = mock.Mock(a=1, b=2)
|
||||||
|
item2 = mock.Mock(a=1, b=3)
|
||||||
|
item3 = mock.Mock(a=2, b=2)
|
||||||
|
item4 = mock.Mock(a=2, b=1)
|
||||||
|
items = [item1, item2, item3, item4]
|
||||||
|
sort_str = 'b,a'
|
||||||
|
expect_items = [item4, item1, item3, item2]
|
||||||
|
self.assertEqual(expect_items, utils.sort_items(items, sort_str))
|
||||||
|
|
||||||
|
def test_sort_items_with_empty_key(self):
|
||||||
|
items = self.get_test_items()
|
||||||
|
sort_srt = ''
|
||||||
|
self.assertEqual(items, utils.sort_items(items, sort_srt))
|
||||||
|
sort_srt = None
|
||||||
|
self.assertEqual(items, utils.sort_items(items, sort_srt))
|
||||||
|
|
||||||
|
def test_sort_items_with_invalid_key(self):
|
||||||
|
items = self.get_test_items()
|
||||||
|
sort_str = 'c'
|
||||||
|
self.assertRaises(exceptions.CommandError,
|
||||||
|
utils.sort_items,
|
||||||
|
items, sort_str)
|
||||||
|
|
||||||
|
def test_sort_items_with_invalid_direction(self):
|
||||||
|
items = self.get_test_items()
|
||||||
|
sort_str = 'a:bad_dir'
|
||||||
|
self.assertRaises(exceptions.CommandError,
|
||||||
|
utils.sort_items,
|
||||||
|
items, sort_str)
|
||||||
|
|
||||||
|
|
||||||
class NoUniqueMatch(Exception):
|
class NoUniqueMatch(Exception):
|
||||||
pass
|
pass
|
||||||
|
@ -470,6 +470,35 @@ class TestImageList(TestImage):
|
|||||||
), )
|
), )
|
||||||
self.assertEqual(datalist, tuple(data))
|
self.assertEqual(datalist, tuple(data))
|
||||||
|
|
||||||
|
@mock.patch('openstackclient.common.utils.sort_items')
|
||||||
|
def test_image_list_sort_option(self, si_mock):
|
||||||
|
si_mock.return_value = [
|
||||||
|
copy.deepcopy(image_fakes.IMAGE)
|
||||||
|
]
|
||||||
|
|
||||||
|
arglist = ['--sort', 'name:asc']
|
||||||
|
verifylist = [('sort', 'name:asc')]
|
||||||
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||||
|
|
||||||
|
# DisplayCommandBase.take_action() returns two tuples
|
||||||
|
columns, data = self.cmd.take_action(parsed_args)
|
||||||
|
self.api_mock.image_list.assert_called_with(
|
||||||
|
detailed=False
|
||||||
|
)
|
||||||
|
si_mock.assert_called_with(
|
||||||
|
[image_fakes.IMAGE],
|
||||||
|
'name:asc'
|
||||||
|
)
|
||||||
|
|
||||||
|
collist = ('ID', 'Name')
|
||||||
|
|
||||||
|
self.assertEqual(collist, columns)
|
||||||
|
datalist = ((
|
||||||
|
image_fakes.image_id,
|
||||||
|
image_fakes.image_name
|
||||||
|
), )
|
||||||
|
self.assertEqual(datalist, tuple(data))
|
||||||
|
|
||||||
|
|
||||||
class TestImageSet(TestImage):
|
class TestImageSet(TestImage):
|
||||||
|
|
||||||
|
@ -255,3 +255,30 @@ class TestImageList(TestImage):
|
|||||||
image_fakes.image_name,
|
image_fakes.image_name,
|
||||||
), )
|
), )
|
||||||
self.assertEqual(datalist, tuple(data))
|
self.assertEqual(datalist, tuple(data))
|
||||||
|
|
||||||
|
@mock.patch('openstackclient.common.utils.sort_items')
|
||||||
|
def test_image_list_sort_option(self, si_mock):
|
||||||
|
si_mock.return_value = [
|
||||||
|
copy.deepcopy(image_fakes.IMAGE)
|
||||||
|
]
|
||||||
|
|
||||||
|
arglist = ['--sort', 'name:asc']
|
||||||
|
verifylist = [('sort', 'name:asc')]
|
||||||
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||||
|
|
||||||
|
# DisplayCommandBase.take_action() returns two tuples
|
||||||
|
columns, data = self.cmd.take_action(parsed_args)
|
||||||
|
self.api_mock.image_list.assert_called_with()
|
||||||
|
si_mock.assert_called_with(
|
||||||
|
[image_fakes.IMAGE],
|
||||||
|
'name:asc'
|
||||||
|
)
|
||||||
|
|
||||||
|
collist = ('ID', 'Name')
|
||||||
|
|
||||||
|
self.assertEqual(collist, columns)
|
||||||
|
datalist = ((
|
||||||
|
image_fakes.image_id,
|
||||||
|
image_fakes.image_name
|
||||||
|
), )
|
||||||
|
self.assertEqual(datalist, tuple(data))
|
||||||
|
Loading…
Reference in New Issue
Block a user