Search code examples
pythonargparse

Argparse: How to switch from default parser to a different subparser when a certain optional argument is given?


I have a certain script that is normally called with 2 positional arguments and bunch of optional arguments.

script.py <file1> <file2> 

I want to add another subparser which should be called when I pass an optional argument.

script.py -file_list <files.list>

Basically, what I require is that when -file_list is passed, the parser shouldn't look for file1 and file2. I do not want the default case to require another option to invoke it (since the default case is already in use and thus I do not want to break it).

I tried keeping the default parser as is and creating subparser for -file_list. But the parser still expects the positional arguments file1 and file2.

Sample code (this doesn't work as I want it to):

args = argparse.ArgumentParser()

#default arguments
args.add_argument("file1", type=str)
args.add_argument("file2", type=str)

#subparser for file_list
file_list_sp = args.add_subparsers()
file_list_parser = file_list_sp.ad_parser("-file_list")
file_list_parser.add_argument("file_list")

all_args = args.parse_args()

Maybe I need to create a seperate subparser for the default case; but all subparsers seem to need an extra command to invoke them. I want default case to be invoked automatically whenever -file_list is not passed


Solution

  • You mention in passing other optionals, so I assume there's good incentive to keep argparse. Assuming there aren't other positionals, you could try:

    In [23]: parser = argparse.ArgumentParser()    
    In [24]: parser.add_argument('--other'); # other optionals
    In [25]: parser.add_argument('--filelist');   
    In [26]: parser.add_argument('files', nargs='*');
    

    This accepts the '--filelist', without requiring the positional files, (with a resulting empty list):

    In [27]: parser.parse_args('--filelist alist'.split())
    Out[27]: Namespace(other=None, filelist='alist', files=[])
    

    Other the files - but restricting that list to 2 requires your own testing after parsing:

    In [28]: parser.parse_args('file1 file2'.split())
    Out[28]: Namespace(other=None, filelist=None, files=['file1', 'file2'])
    

    But it also accepts both forms. Again your own post parsing code will have to sort out the conflicting message:

    In [29]: parser.parse_args('file1 file2 --filelist xxx'.split())
    Out[29]: Namespace(other=None, filelist='xxx', files=['file1', 'file2'])
    

    And you have to deal with the case where neither is provided:

    In [30]: parser.parse_args('--other foobar'.split())
    Out[30]: Namespace(other='foobar', filelist=None, files=[])
    

    The help:

    In [32]: parser.print_help()
    usage: ipykernel_launcher.py [-h] [--other OTHER] [--filelist FILELIST]
                                 [files ...]
    
    positional arguments:
      files
    
    optional arguments:
      -h, --help           show this help message and exit
      --other OTHER
      --filelist FILELIST
    

    So what I've created accepts both forms of input, but does not constrain them.

    I tried using a mutually_exclusive_group, but that only works with nargs='?'. I thought at one time nargs='*' worked in a group, but either my memory is wrong, or there was a patch that changed things. A positional in a m-x-group has to have 'required=False' value.

    A subparsers is actually a special kind of positional. So if you created parser with usage 'prog file1 file2 {cmd1, cmd2}' it would still expect the 2 file names before checking on the third string. All positionals, including subparsers are handled by position, not value. A flagged argument (optional) can occur in any order, but does not override the positionals.

    And if you define the subparsers first, then the first positional has to be one of those commands. subparsers are not required (by default), but that doesn't change the handling of other positionals.

    working mutually exclusive group

    nargs='*' does work if we also specify a default. This is a undocumented detail. I forgot about it until I looked at the code (specifically the _get_positional_kwargs method). A * is marked as required unless it's given a default. It's been discussed in one or more bug/issue some time ago.

    So can make positional list and '--file_list' mutually exclusive

    In [71]: parser = argparse.ArgumentParser()
    In [72]: parser.add_argument('--other'); # other optionals
    In [73]: group = parser.add_mutually_exclusive_group(required=True)
    In [74]: group.add_argument('--file_list');
    In [75]: group.add_argument('files', nargs='*', default=[]);
    

    Good cases:

    In [76]: parser.parse_args('--other foobar file1 file2'.split())
    Out[76]: Namespace(other='foobar', file_list=None, files=['file1', 'file2'])
    
    In [77]: parser.parse_args('--other foobar --file_list file3'.split())
    Out[77]: Namespace(other='foobar', file_list='file3', files=[])
    

    If both are used:

    In [78]: parser.parse_args('--other foobar --file_list file3 file1 file2'.split())
    usage: ipykernel_launcher.py [-h] [--other OTHER] [--file_list FILE_LIST]
                                 [files ...]
    ipykernel_launcher.py: error: argument files: not allowed with argument --file_list
    

    And if neither is provided:

    In [79]: parser.parse_args('--other foobar'.split())
    usage: ipykernel_launcher.py [-h] [--other OTHER] [--file_list FILE_LIST]
                                 [files ...]
    ipykernel_launcher.py: error: one of the arguments --file_list files is required
    

    Usage now looks like:

    In [81]: parser.print_usage()
    usage: ipykernel_launcher.py [-h] [--other OTHER] [--file_list FILE_LIST]
                                 [files ...]
    

    It still doesn't control the number of files - except there must be atleast one.