Skip to content

Commit 42cff38

Browse files
committed
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
1 parent 0cc3955 commit 42cff38

7 files changed

Lines changed: 184 additions & 0 deletions

File tree

‎doc/source/command-objects/image.rst‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ List available images
141141
[--public | --private | --shared]
142142
[--property <key=value>]
143143
[--long]
144+
[--sort <key>[:<direction>]]
144145
145146
.. option:: --public
146147
@@ -164,6 +165,11 @@ List available images
164165
165166
List additional fields in output
166167
168+
.. option:: --sort <key>[:<direction>]
169+
170+
Sort output by selected keys and directions(asc or desc) (default: asc),
171+
multiple keys and directions can be specified separated by comma
172+
167173
image save
168174
----------
169175

‎openstackclient/common/utils.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,17 @@ def format_list(data):
122122
return ', '.join(sorted(data))
123123

124124

125+
def get_field(item, field):
126+
try:
127+
if isinstance(item, dict):
128+
return item[field]
129+
else:
130+
return getattr(item, field)
131+
except Exception:
132+
msg = "Resource doesn't have field %s" % field
133+
raise exceptions.CommandError(msg)
134+
135+
125136
def get_item_properties(item, fields, mixed_case_fields=[], formatters={}):
126137
"""Return a tuple containing the item properties.
127138
@@ -170,6 +181,35 @@ def get_dict_properties(item, fields, mixed_case_fields=[], formatters={}):
170181
return tuple(row)
171182

172183

184+
def sort_items(items, sort_str):
185+
"""Sort items based on sort keys and sort directions given by sort_str.
186+
187+
:param items: a list or generator object of items
188+
:param sort_str: a string defining the sort rules, the format is
189+
'<key1>:[direction1],<key2>:[direction2]...', direction can be 'asc'
190+
for ascending or 'desc' for descending, if direction is not given,
191+
it's ascending by default
192+
:return: sorted items
193+
"""
194+
if not sort_str:
195+
return items
196+
# items may be a generator object, transform it to a list
197+
items = list(items)
198+
sort_keys = sort_str.strip().split(',')
199+
for sort_key in reversed(sort_keys):
200+
reverse = False
201+
if ':' in sort_key:
202+
sort_key, direction = sort_key.split(':', 1)
203+
if direction not in ['asc', 'desc']:
204+
msg = "Specify sort direction by asc or desc"
205+
raise exceptions.CommandError(msg)
206+
if direction == 'desc':
207+
reverse = True
208+
items.sort(key=lambda item: get_field(item, sort_key),
209+
reverse=reverse)
210+
return items
211+
212+
173213
def string_to_bool(arg):
174214
return arg.strip().lower() in ('t', 'true', 'yes', '1')
175215

‎openstackclient/image/v1/image.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,13 @@ def get_parser(self, prog_name):
355355
metavar="<size>",
356356
help=argparse.SUPPRESS,
357357
)
358+
parser.add_argument(
359+
'--sort',
360+
metavar="<key>[:<direction>]",
361+
help="Sort output by selected keys and directions(asc or desc) "
362+
"(default: asc), multiple keys and directions can be "
363+
"specified separated by comma",
364+
)
358365
return parser
359366

360367
def take_action(self, parsed_args):
@@ -409,6 +416,9 @@ def take_action(self, parsed_args):
409416
value=value,
410417
property_field='properties',
411418
)
419+
420+
data = utils.sort_items(data, parsed_args.sort)
421+
412422
return (
413423
column_headers,
414424
(utils.get_dict_properties(

‎openstackclient/image/v2/image.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ def get_parser(self, prog_name):
105105
metavar="<size>",
106106
help=argparse.SUPPRESS,
107107
)
108+
parser.add_argument(
109+
'--sort',
110+
metavar="<key>[:<direction>]",
111+
help="Sort output by selected keys and directions(asc or desc) "
112+
"(default: asc), multiple keys and directions can be "
113+
"specified separated by comma",
114+
)
108115
return parser
109116

110117
def take_action(self, parsed_args):
@@ -160,6 +167,9 @@ def take_action(self, parsed_args):
160167
value=value,
161168
property_field='properties',
162169
)
170+
171+
data = utils.sort_items(data, parsed_args.sort)
172+
163173
return (
164174
column_headers,
165175
(utils.get_dict_properties(

‎openstackclient/tests/common/test_utils.py‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,68 @@ def test_get_password_cntrl_d(self):
5858
utils.get_password,
5959
mock_stdin)
6060

61+
def get_test_items(self):
62+
item1 = {'a': 1, 'b': 2}
63+
item2 = {'a': 1, 'b': 3}
64+
item3 = {'a': 2, 'b': 2}
65+
item4 = {'a': 2, 'b': 1}
66+
return [item1, item2, item3, item4]
67+
68+
def test_sort_items_with_one_key(self):
69+
items = self.get_test_items()
70+
sort_str = 'b'
71+
expect_items = [items[3], items[0], items[2], items[1]]
72+
self.assertEqual(expect_items, utils.sort_items(items, sort_str))
73+
74+
def test_sort_items_with_multiple_keys(self):
75+
items = self.get_test_items()
76+
sort_str = 'a,b'
77+
expect_items = [items[0], items[1], items[3], items[2]]
78+
self.assertEqual(expect_items, utils.sort_items(items, sort_str))
79+
80+
def test_sort_items_all_with_direction(self):
81+
items = self.get_test_items()
82+
sort_str = 'a:desc,b:desc'
83+
expect_items = [items[2], items[3], items[1], items[0]]
84+
self.assertEqual(expect_items, utils.sort_items(items, sort_str))
85+
86+
def test_sort_items_some_with_direction(self):
87+
items = self.get_test_items()
88+
sort_str = 'a,b:desc'
89+
expect_items = [items[1], items[0], items[2], items[3]]
90+
self.assertEqual(expect_items, utils.sort_items(items, sort_str))
91+
92+
def test_sort_items_with_object(self):
93+
item1 = mock.Mock(a=1, b=2)
94+
item2 = mock.Mock(a=1, b=3)
95+
item3 = mock.Mock(a=2, b=2)
96+
item4 = mock.Mock(a=2, b=1)
97+
items = [item1, item2, item3, item4]
98+
sort_str = 'b,a'
99+
expect_items = [item4, item1, item3, item2]
100+
self.assertEqual(expect_items, utils.sort_items(items, sort_str))
101+
102+
def test_sort_items_with_empty_key(self):
103+
items = self.get_test_items()
104+
sort_srt = ''
105+
self.assertEqual(items, utils.sort_items(items, sort_srt))
106+
sort_srt = None
107+
self.assertEqual(items, utils.sort_items(items, sort_srt))
108+
109+
def test_sort_items_with_invalid_key(self):
110+
items = self.get_test_items()
111+
sort_str = 'c'
112+
self.assertRaises(exceptions.CommandError,
113+
utils.sort_items,
114+
items, sort_str)
115+
116+
def test_sort_items_with_invalid_direction(self):
117+
items = self.get_test_items()
118+
sort_str = 'a:bad_dir'
119+
self.assertRaises(exceptions.CommandError,
120+
utils.sort_items,
121+
items, sort_str)
122+
61123

62124
class NoUniqueMatch(Exception):
63125
pass

‎openstackclient/tests/image/v1/test_image.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,35 @@ def test_image_list_property_option(self, sf_mock):
470470
), )
471471
self.assertEqual(datalist, tuple(data))
472472

473+
@mock.patch('openstackclient.common.utils.sort_items')
474+
def test_image_list_sort_option(self, si_mock):
475+
si_mock.return_value = [
476+
copy.deepcopy(image_fakes.IMAGE)
477+
]
478+
479+
arglist = ['--sort', 'name:asc']
480+
verifylist = [('sort', 'name:asc')]
481+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
482+
483+
# DisplayCommandBase.take_action() returns two tuples
484+
columns, data = self.cmd.take_action(parsed_args)
485+
self.api_mock.image_list.assert_called_with(
486+
detailed=False
487+
)
488+
si_mock.assert_called_with(
489+
[image_fakes.IMAGE],
490+
'name:asc'
491+
)
492+
493+
collist = ('ID', 'Name')
494+
495+
self.assertEqual(collist, columns)
496+
datalist = ((
497+
image_fakes.image_id,
498+
image_fakes.image_name
499+
), )
500+
self.assertEqual(datalist, tuple(data))
501+
473502

474503
class TestImageSet(TestImage):
475504

‎openstackclient/tests/image/v2/test_image.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,30 @@ def test_image_list_property_option(self, sf_mock):
255255
image_fakes.image_name,
256256
), )
257257
self.assertEqual(datalist, tuple(data))
258+
259+
@mock.patch('openstackclient.common.utils.sort_items')
260+
def test_image_list_sort_option(self, si_mock):
261+
si_mock.return_value = [
262+
copy.deepcopy(image_fakes.IMAGE)
263+
]
264+
265+
arglist = ['--sort', 'name:asc']
266+
verifylist = [('sort', 'name:asc')]
267+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
268+
269+
# DisplayCommandBase.take_action() returns two tuples
270+
columns, data = self.cmd.take_action(parsed_args)
271+
self.api_mock.image_list.assert_called_with()
272+
si_mock.assert_called_with(
273+
[image_fakes.IMAGE],
274+
'name:asc'
275+
)
276+
277+
collist = ('ID', 'Name')
278+
279+
self.assertEqual(collist, columns)
280+
datalist = ((
281+
image_fakes.image_id,
282+
image_fakes.image_name
283+
), )
284+
self.assertEqual(datalist, tuple(data))

0 commit comments

Comments
 (0)