Skip to content

Commit

Permalink
basic: allow one or more when param list having choices (#34537)
Browse files Browse the repository at this point in the history
* basic: allow one or more when param list having choices

* add unit tests

* optimize a bit

* re-add get_exception import

* a number of existing modules expect to be able to get it from basic.py
  • Loading branch information
resmo authored and nitzmahone committed Feb 8, 2018
1 parent 82c1456 commit 2f36b9e
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 2 deletions.
13 changes: 11 additions & 2 deletions lib/ansible/module_utils/basic.py
Expand Up @@ -170,7 +170,7 @@
)
from ansible.module_utils.six.moves import map, reduce, shlex_quote
from ansible.module_utils._text import to_native, to_bytes, to_text
from ansible.module_utils.parsing.convert_bool import BOOLEANS, BOOLEANS_FALSE, BOOLEANS_TRUE, boolean

This comment has been minimized.

Copy link
@lazouz

lazouz May 16, 2018

Contributor

I do not clearly understand why you do not want to import BOOLEANS. Could you explain this choice ?

from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE, boolean


PASSWORD_MATCH = re.compile(r'^(?:.+[-_\s])?pass(?:[-_\s]?(?:word|phrase|wrd|wd)?)(?:[-_\s].+)?$', re.I)
Expand Down Expand Up @@ -1778,7 +1778,16 @@ def _check_argument_values(self, spec=None, param=None):
continue
if isinstance(choices, SEQUENCETYPE) and not isinstance(choices, (binary_type, text_type)):
if k in param:
if param[k] not in choices:
# Allow one or more when type='list' param with choices
if isinstance(param[k], list):
diff_list = ", ".join([item for item in param[k] if item not in choices])
if diff_list:
choices_str = ", ".join([to_native(c) for c in choices])
msg = "value of %s must be one or more of: %s. Got no match for: %s" % (k, choices_str, diff_list)
if self._options_context:
msg += " found in %s" % " -> ".join(self._options_context)
self.fail_json(msg=msg)
elif param[k] not in choices:
# PyYaml converts certain strings to bools. If we can unambiguously convert back, do so before checking
# the value. If we can't figure this out, module author is responsible.
lowered_choices = None
Expand Down
20 changes: 20 additions & 0 deletions test/units/module_utils/basic/test_argument_spec.py
Expand Up @@ -71,6 +71,7 @@ def complex_argspec():
baz=dict(fallback=(basic.env_fallback, ['BAZ'])),
bar1=dict(type='bool'),
zardoz=dict(choices=['one', 'two']),
zardoz2=dict(type='list', choices=['one', 'two', 'three']),
)
mut_ex = (('bar', 'bam'),)
req_to = (('bam', 'baz'),)
Expand Down Expand Up @@ -254,6 +255,25 @@ def test_fail_required_together_and_fallback(self, capfd, mocker, stdin, complex
assert results['failed']
assert results['msg'] == "parameters are required together: bam, baz"

@pytest.mark.parametrize('stdin', [{'foo': 'hello', 'zardoz2': ['one', 'four', 'five']}], indirect=['stdin'])
def test_fail_list_with_choices(self, capfd, mocker, stdin, complex_argspec):
"""Fail because one of the items is not in the choice"""
with pytest.raises(SystemExit):
basic.AnsibleModule(**complex_argspec)

out, err = capfd.readouterr()
results = json.loads(out)

assert results['failed']
assert results['msg'] == "value of zardoz2 must be one or more of: one, two, three. Got no match for: four, five"

@pytest.mark.parametrize('stdin', [{'foo': 'hello', 'zardoz2': ['one', 'three']}], indirect=['stdin'])
def test_list_with_choices(self, capfd, mocker, stdin, complex_argspec):
"""Test choices with list"""
am = basic.AnsibleModule(**complex_argspec)
assert isinstance(am.params['zardoz2'], list)
assert am.params['zardoz2'] == ['one', 'three']


class TestComplexOptions:
"""Test arg spec options"""
Expand Down

0 comments on commit 2f36b9e

Please sign in to comment.