Add sort support to image list
Add sort support to image list by sorting items in the client side. The parameter syntax follows this spec[1]. [1] https://review.openstack.org/#/c/145544/ Change-Id: I42b487d18f00f937db1938daa46487cea2a896ab Closes-Bug: #1410251
This commit is contained in:
		| @@ -141,6 +141,7 @@ List available images | ||||
|         [--public | --private | --shared] | ||||
|         [--property <key=value>] | ||||
|         [--long] | ||||
|         [--sort <key>[:<direction>]] | ||||
|  | ||||
| .. option:: --public | ||||
|  | ||||
| @@ -164,6 +165,11 @@ List available images | ||||
|  | ||||
|     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 | ||||
| ---------- | ||||
|  | ||||
|   | ||||
| @@ -122,6 +122,17 @@ def format_list(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={}): | ||||
|     """Return a tuple containing the item properties. | ||||
|  | ||||
| @@ -170,6 +181,35 @@ def get_dict_properties(item, fields, mixed_case_fields=[], formatters={}): | ||||
|     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): | ||||
|     return arg.strip().lower() in ('t', 'true', 'yes', '1') | ||||
|  | ||||
|   | ||||
| @@ -355,6 +355,13 @@ class ListImage(lister.Lister): | ||||
|             metavar="<size>", | ||||
|             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 | ||||
|  | ||||
|     def take_action(self, parsed_args): | ||||
| @@ -409,6 +416,9 @@ class ListImage(lister.Lister): | ||||
|                 value=value, | ||||
|                 property_field='properties', | ||||
|             ) | ||||
|  | ||||
|         data = utils.sort_items(data, parsed_args.sort) | ||||
|  | ||||
|         return ( | ||||
|             column_headers, | ||||
|             (utils.get_dict_properties( | ||||
|   | ||||
| @@ -105,6 +105,13 @@ class ListImage(lister.Lister): | ||||
|             metavar="<size>", | ||||
|             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 | ||||
|  | ||||
|     def take_action(self, parsed_args): | ||||
| @@ -160,6 +167,9 @@ class ListImage(lister.Lister): | ||||
|                 value=value, | ||||
|                 property_field='properties', | ||||
|             ) | ||||
|  | ||||
|         data = utils.sort_items(data, parsed_args.sort) | ||||
|  | ||||
|         return ( | ||||
|             column_headers, | ||||
|             (utils.get_dict_properties( | ||||
|   | ||||
| @@ -58,6 +58,68 @@ class TestUtils(test_utils.TestCase): | ||||
|                               utils.get_password, | ||||
|                               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): | ||||
|     pass | ||||
|   | ||||
| @@ -470,6 +470,35 @@ class TestImageList(TestImage): | ||||
|         ), ) | ||||
|         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): | ||||
|  | ||||
|   | ||||
| @@ -255,3 +255,30 @@ class TestImageList(TestImage): | ||||
|             image_fakes.image_name, | ||||
|         ), ) | ||||
|         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)) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 zhiyuan_cai
					zhiyuan_cai