0

What I want to achieve is this: --read_file <some file> and positional argument should only be specified either one. prog --read_file inputfile is OK. prog --read_file is also OK. prog input1 input2 ... is OK. prog input1 --read_file is BAD.

so I did this:

readlist_group.add_argument(
    "--read_file",
    dest="read_file",
    metavar="file_to_read",
    help="If enabled, input will be ignored and list will be read from given file. [default: %(default)s][const: %(const)s]",
    type=argparse.FileType("r"),
    default=None,
    const=sys.stdin,
    nargs="?",
)

readlist_group.add_argument(
    dest="inputs",
    help="inputs",
    metavar="input",
    type=str,
    nargs="*",
)

However, I got ValueError: mutually exclusive arguments must be optional But "*" is kind of optional isn't it?

Wang
  • 7,250
  • 4
  • 35
  • 66

1 Answers1

1

A '*' argument is 'required' unless a default is also provided.

In [15]: parser=argparse.ArgumentParser()                                       
In [16]: gp = parser.add_mutually_exclusive_group()                             
In [17]: a = gp.add_argument('-r', nargs='?', default='def', const='con')       
In [18]: b = gp.add_argument('foo', nargs='*', default=[])                      
In [19]: parser.print_help()                                                    
usage: ipython3 [-h] [-r [R] | foo [foo ...]]

positional arguments:
  foo

optional arguments:
  -h, --help  show this help message and exit
  -r [R]

I should review the code and/or docs to see why.

In [22]: parser.parse_args([])                                                  
Out[22]: Namespace(foo=[], r='def')
In [23]: parser.parse_args(['-r'])                                              
Out[23]: Namespace(foo=[], r='con')
In [24]: parser.parse_args(['-r','test'])                                       
Out[24]: Namespace(foo=[], r='test')
In [25]: parser.parse_args(['test'])                                            
Out[25]: Namespace(foo=['test'], r='def')
In [26]: parser.parse_args(['test','1','2'])                                    
Out[26]: Namespace(foo=['test', '1', '2'], r='def')
In [27]: parser.parse_args(['test','1','2','-r'])                               
usage: ipython3 [-h] [-r [R] | foo [foo ...]]
ipython3: error: argument -r: not allowed with argument foo

In [28]: parser.parse_args(['-r','a','b'])                                      
usage: ipython3 [-h] [-r [R] | foo [foo ...]]
ipython3: error: argument foo: not allowed with argument -r

OK, here it is in the code:

def _get_positional_kwargs(self, dest, **kwargs):
    # make sure required is not specified
    if 'required' in kwargs:
        msg = _("'required' is an invalid argument for positionals")
        raise TypeError(msg)

    # mark positional arguments as required if at least one is
    # always required
    if kwargs.get('nargs') not in [OPTIONAL, ZERO_OR_MORE]:
        kwargs['required'] = True
    if kwargs.get('nargs') == ZERO_OR_MORE and 'default' not in kwargs:
        kwargs['required'] = True

    # return the keyword arguments with no option strings
    return dict(kwargs, dest=dest, option_strings=[])

There probably has been some discussion in the bug/issues as to why it's coded this way, but I don't recall any details.

This puzzled me a couple of years ago:

https://bugs.python.org/issue26235 (needs to be documented)

How can I create an argparse mutually exclusive group with multiple positional parameters?

hpaulj
  • 221,503
  • 14
  • 230
  • 353