5

I'm trying to group arguments such that the user can do either:

python sample.py scan -a 1 -b 2
or
python sample.pt save -d /tmp -n something

here is my code:

import argparse
if __name__ == '__main__':
    parser = argparse.ArgumentParser(
            description='this is the description'
            )
    parser.add_argument('op', choices=['scan','save'], help='operation', default='scan')
    root_group = parser.add_mutually_exclusive_group()

    group1 = root_group.add_argument_group('g1', 'scan')
    group1.add_argument('-a', help='dir1')
    group1.add_argument('-b', help='dir2')

    group2 = root_group.add_argument_group('g2', 'save')
    group2.add_argument('-d', help='dir')
    group2.add_argument('-n', help='name')

    args = parser.parse_args()
    print args

as I run python sample.py --help

I'm getting an error. Can someone please tell me how to fix it?

Traceback (most recent call last):
  File "sample.py", line 18, in <module>
    args = parser.parse_args()
  File "C:\Python27\lib\argparse.py", line 1688, in parse_args
    args, argv = self.parse_known_args(args, namespace)
  File "C:\Python27\lib\argparse.py", line 1720, in parse_known_args
    namespace, args = self._parse_known_args(args, namespace)
  File "C:\Python27\lib\argparse.py", line 1926, in _parse_known_args
    start_index = consume_optional(start_index)
  File "C:\Python27\lib\argparse.py", line 1866, in consume_optional
    take_action(action, args, option_string)
  File "C:\Python27\lib\argparse.py", line 1794, in take_action
    action(self, namespace, argument_values, option_string)
  File "C:\Python27\lib\argparse.py", line 994, in __call__
    parser.print_help()
  File "C:\Python27\lib\argparse.py", line 2313, in print_help
    self._print_message(self.format_help(), file)
  File "C:\Python27\lib\argparse.py", line 2287, in format_help
    return formatter.format_help()
  File "C:\Python27\lib\argparse.py", line 279, in format_help
    help = self._root_section.format_help()
  File "C:\Python27\lib\argparse.py", line 209, in format_help
    func(*args)
  File "C:\Python27\lib\argparse.py", line 317, in _format_usage
    action_usage = format(optionals + positionals, groups)
  File "C:\Python27\lib\argparse.py", line 388, in _format_actions_usage
    start = actions.index(group._group_actions[0])
IndexError: list index out of range

and if I add action='store_const', the error goes away and a new error occurrs asking for 4 inputs.

max
  • 9,708
  • 15
  • 89
  • 144
  • remove `required=True` from `-d`, `-n` – itzMEonTV May 28 '15 at 07:22
  • That did not work. The only way to avoid it is to specify action but then the other error shows up. – max May 28 '15 at 07:28
  • 1
    As far as I know, add_mutually_exclusive_group() only takes one boolean argument. Did you check the definition of the function? https://docs.python.org/3.4/library/argparse.html#mutual-exclusion – Sandman May 28 '15 at 07:30
  • Yes. I've read the python doc before posting. – max May 28 '15 at 07:33
  • sorry, I meant to put 'g1' in the group parameter...still, the problem exists. – max May 28 '15 at 07:39
  • See the [tutorial](https://docs.python.org/2/howto/argparse.html?highlight=add_mutually_exclusive_group#conflicting-options), I find your structure quite different from that one – llrs May 28 '15 at 07:58
  • [Related question](http://stackoverflow.com/questions/7869345/how-to-make-python-argparse-mutually-exclusive-group-arguments-without-prefix) – llrs May 28 '15 at 08:05
  • The error is produced while formatting the usage line. – hpaulj May 28 '15 at 14:56

3 Answers3

4

Argparse does not seem to fully support adding a group into another group. This error happens because Argparse requires root_group to have some kind of action. A workaround would be adding an argument to the group:

import argparse

if __name__ == '__main__':
    parser = argparse.ArgumentParser(
            description='this is the description'
            )

    # This is now redundant. We can remove it
    # parser.add_argument('op', choices=['scan','save'], help='operation', default='scan')
    root_group = parser.add_mutually_exclusive_group()

    # Workaround
    root_group.add_argument('--scan', help='scan', action='store_true')
    root_group.add_argument('--save', help='save', action='store_true')

    group1 = root_group.add_argument_group('g1', 'scan')
    group2 = root_group.add_argument_group('g2', 'save')

    group1.add_argument('-a', help='dir1')
    group1.add_argument('-b', help='dir2')

    group2.add_argument('-d', help='dir', default='')
    group2.add_argument('-n', help='name')

    args = parser.parse_args()
    print args 

Notice that we are using --scan and --save. To avoid using the -- prefix, you may need the help of Sub-commands. Details can be found here.

ljk321
  • 16,242
  • 7
  • 48
  • 60
2

Thanks to @skyline's link above, I got it working with subparsers:

import argparse
if __name__ == '__main__':
    parser = argparse.ArgumentParser(
            description='this is the description'
            )

    scan_parser = argparse.ArgumentParser(add_help=False)
    scan_parser.add_argument('-a', '--a', help='first num', required=True)
    scan_parser.add_argument('-b', '--b', help='second num', required=True)

    save_parser = argparse.ArgumentParser(add_help=False)
    save_parser.add_argument('-d', '--d', help='directory path', required=True)
    save_parser.add_argument('-n', '--n', help='name of the file', required=True)

    sp = parser.add_subparsers()

    sp_scan = sp.add_parser('scan', parents=[scan_parser], help='scans directories')
    sp_save = sp.add_parser('save', parents=[save_parser], help='saves something')
    args = parser.parse_args()
    print args
max
  • 9,708
  • 15
  • 89
  • 144
  • 1
    If you mark this answer as the accepted will prevent others to try to answer your question ;) – llrs May 28 '15 at 09:06
2

The error occurs while formatting the usage line, and is the result of the root_group not having any _group_actions. I know from other bug issues that the usage formatter is fragile.

argument_groups and mutually_exclusive_groups are not designed to nest. And despite similar names (and class heritage) they have very different purposes. argument_groups control the display of the help lines. mutually_exclusive_groups control the usage display, and raise errors.

In your case, the arguments added to group1 and group2 where added to the master parser list, but not added to the list that root_group uses for exclusivity checking (or usage formatting).

If I add an argument directly to root_group, help works, and produces:

In [19]: parser.print_help()
usage: ipython3 [-h] [-a A] [-b B] [-d D] [-n N] [--foo FOO] {scan,save}

this is the description

positional arguments:
  {scan,save}  operation

optional arguments:
  -h, --help   show this help message and exit
  --foo FOO

You can see from the 'related' sidebar that a number of people have asked about adding groups of arguments to mutually_exclusive_groups. A patch that would allow nested groups with all sorts of logical conditions is a long ways in the future.

But, as you discovered, the subparser mechanism handles your particular case nicely.

You don't need to use the parents mechanism:

sp = parser.add_subparsers()

sp_scan = sp.add_parser('scan', help='scans directories')
sp_scan.add_argument('-a', '--a', help='first num', required=True)
sp_scan.add_argument('-b', '--b', help='second num', required=True)

sp_save = sp.add_parser('save', parents=[save_parser], help='saves something')
sp_save.add_argument('-d', '--d', help='directory path', required=True)
sp_save.add_argument('-n', '--n', help='name of the file', required=True)

The parents mechanism works here, but is intended more for cases where the parsers are defined elsewhere (and imported), or get reused in several subparsers. For example if you have many subparsers which share a core group of arguments (as well as their own unique ones).

hpaulj
  • 221,503
  • 14
  • 230
  • 353