Skip to content

Commit ada06f4

Browse files
author
Tang Chen
committed
Add MultiKeyValueAction to custom parser action
Class MultiKeyValueAction will be used to parse arguments like this: --route destination=xxx,gateway=xxx --route destination=yyy,gateway=yyy The result is a list like this: [{destination:xxx, gateway:xxx}, {destination:yyy, gateway:yyy}] This action also contain validation of the parameters. Change-Id: Ie3aa8635c6a13fc2e429fe6922acd681dc7244cf
1 parent 2819450 commit ada06f4

2 files changed

Lines changed: 210 additions & 0 deletions

File tree

openstackclient/common/parseractions.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import argparse
1919

20+
from openstackclient.i18n import _
21+
2022

2123
class KeyValueAction(argparse.Action):
2224
"""A custom action to parse arguments as key=value pairs
@@ -36,6 +38,85 @@ def __call__(self, parser, namespace, values, option_string=None):
3638
getattr(namespace, self.dest, {}).pop(values, None)
3739

3840

41+
class MultiKeyValueAction(argparse.Action):
42+
"""A custom action to parse arguments as key1=value1,key2=value2 pairs
43+
44+
Ensure that ``dest`` is a list. The list will finally contain multiple
45+
dicts, with key=value pairs in them.
46+
47+
NOTE: The arguments string should be a comma separated key-value pairs.
48+
And comma(',') and equal('=') may not be used in the key or value.
49+
"""
50+
51+
def __init__(self, option_strings, dest, nargs=None,
52+
required_keys=None, optional_keys=None, **kwargs):
53+
"""Initialize the action object, and parse customized options
54+
55+
Required keys and optional keys can be specified when initializing
56+
the action to enable the key validation. If none of them specified,
57+
the key validation will be skipped.
58+
59+
:param required_keys: a list of required keys
60+
:param optional_keys: a list of optional keys
61+
"""
62+
if nargs:
63+
raise ValueError("Parameter 'nargs' is not allowed, but got %s"
64+
% nargs)
65+
66+
super(MultiKeyValueAction, self).__init__(option_strings,
67+
dest, **kwargs)
68+
69+
# required_keys: A list of keys that is required. None by default.
70+
if required_keys and not isinstance(required_keys, list):
71+
raise TypeError("'required_keys' must be a list")
72+
self.required_keys = set(required_keys or [])
73+
74+
# optional_keys: A list of keys that is optional. None by default.
75+
if optional_keys and not isinstance(optional_keys, list):
76+
raise TypeError("'optional_keys' must be a list")
77+
self.optional_keys = set(optional_keys or [])
78+
79+
def __call__(self, parser, namespace, values, metavar=None):
80+
# Make sure we have an empty list rather than None
81+
if getattr(namespace, self.dest, None) is None:
82+
setattr(namespace, self.dest, [])
83+
84+
params = {}
85+
for kv in values.split(','):
86+
# Add value if an assignment else raise ArgumentTypeError
87+
if '=' in kv:
88+
params.update([kv.split('=', 1)])
89+
else:
90+
msg = ("Expected key=value pairs separated by comma, "
91+
"but got: %s" % (str(kv)))
92+
raise argparse.ArgumentTypeError(self, msg)
93+
94+
# Check key validation
95+
valid_keys = self.required_keys | self.optional_keys
96+
if valid_keys:
97+
invalid_keys = [k for k in params if k not in valid_keys]
98+
if invalid_keys:
99+
msg = _("Invalid keys %(invalid_keys)s specified.\n"
100+
"Valid keys are: %(valid_keys)s.")
101+
raise argparse.ArgumentTypeError(
102+
msg % {'invalid_keys': ', '.join(invalid_keys),
103+
'valid_keys': ', '.join(valid_keys)}
104+
)
105+
106+
if self.required_keys:
107+
missing_keys = [k for k in self.required_keys if k not in params]
108+
if missing_keys:
109+
msg = _("Missing required keys %(missing_keys)s.\n"
110+
"Required keys are: %(required_keys)s.")
111+
raise argparse.ArgumentTypeError(
112+
msg % {'missing_keys': ', '.join(missing_keys),
113+
'required_keys': ', '.join(self.required_keys)}
114+
)
115+
116+
# Update the dest dict
117+
getattr(namespace, self.dest, []).append(params)
118+
119+
39120
class RangeAction(argparse.Action):
40121
"""A custom action to parse a single value or a range of values
41122

