diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py index b74cd28d5a2..af0af93cc7a 100644 --- a/lib/ansible/module_utils/basic.py +++ b/lib/ansible/module_utils/basic.py @@ -748,7 +748,7 @@ class AnsibleModule(object): def __init__(self, argument_spec, bypass_checks=False, no_log=False, check_invalid_arguments=None, mutually_exclusive=None, required_together=None, required_one_of=None, add_file_common_args=False, supports_check_mode=False, - required_if=None): + required_if=None, required_by=None): ''' Common code for quickly building an ansible module in Python @@ -777,6 +777,7 @@ class AnsibleModule(object): self.required_together = required_together self.required_one_of = required_one_of self.required_if = required_if + self.required_by = required_by self.cleanup_files = [] self._debug = False self._diff = False @@ -848,6 +849,7 @@ class AnsibleModule(object): self._check_required_together(required_together) self._check_required_one_of(required_one_of) self._check_required_if(required_if) + self._check_required_by(required_by) self._set_defaults(pre=False) @@ -1706,6 +1708,24 @@ class AnsibleModule(object): msg += " found in %s" % " -> ".join(self._options_context) self.fail_json(msg=msg) + def _check_required_by(self, spec, param=None): + if spec is None: + return + if param is None: + param = self.params + for (key, value) in spec.items(): + if key not in param or param[key] is None: + continue + missing = [] + # Support strings (single-item lists) + if isinstance(value, string_types): + value = [value, ] + for required in value: + if required not in param or param[required] is None: + missing.append(required) + if len(missing) > 0: + self.fail_json(msg="missing parameter(s) required by '%s': %s" % (key, ', '.join(missing))) + def _check_required_arguments(self, spec=None, param=None): ''' ensure all required arguments are present ''' missing = [] @@ -2008,6 +2028,7 @@ class AnsibleModule(object): self._check_required_together(v.get('required_together', None), param) self._check_required_one_of(v.get('required_one_of', None), param) self._check_required_if(v.get('required_if', None), param) + self._check_required_by(v.get('required_by', None), param) self._set_defaults(pre=False, spec=spec, param=param) diff --git a/lib/ansible/modules/cloud/amazon/ec2_eip.py b/lib/ansible/modules/cloud/amazon/ec2_eip.py index 22b00645a00..badc5002d63 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_eip.py +++ b/lib/ansible/modules/cloud/amazon/ec2_eip.py @@ -385,7 +385,10 @@ def main(): module = AnsibleModule( argument_spec=argument_spec, - supports_check_mode=True + supports_check_mode=True, + required_together=[ + ['device_id', 'private_ip_address'], + ], ) if not HAS_BOTO: @@ -404,10 +407,6 @@ def main(): release_on_disassociation = module.params.get('release_on_disassociation') allow_reassociation = module.params.get('allow_reassociation') - # Parameter checks - if private_ip_address is not None and device_id is None: - module.fail_json(msg="parameters are required together: ('device_id', 'private_ip_address')") - if instance_id: warnings = ["instance_id is no longer used, please use device_id going forward"] is_instance = True diff --git a/lib/ansible/modules/cloud/amazon/route53.py b/lib/ansible/modules/cloud/amazon/route53.py index 8533df02af6..bd80e1de7f5 100644 --- a/lib/ansible/modules/cloud/amazon/route53.py +++ b/lib/ansible/modules/cloud/amazon/route53.py @@ -1,4 +1,5 @@ #!/usr/bin/python +# -*- coding: utf-8 -*- # Copyright: (c) 2018, Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -459,41 +460,50 @@ def invoke_with_throttling_retries(function_ref, *argv, **kwargs): def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( - state=dict(aliases=['command'], choices=['present', 'absent', 'get', 'create', 'delete'], required=True), - zone=dict(required=True), - hosted_zone_id=dict(required=False, default=None), - record=dict(required=True), - ttl=dict(required=False, type='int', default=3600), - type=dict(choices=['A', 'CNAME', 'MX', 'AAAA', 'TXT', 'PTR', 'SRV', 'SPF', 'CAA', 'NS', 'SOA'], required=True), - alias=dict(required=False, type='bool'), - alias_hosted_zone_id=dict(required=False), - alias_evaluate_target_health=dict(required=False, type='bool', default=False), - value=dict(required=False, type='list'), - overwrite=dict(required=False, type='bool'), - retry_interval=dict(required=False, default=500), - private_zone=dict(required=False, type='bool', default=False), - identifier=dict(required=False, default=None), - weight=dict(required=False, type='int'), - region=dict(required=False), - health_check=dict(required=False), - failover=dict(required=False, choices=['PRIMARY', 'SECONDARY']), - vpc_id=dict(required=False), - wait=dict(required=False, type='bool', default=False), - wait_timeout=dict(required=False, type='int', default=300), + state=dict(type='str', required=True, choices=['absent', 'create', 'delete', 'get', 'present'], aliases=['command']), + zone=dict(type='str', required=True), + hosted_zone_id=dict(type='str', ), + record=dict(type='str', required=True), + ttl=dict(type='int', default=3600), + type=dict(type='str', required=True, choices=['A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'PTR', 'SOA', 'SPF', 'SRV', 'TXT']), + alias=dict(type='bool'), + alias_hosted_zone_id=dict(type='str'), + alias_evaluate_target_health=dict(type='bool', default=False), + value=dict(type='list'), + overwrite=dict(type='bool'), + retry_interval=dict(type='int', default=500), + private_zone=dict(type='bool', default=False), + identifier=dict(type='str'), + weight=dict(type='int'), + region=dict(type='str'), + health_check=dict(type='str'), + failover=dict(type='str', choices=['PRIMARY', 'SECONDARY']), + vpc_id=dict(type='str'), + wait=dict(type='bool', default=False), + wait_timeout=dict(type='int', default=300), )) - # state=present, absent, create, delete THEN value is required - required_if = [('state', 'present', ['value']), ('state', 'create', ['value'])] - required_if.extend([('state', 'absent', ['value']), ('state', 'delete', ['value'])]) - - # If alias is True then you must specify alias_hosted_zone as well - required_together = [['alias', 'alias_hosted_zone_id']] - - # failover, region, and weight are mutually exclusive - mutually_exclusive = [('failover', 'region', 'weight')] - - module = AnsibleModule(argument_spec=argument_spec, required_together=required_together, required_if=required_if, - mutually_exclusive=mutually_exclusive, supports_check_mode=True) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + # If alias is True then you must specify alias_hosted_zone as well + required_together=[['alias', 'alias_hosted_zone_id']], + # state=present, absent, create, delete THEN value is required + required_if=( + ('state', 'present', ['value']), + ('state', 'create', ['value']), + ('state', 'absent', ['value']), + ('state', 'delete', ['value']), + ), + # failover, region and weight are mutually exclusive + mutually_exclusive=[('failover', 'region', 'weight')], + # failover, region and weight require identifier + required_by=dict( + failover=('identifier'), + region=('identifier'), + weight=('identifier'), + ), + ) if not HAS_BOTO: module.fail_json(msg='boto required for this module') @@ -544,8 +554,6 @@ def main(): if command_in == 'create' or command_in == 'delete': if alias_in and len(value_in) != 1: module.fail_json(msg="parameter 'value' must contain a single dns name for alias records") - if (weight_in is not None or region_in is not None or failover_in is not None) and identifier_in is None: - module.fail_json(msg="If you specify failover, region or weight you must also specify identifier") if (weight_in is None and region_in is None and failover_in is None) and identifier_in is not None: module.fail_json(msg="You have specified identifier which makes sense only if you specify one of: weight, region or failover.") diff --git a/lib/ansible/modules/cloud/openstack/os_subnet.py b/lib/ansible/modules/cloud/openstack/os_subnet.py index be0d05a1255..9980d5dd4c6 100644 --- a/lib/ansible/modules/cloud/openstack/os_subnet.py +++ b/lib/ansible/modules/cloud/openstack/os_subnet.py @@ -223,28 +223,31 @@ def _system_state_change(module, subnet, cloud, filters=None): def main(): ipv6_mode_choices = ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'] argument_spec = openstack_full_argument_spec( - name=dict(required=True), - network_name=dict(default=None), - cidr=dict(default=None), - ip_version=dict(default='4', choices=['4', '6']), - enable_dhcp=dict(default='true', type='bool'), - gateway_ip=dict(default=None), - no_gateway_ip=dict(default=False, type='bool'), - dns_nameservers=dict(default=None, type='list'), - allocation_pool_start=dict(default=None), - allocation_pool_end=dict(default=None), - host_routes=dict(default=None, type='list'), - ipv6_ra_mode=dict(default=None, choice=ipv6_mode_choices), - ipv6_address_mode=dict(default=None, choice=ipv6_mode_choices), - use_default_subnetpool=dict(default=False, type='bool'), - extra_specs=dict(required=False, default=dict(), type='dict'), - state=dict(default='present', choices=['absent', 'present']), - project=dict(default=None) + name=dict(type='str', required=True), + network_name=dict(type='str'), + cidr=dict(type='str'), + ip_version=dict(type='str', default='4', choices=['4', '6']), + enable_dhcp=dict(type='bool', default=True), + gateway_ip=dict(type='str'), + no_gateway_ip=dict(type='bool', default=False), + dns_nameservers=dict(type='list', default=None), + allocation_pool_start=dict(type='str'), + allocation_pool_end=dict(type='str'), + host_routes=dict(type='list', default=None), + ipv6_ra_mode=dict(type='str', choice=ipv6_mode_choices), + ipv6_address_mode=dict(type='str', choice=ipv6_mode_choices), + use_default_subnetpool=dict(type='bool', default=False), + extra_specs=dict(type='dict', default=dict()), + state=dict(type='str', default='present', choices=['absent', 'present']), + project=dict(type='str'), ) module_kwargs = openstack_module_kwargs() module = AnsibleModule(argument_spec, supports_check_mode=True, + required_together=[ + ['allocation_pool_end', 'allocation_pool_start'], + ], **module_kwargs) state = module.params['state'] @@ -275,8 +278,6 @@ def main(): if pool_start and pool_end: pool = [dict(start=pool_start, end=pool_end)] - elif pool_start or pool_end: - module.fail_json(msg='allocation pool requires start and end values') else: pool = None diff --git a/lib/ansible/modules/files/xml.py b/lib/ansible/modules/files/xml.py index c547cec1534..e574965cd01 100644 --- a/lib/ansible/modules/files/xml.py +++ b/lib/ansible/modules/files/xml.py @@ -831,16 +831,14 @@ def main(): insertafter=dict(type='bool', default=False), ), supports_check_mode=True, - # TODO: Implement this as soon as #28662 (required_by functionality) is merged - # required_by=dict( - # add_children=['xpath'], - # attribute=['value'], - # set_children=['xpath'], - # value=['xpath'], - # ), + required_by=dict( + add_children=['xpath'], + attribute=['value'], + content=['xpath'], + set_children=['xpath'], + value=['xpath'], + ), required_if=[ - ['content', 'attribute', ['xpath']], - ['content', 'text', ['xpath']], ['count', True, ['xpath']], ['print_match', True, ['xpath']], ['insertbefore', True, ['xpath']], diff --git a/lib/ansible/modules/system/cron.py b/lib/ansible/modules/system/cron.py index 7b31f3bc111..bc71d74eb69 100644 --- a/lib/ansible/modules/system/cron.py +++ b/lib/ansible/modules/system/cron.py @@ -98,7 +98,7 @@ options: special_time: description: - Special time specification nickname. - choices: [ reboot, yearly, annually, monthly, weekly, daily, hourly ] + choices: [ annually, daily, hourly, monthly, reboot, weekly, yearly ] version_added: "1.3" disabled: description: @@ -566,6 +566,12 @@ def main(): ['reboot', 'special_time'], ['insertafter', 'insertbefore'], ], + required_by=dict( + cron_file=('user'), + ), + required_if=( + ('state', 'present', ('job')), + ), ) name = module.params['name'] @@ -624,13 +630,6 @@ def main(): if (special_time or reboot) and get_platform() == 'SunOS': module.fail_json(msg="Solaris does not support special_time=... or @reboot") - if cron_file and do_install: - if not user: - module.fail_json(msg="To use cron_file=... parameter you must specify user=... as well") - - if job is None and do_install: - module.fail_json(msg="You must specify 'job' to install a new cron job or variable") - if (insertafter or insertbefore) and not env and do_install: module.fail_json(msg="Insertafter and insertbefore parameters are valid only with env=yes") diff --git a/lib/ansible/modules/system/firewalld.py b/lib/ansible/modules/system/firewalld.py index 3d49834e724..0e5976e46da 100644 --- a/lib/ansible/modules/system/firewalld.py +++ b/lib/ansible/modules/system/firewalld.py @@ -647,7 +647,11 @@ def main(): masquerade=dict(type='str'), offline=dict(type='bool'), ), - supports_check_mode=True + supports_check_mode=True, + required_by=dict( + interface=('zone'), + source=('permanent'), + ), ) permanent = module.params['permanent'] diff --git a/lib/ansible/modules/system/interfaces_file.py b/lib/ansible/modules/system/interfaces_file.py index ed1ce6d8fb5..562f07e7e36 100644 --- a/lib/ansible/modules/system/interfaces_file.py +++ b/lib/ansible/modules/system/interfaces_file.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # -# (c) 2016, Roman Belyakovsky +# Copyright: (c) 2016, Roman Belyakovsky # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function @@ -350,16 +350,19 @@ def write_changes(module, lines, dest): def main(): module = AnsibleModule( argument_spec=dict( - dest=dict(default='/etc/network/interfaces', required=False, type='path'), - iface=dict(required=False), - address_family=dict(required=False), - option=dict(required=False), - value=dict(required=False), - backup=dict(default='no', type='bool'), - state=dict(default='present', choices=['present', 'absent']), + dest=dict(type='path', default='/etc/network/interfaces'), + iface=dict(type='str'), + address_family=dict(type='str'), + option=dict(type='str'), + value=dict(type='str'), + backup=dict(type='bool', default=False), + state=dict(type='str', default='present', choices=['absent', 'present']), ), add_file_common_args=True, - supports_check_mode=True + supports_check_mode=True, + required_by=dict( + option=('iface'), + ), ) dest = module.params['dest'] @@ -370,9 +373,6 @@ def main(): backup = module.params['backup'] state = module.params['state'] - if option is not None and iface is None: - module.fail_json(msg="Inteface must be set if option is defined") - if option is not None and state == "present" and value is None: module.fail_json(msg="Value must be set if option is defined and state is 'present'") diff --git a/lib/ansible/modules/system/systemd.py b/lib/ansible/modules/system/systemd.py index a9d839b0bd2..f820c462afd 100644 --- a/lib/ansible/modules/system/systemd.py +++ b/lib/ansible/modules/system/systemd.py @@ -327,6 +327,11 @@ def main(): ), supports_check_mode=True, required_one_of=[['state', 'enabled', 'masked', 'daemon_reload']], + required_by=dict( + state=('name', ), + enabled=('name', ), + masked=('name', ), + ), mutually_exclusive=[['scope', 'user']], ) @@ -364,10 +369,6 @@ def main(): status=dict(), ) - for requires in ('state', 'enabled', 'masked'): - if module.params[requires] is not None and unit is None: - module.fail_json(msg="name is also required when specifying %s" % requires) - # Run daemon-reload first, if requested if module.params['daemon_reload'] and not module.check_mode: (rc, out, err) = module.run_command("%s daemon-reload" % (systemctl)) diff --git a/lib/ansible/modules/system/ufw.py b/lib/ansible/modules/system/ufw.py index 5c94e2b4eaa..d46c8385d19 100644 --- a/lib/ansible/modules/system/ufw.py +++ b/lib/ansible/modules/system/ufw.py @@ -290,6 +290,8 @@ def compile_ipv6_regexp(): def main(): + command_keys = ['state', 'default', 'rule', 'logging'] + module = AnsibleModule( argument_spec=dict( state=dict(type='str', choices=['enabled', 'disabled', 'reloaded', 'reset']), @@ -313,8 +315,12 @@ def main(): ), supports_check_mode=True, mutually_exclusive=[ - ['app', 'proto', 'logging'] + ['app', 'proto', 'logging'], ], + required_one_of=([command_keys]), + required_by=dict( + interface=('direction', ), + ), ) cmds = [] @@ -395,16 +401,8 @@ def main(): params = module.params - # Ensure at least one of the command arguments are given - command_keys = ['state', 'default', 'rule', 'logging'] commands = dict((key, params[key]) for key in command_keys if params[key]) - if len(commands) < 1: - module.fail_json(msg="Not any of the command arguments %s given" % commands) - - if (params['interface'] is not None and params['direction'] is None): - module.fail_json(msg="Direction must be specified when creating a rule on an interface") - # Ensure ufw is available ufw_bin = module.get_bin_path('ufw', True) grep_bin = module.get_bin_path('grep', True) diff --git a/test/sanity/validate-modules/schema.py b/test/sanity/validate-modules/schema.py index 7b9da3efc07..2feb41c0180 100644 --- a/test/sanity/validate-modules/schema.py +++ b/test/sanity/validate-modules/schema.py @@ -9,6 +9,7 @@ from voluptuous import ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Length, Invalid, Re from ansible.module_utils.six import string_types from ansible.module_utils.common.collections import is_iterable list_string_types = list(string_types) +tuple_string_types = tuple(string_types) any_string_types = Any(*string_types) # Valid DOCUMENTATION.author lines @@ -67,6 +68,7 @@ ansible_module_kwargs_schema = Schema( 'add_file_common_args': bool, 'supports_check_mode': bool, 'required_if': sequence_of_sequences(min=3), + 'required_by': Schema({str: Any(list_string_types, tuple_string_types, *string_types)}), } ) diff --git a/test/units/module_utils/basic/test_argument_spec.py b/test/units/module_utils/basic/test_argument_spec.py index 14c134d9f11..3ecb05ffb1c 100644 --- a/test/units/module_utils/basic/test_argument_spec.py +++ b/test/units/module_utils/basic/test_argument_spec.py @@ -97,6 +97,7 @@ def options_argspec_list(): bam1=dict(), bam2=dict(default='test'), bam3=dict(type='bool'), + bam4=dict(type='str'), ) arg_spec = dict( @@ -116,7 +117,10 @@ def options_argspec_list(): ], required_together=[ ['bam1', 'baz'] - ] + ], + required_by={ + 'bam4': ('bam1', 'bam3'), + }, ) ) @@ -278,46 +282,54 @@ class TestComplexArgSpecs: class TestComplexOptions: """Test arg spec options""" - # (Paramaters, expected value of module.params['foobar']) + # (Parameters, expected value of module.params['foobar']) OPTIONS_PARAMS_LIST = ( ({'foobar': [{"foo": "hello", "bam": "good"}, {"foo": "test", "bar": "good"}]}, - [{'foo': 'hello', 'bam': 'good', 'bam2': 'test', 'bar': None, 'baz': None, 'bam1': None, 'bam3': None}, - {'foo': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None}, + [{'foo': 'hello', 'bam': 'good', 'bam2': 'test', 'bar': None, 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None}, + {'foo': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None}, ]), # Alias for required param ({'foobar': [{"dup": "test", "bar": "good"}]}, - [{'foo': 'test', 'dup': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None}] + [{'foo': 'test', 'dup': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None}] ), # Required_if utilizing default value of the requirement ({'foobar': [{"foo": "bam2", "bar": "required_one_of"}]}, - [{'bam': None, 'bam1': None, 'bam2': 'test', 'bam3': None, 'bar': 'required_one_of', 'baz': None, 'foo': 'bam2'}] + [{'bam': None, 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': 'required_one_of', 'baz': None, 'foo': 'bam2'}] ), # Check that a bool option is converted ({"foobar": [{"foo": "required", "bam": "good", "bam3": "yes"}]}, - [{'bam': 'good', 'bam1': None, 'bam2': 'test', 'bam3': True, 'bar': None, 'baz': None, 'foo': 'required'}] + [{'bam': 'good', 'bam1': None, 'bam2': 'test', 'bam3': True, 'bam4': None, 'bar': None, 'baz': None, 'foo': 'required'}] + ), + # Check required_by options + ({"foobar": [{"foo": "required", "bar": "good", "baz": "good", "bam4": "required_by", "bam1": "ok", "bam3": "yes"}]}, + [{'bar': 'good', 'baz': 'good', 'bam1': 'ok', 'bam2': 'test', 'bam3': True, 'bam4': 'required_by', 'bam': None, 'foo': 'required'}] ), ) - # (Paramaters, expected value of module.params['foobar']) + # (Parameters, expected value of module.params['foobar']) OPTIONS_PARAMS_DICT = ( ({'foobar': {"foo": "hello", "bam": "good"}}, - {'foo': 'hello', 'bam': 'good', 'bam2': 'test', 'bar': None, 'baz': None, 'bam1': None, 'bam3': None} + {'foo': 'hello', 'bam': 'good', 'bam2': 'test', 'bar': None, 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None} ), # Alias for required param ({'foobar': {"dup": "test", "bar": "good"}}, - {'foo': 'test', 'dup': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None} + {'foo': 'test', 'dup': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None} ), # Required_if utilizing default value of the requirement ({'foobar': {"foo": "bam2", "bar": "required_one_of"}}, - {'bam': None, 'bam1': None, 'bam2': 'test', 'bam3': None, 'bar': 'required_one_of', 'baz': None, 'foo': 'bam2'} + {'bam': None, 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': 'required_one_of', 'baz': None, 'foo': 'bam2'} ), # Check that a bool option is converted ({"foobar": {"foo": "required", "bam": "good", "bam3": "yes"}}, - {'bam': 'good', 'bam1': None, 'bam2': 'test', 'bam3': True, 'bar': None, 'baz': None, 'foo': 'required'} + {'bam': 'good', 'bam1': None, 'bam2': 'test', 'bam3': True, 'bam4': None, 'bar': None, 'baz': None, 'foo': 'required'} + ), + # Check required_by options + ({"foobar": {"foo": "required", "bar": "good", "baz": "good", "bam4": "required_by", "bam1": "ok", "bam3": "yes"}}, + {'bar': 'good', 'baz': 'good', 'bam1': 'ok', 'bam2': 'test', 'bam3': True, 'bam4': 'required_by', 'bam': None, 'foo': 'required'} ), ) - # (Paramaters, failure message) + # (Parameters, failure message) FAILING_PARAMS_LIST = ( # Missing required option ({'foobar': [{}]}, 'missing required arguments: foo found in foobar'), @@ -335,9 +347,12 @@ class TestComplexOptions: # Missing required_together option ({'foobar': [{"foo": "test", "bar": "required_one_of", "bam1": "bad"}]}, 'parameters are required together: bam1, baz found in foobar'), + # Missing required_by options + ({'foobar': [{"foo": "test", "bar": "required_one_of", "bam4": "required_by"}]}, + "missing parameter(s) required by 'bam4': bam1, bam3"), ) - # (Paramaters, failure message) + # (Parameters, failure message) FAILING_PARAMS_DICT = ( # Missing required option ({'foobar': {}}, 'missing required arguments: foo found in foobar'), @@ -356,6 +371,9 @@ class TestComplexOptions: # Missing required_together option ({'foobar': {"foo": "test", "bar": "required_one_of", "bam1": "bad"}}, 'parameters are required together: bam1, baz found in foobar'), + # Missing required_by options + ({'foobar': {"foo": "test", "bar": "required_one_of", "bam4": "required_by"}}, + "missing parameter(s) required by 'bam4': bam1, bam3"), ) @pytest.mark.parametrize('stdin, expected', OPTIONS_PARAMS_DICT, indirect=['stdin'])