Skip to content

Commit 6525c06

Browse files
Jenkinsopenstack-gerrit
authored andcommitted
Merge "Add sort support to image list"
2 parents ee98cb9 + 42cff38 commit 6525c06

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)