Search code examples
pythoncommand-line-interfacecommand-line-argumentsargparse

Show subparser help and not main parser help when non-existent arguments used?


I have a small CLI app (myscript.py) that is defined like so.

import sys
import argparse

class MyParser(argparse.ArgumentParser):
    '''
    Overriden to show help on default.
    '''
    def error(self, message):
        print(f'error: {message}')
        self.print_help()
        sys.exit(2)

def myfunc(args):
    '''
    Some function.
    '''
    print(args.input_int**2)

def main():
    # Define Main Parser
    main_par = MyParser(
        prog='myapp',
        description='main help')

    # Define Command Parser
    cmd_par = main_par.add_subparsers(
        dest='command',
        required=True)

    # Add Subcommand Parser
    subcmd_par = cmd_par.add_parser(
        'subcmd',
        description='subcmd help')

    # Add Subcommand Argument
    subcmd_par.add_argument(
        '-i', '--input-int',
        type=int,
        help='some integer',
        required=True)

    # Add FromName Dispatcher
    subcmd_par.set_defaults(
        func=myfunc)

    # Parse Arguments
    args = main_par.parse_args()

    # Call Method
    args.func(args)

if __name__ == '__main__':
    main()

The MyParser class simply overrides the error() method in argparse.ArgumentParser class to print help on error.

When I execute

$ python myscript.py

I see the default / main help. Expected.

When I execute

$ python myscript.py subcmd

I see the subcmd help. Expected.

When I execute

$ python myscript.py subcmd -i ClearlyWrongValue

I also see the subcmd help. Expected.

However, very annoyingly if I do the following

$ python myscript.py subcmd -i 2 --non-existent-argument WhateverValue

I see the default / main help and not subcmd help.

What can I do, to ensure that this last case shows me the subcmd help and not the main help? I thought the subparser structure would automatically procure the help from subcmd as found in the third case, but it is not so? Why?


Solution

  • The unrecognized args error is raised by parse_args

    def parse_args(self, args=None, namespace=None):
        args, argv = self.parse_known_args(args, namespace)
        if argv:
            msg = _('unrecognized arguments: %s')
            self.error(msg % ' '.join(argv))
        return args
    

    The subparser is called via the cmd_par.__call__ with:

            subnamespace, arg_strings = parser.parse_known_args(arg_strings, None)
            for key, value in vars(subnamespace).items():
                setattr(namespace, key, value)
    
            if arg_strings:
                vars(namespace).setdefault(_UNRECOGNIZED_ARGS_ATTR, [])
                getattr(namespace, _UNRECOGNIZED_ARGS_ATTR).extend(arg_strings)
    

    That is it is called with parse_known_args, and it's extras are returned to the main as UNRECOGNIZED. So it's the main than handles these, not the subparser.

    In the $ python myscript.py subcmd -i ClearlyWrongValue case, the subparser raises a ArgumentError which is caught and converted into a self.error call.

    Similarly, the newish exit_on_error parameter handles this kind of ArgumentError, but does not handle the urecognized error. There was some discussion of this in the bug/issues.

    If you used parse_known_args, the extras would be ['--non-existent-argument', 'WhateverValue'], without distinguishing which parser initially classified them as such.