Parameter Groups
Parameters can be grouped so that they are mutually exclusive or mutually dependent, or just to organize them in help text. Arbitrary levels of nesting are supported, including mutually dependent groups inside mutually exclusive groups, and vice versa.
Groups are defined by initializing a ParamGroup
as a context manager, and defining the member Parameters
inside the with
block.
Initialization Parameters
- name:
The name of the group to appear in help text. Ignored if
description
is provided.- description:
The description (header) for this group in help text. Defaults to
{name} options
ifname
is specified. If show_group_type is True (the default), and the group is mutually exclusive/dependent, then(mutually {type})
will be appended to this text. If no description or name is provided andshow_group_type
is True, then the entire description will default toMutually {type} options
, otherwise it will beOptional arguments
.- mutually_exclusive:
True
if Parameters in the group are mutually exclusive,False
otherwise. I.e., if one Parameter in the group is provided, then no other Parameter in the group will be allowed. Cannot be combined withmutually_dependent
.- mutually_dependent:
True
if Parameters in the group are mutually dependent,False
otherwise. I.e., if one Parameter in the group is provided, then all other Parameters in the group must also be provided. Cannot be combined withmutually_exclusive
.- required:
Whether at least one Parameter in the group is required or not. If it is required, then an exception will be raised if the user did not provide a value for any Parameters in the group. Defaults to
False
.- hide:
Set this to
True
to hide the group and all of its members so they will not be included in usage / help text.
One example use case for a basic group is to indicate that common arguments are accepted by all subcommands:
class ApiWrapper(Command):
sub_cmd = SubCommand(help='The command to run')
with ParamGroup('Common'):
verbose = Counter('-v', help='Increase logging verbosity (can specify multiple times)')
env = Option('-e', choices=('dev', 'qa', 'uat', 'prod'), default='prod', help='Environment to connect to')
Using that example, we can see the common options group in a subcommand’s help text:
$ rest_api_wrapper.py show -h
usage: rest_api_wrapper.py show {foo,bar,baz} [--verbose [VERBOSE]] [--env {dev,qa,uat,prod}] [--help] [--ids IDS]
Positional arguments:
{foo,bar,baz} The type of object to show
Optional arguments:
--help, -h Show this help message and exit
--ids IDS, -i IDS The IDs of the objects to show
Common options:
--verbose [VERBOSE], -v [VERBOSE]
Increase logging verbosity (can specify multiple times) (default: 0)
--env {dev,qa,uat,prod}, -e {dev,qa,uat,prod}
Environment to connect to (default: 'prod')
Mutually Exclusive
In the following example, wait
and no_wait
are mutually exclusive - if both are provided, then an exception is
raised. The tasks
and verbose
parameters are not in the group:
class TaskRunner(Command):
tasks = Positional(nargs='+', help='The tasks to run')
with ParamGroup('Wait Options', mutually_exclusive=True):
wait: int = Option('-w', default=1, help='Seconds to wait (0 or below to wait indefinitely)')
no_wait = Flag('-W', help='Do not wait')
verbose = Counter('-v', help='Increase logging verbosity (can specify multiple times)')
Note
Unlike using parser.add_mutually_exclusive_group(...)
from argparse
, mutually exclusive ParamGroups can
have a name / description that will be displayed in --help
text, just like they can for normal groups.
By making a group both mutually exclusive and required, we can ensure that one argument is always provided. Given the following example snippet:
class FindBaz(Find, choices=('baz', 'bazs'), help='Find baz objects'):
with ParamGroup(description='Filter Choices', mutually_exclusive=True, required=True):
foo = Option('-f', metavar='NAME', help='Find baz objects related to the foo object with the specified name')
bar: int = Option('-b', metavar='ID', help='Find baz objects related to the bar object with the specified ID')
def find_objects(self):
if self.foo:
...
else: # self.bar was provided
...
Either argument can be provided, but they cannot be combined:
$ rest_api_wrapper.py find baz -b 42 -f test
argument conflict - the following arguments cannot be combined: --foo / -f, --bar / -b (they are mutually exclusive - only one is allowed)
And one of them must be provided:
$ rest_api_wrapper.py find baz
arguments missing - the following arguments are required: --foo / -f, --bar / -b
Mutually Dependent
Mutually dependent groups provide a way to enforce that when one argument is provided for a Parameter in the group,
then arguments for all other Parameters in that group must also be provided. Similar to mutually exclusive groups,
unless the group itself is marked as required
, none of the members will be required if no arguments are provided
for any of the other members.
An example can be found below.
Hint
If a command has multiple required parameters, to avoid needing to repeat required=True
for each parameter,
they can be grouped in a ParamGroup(mutually_dependent=True, required=True)
. Since the group’s parameters are
marked as mutually dependent and the group is required, the group will require arguments to be provided for ALL of
its parameters.
Combining Group Types
When nesting a basic group inside of a mutually exclusive group, the members of the basic group can be combined, but none of the inner basic group members can be combined with the members of the outer exclusive group. Given the following example snippet:
class Sync(ApiWrapper, help='Sync group members'):
dry_run = Flag('-D', help='Print the actions that would be taken instead of taking them')
with ParamGroup(mutually_exclusive=True, required=True):
all = Flag('-a', help='Sync all groups')
with ParamGroup(): # --role and --group can be combined, but neither can be combined with --all
role = Option('-r', default='all', choices=('all', 'admin', 'user'), help='Sync members with this role')
group = Option('-g', help='Sync members for this group')
def main(self):
prefix = '[DRY RUN] Would sync' if self.dry_run else 'Syncing'
roles = ['admin', 'user'] if self.role == 'all' else [self.role]
groups = [self.group] if self.group else ['foo', 'bar', 'baz']
for group in groups:
for role in roles:
log.info(f'{prefix} group={group} members with role={role}')
We can see that a member needs to be provided:
$ rest_api_wrapper.py sync
arguments missing - the following arguments are required: --all / -a, {--role / -r,--group / -g}
The inner group members can be combined:
$ examples/rest_api_wrapper.py sync -g foo -r admin
Syncing group=foo members with role=admin
And neither can be combined with the mutually exclusive --all
Parameter:
$ rest_api_wrapper.py sync -g foo -a
argument conflict - the following arguments cannot be combined: --all / -a, {--role / -r,--group / -g} (they are mutually exclusive - only one is allowed)
Any of the valid combos can be combined with the Parameter outside of the group:
$ rest_api_wrapper.py sync -g foo -D
[DRY RUN] Would sync group=foo members with role=admin
[DRY RUN] Would sync group=foo members with role=user
$ rest_api_wrapper.py sync -aD
[DRY RUN] Would sync group=foo members with role=admin
[DRY RUN] Would sync group=foo members with role=user
[DRY RUN] Would sync group=bar members with role=admin
[DRY RUN] Would sync group=bar members with role=user
[DRY RUN] Would sync group=baz members with role=admin
[DRY RUN] Would sync group=baz members with role=user
Similarly, it is also possible to nest mutually dependent groups inside mutually exclusive groups. Using a refactored version of the same example:
class Sync(ApiWrapper, help='Sync group members'):
dry_run = Flag('-D', help='Print the actions that would be taken instead of taking them')
with ParamGroup(mutually_exclusive=True, required=True):
all = Flag('-a', help='Sync all groups')
with ParamGroup(mutually_dependent=True):
role = Option('-r', choices=('all', 'admin', 'user'), help='Sync members with this role')
group = Option('-g', help='Sync members for this group')
We can see the resulting output:
$ rest_api_wrapper.py sync -g foo
argument missing - the following argument is required: --role / -r (because --group/-g was provided)
$ rest_api_wrapper.py sync -r admin
argument missing - the following argument is required: --group / -g (because --role/-r was provided)
$ rest_api_wrapper.py sync -r admin -g foo
Syncing group=foo members with role=admin
$ rest_api_wrapper.py sync -r admin -g foo -a
argument conflict - the following arguments cannot be combined: --all / -a, {--role / -r,--group / -g} (they are mutually exclusive - only one is allowed)
How it Works
The nesting of exclusive / dependent (and basic) groups can work either way, and they can be nested multiple levels deep. They can also contain nested groups of the same mutual type.
Mutually Exclusive Outer Group
Given a mutually exclusive group A
that contains a mutually dependent group B
, if any member of B
is
provided, then all members of B
must be provided, but no other members of A
(that are not members of B
) may
be provided.
Given a mutually exclusive group A
that contains parameters x
and y
and a normal group B
, which
contains parameters c
and d
, then similar rules apply. It is possible to provide any one of x
, y
,
c
, or d
, but only c
and d
can be combined.
Mutually Dependent Outer Group
Given a mutually dependent group C
that contains a mutually exclusive group D
, if any member of C
is
provided, then all members of C
(that are not members of D
) must be provided, and one and only one member of
D
must be provided.
Given a mutually dependent group A
that contains parameters x
and y
and a normal group B
, which
contains parameters c
and d
, then similar rules apply. If any of x
, y
, c
, or d
are provided,
then x
and y
must always be provided, and one or both of c
and d
must be provided.
Examples
The following example demonstrates combinations in both directions for nested mutually exclusive / dependent groups using ActionFlags that simply print their corresponding letter:
class GroupedFlags(Command):
with ParamGroup(mutually_exclusive=True):
@before_main('-a', order=1)
def action_a(self):
print('a')
@before_main('-b', order=2)
def action_b(self):
print('b')
with ParamGroup(mutually_dependent=True):
@before_main('-c', order=3)
def action_c(self):
print('c')
@before_main('-d', order=4)
def action_d(self):
print('d')
with ParamGroup(mutually_dependent=True):
@after_main('-w', order=1)
def action_w(self):
print('w')
@after_main('-x', order=2)
def action_x(self):
print('x')
with ParamGroup(mutually_exclusive=True):
@after_main('-y', order=3)
def action_y(self):
print('y')
@after_main('-z', order=4)
def action_z(self):
print('z')
def main(self):
print('main')
Example output for the mutually dependent group nested inside the mutually exclusive group:
$ grouped_action_flags.py -a
a
main
$ grouped_action_flags.py -ab
argument conflict - the following arguments cannot be combined: --action-a / -a, --action-b / -b (they are mutually exclusive - only one is allowed)
$ grouped_action_flags.py -abc
argument conflict - the following arguments cannot be combined: --action-a / -a, --action-b / -b, {--action-c / -c,--action-d / -d} (they are mutually exclusive - only one is allowed)
$ grouped_action_flags.py -c
argument missing - the following argument is required: --action-d / -d (because --action-c/-c was provided)
$ grouped_action_flags.py -cd
c
d
main
Example output for the mutually exclusive group nested inside the mutually dependent group:
$ grouped_action_flags.py -w
arguments missing - the following arguments are required: --action-x / -x, {--action-y / -y,--action-z / -z} (because --action-w/-w was provided)
$ grouped_action_flags.py -wx
argument missing - the following argument is required: {--action-y / -y,--action-z / -z} (because --action-w/-w, --action-x/-x were provided)
$ grouped_action_flags.py -wxy
main
w
x
y
$ grouped_action_flags.py -wxyz
argument conflict - the following arguments cannot be combined: --action-y / -y, --action-z / -z (they are mutually exclusive - only one is allowed)