openstackclient/tests/common/test_parseractions.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,135 @@ def test_error_values(self):
6161
self.assertDictEqual(expect, actual)
6262

6363

64+
class TestMultiKeyValueAction(utils.TestCase):
65+
66+
def setUp(self):
67+
super(TestMultiKeyValueAction, self).setUp()
68+
69+
self.parser = argparse.ArgumentParser()
70+
71+
# Set up our typical usage
72+
self.parser.add_argument(
73+
'--test',
74+
metavar='req1=xxx,req2=yyy',
75+
action=parseractions.MultiKeyValueAction,
76+
dest='test',
77+
default=None,
78+
required_keys=['req1', 'req2'],
79+
optional_keys=['opt1', 'opt2'],
80+
help='Test'
81+
)
82+
83+
def test_good_values(self):
84+
results = self.parser.parse_args([
85+
'--test', 'req1=aaa,req2=bbb',
86+
'--test', 'req1=,req2=',
87+
])
88+
89+
actual = getattr(results, 'test', [])
90+
expect = [
91+
{'req1': 'aaa', 'req2': 'bbb'},
92+
{'req1': '', 'req2': ''},
93+
]
94+
# Need to sort the lists before comparing them
95+
key = lambda x: x['req1']
96+
expect.sort(key=key)
97+
actual.sort(key=key)
98+
self.assertListEqual(expect, actual)
99+
100+
def test_empty_required_optional(self):
101+
self.parser.add_argument(
102+
'--test-empty',
103+
metavar='req1=xxx,req2=yyy',
104+
action=parseractions.MultiKeyValueAction,
105+
dest='test_empty',
106+
default=None,
107+
required_keys=[],
108+
optional_keys=[],
109+
help='Test'
110+
)
111+
112+
results = self.parser.parse_args([
113+
'--test-empty', 'req1=aaa,req2=bbb',
114+
'--test-empty', 'req1=,req2=',
115+
])
116+
117+
actual = getattr(results, 'test_empty', [])
118+
expect = [
119+
{'req1': 'aaa', 'req2': 'bbb'},
120+
{'req1': '', 'req2': ''},
121+
]
122+
# Need to sort the lists before comparing them
123+
key = lambda x: x['req1']
124+
expect.sort(key=key)
125+
actual.sort(key=key)
126+
self.assertListEqual(expect, actual)
127+
128+
def test_error_values_with_comma(self):
129+
self.assertRaises(
130+
argparse.ArgumentTypeError,
131+
self.parser.parse_args,
132+
[
133+
'--test', 'mmm,nnn=zzz',
134+
]
135+
)
136+
137+
def test_error_values_without_comma(self):
138+
self.assertRaises(
139+
argparse.ArgumentTypeError,
140+
self.parser.parse_args,
141+
[
142+
'--test', 'mmmnnn',
143+
]
144+
)
145+
146+
def test_missing_key(self):
147+
self.assertRaises(
148+
argparse.ArgumentTypeError,
149+
self.parser.parse_args,
150+
[
151+
'--test', 'req2=ddd',
152+
]
153+
)
154+
155+
def test_invalid_key(self):
156+
self.assertRaises(
157+
argparse.ArgumentTypeError,
158+
self.parser.parse_args,
159+
[
160+
'--test', 'req1=aaa,req2=bbb,aaa=req1',
161+
]
162+
)
163+
164+
def test_required_keys_not_list(self):
165+
self.assertRaises(
166+
TypeError,
167+
self.parser.add_argument,
168+
'--test-required-dict',
169+
metavar='req1=xxx,req2=yyy',
170+
action=parseractions.MultiKeyValueAction,
171+
dest='test_required_dict',
172+
default=None,
173+
required_keys={'aaa': 'bbb'},
174+
optional_keys=['opt1', 'opt2'],
175+
help='Test'
176+
)
177+
178+
def test_optional_keys_not_list(self):
179+
self.assertRaises(
180+
TypeError,
181+
self.parser.add_argument,
182+
'--test-optional-dict',
183+
metavar='req1=xxx,req2=yyy',
184+
action=parseractions.MultiKeyValueAction,
185+
dest='test_optional_dict',
186+
default=None,
187+
required_keys=['req1', 'req2'],
188+
optional_keys={'aaa': 'bbb'},
189+
help='Test'
190+
)
191+
192+
64193
class TestNonNegativeAction(utils.TestCase):
65194

66195
def setUp(self):

0 commit comments

Comments
 (0)