0

I'm using argparse to digest text commands rather than trying to roll my own custom parser, but the code path is not obvious. Consider the following:

import argparse

##class ReadAction(argparse.Action):
##    def __init__(self, option_strings, dest, nargs=None, **kwargs):
##        if nargs is not None:
##            raise ValueError("nargs not allowed")
##        super(ReadAction, self).__init__(option_strings, dest, **kwargs)
##    def __call__(self, parser, namespace, values, option_strings=None):
##        print("Read a file")
##        setattr(namespace, self.dest, values)

class ExitAction(argparse.Action):
    def __init__(self, option_strings, dest, nargs=None, **kwargs):
        if nargs is not None:
            raise ValueError("nargs not allowed")
        super(ExitAction, self).__init__(option_strings, dest, **kwargs)
    def __call__(self, parser, namespace, values, option_strings=None):
        print("Exiting the program")
        setattr(namespace, self.dest, values)

def setup_parser(parser):
##    parser.add_argument('read', help='Reads in a file', action=ReadAction)
    parser.add_argument('exit', help='Exit command', action=ExitAction)

def run():
    parser = argparse.ArgumentParser()
    setup_parser(parser)
    while True:
        raw_input = input("Command >>>").split(' ')
        args = parser.parse_args(raw_input)
        print(args)
    print('Good bye')

if __name__ == '__main__':
    run()

If I run it as is, I get the expected output:

Command >>>exit
Exiting the program
Namespace(exit='exit')

But if I take out the comments and run again, I get this unexpected behavior:

Command >>>exit
Read a file
usage: prog.py [-h] read exit
prog.py: error: the following arguments are required: exit

Does anyone understand the code path through this? It's like the __call__ method isn't being called (ironic).

2 Answers2

0

You aren't binding commands to the literal strings read and exit; that's not how argparse works. Instead, you are defining a parser that takes two arbitrary words, and binds the first to read and the second to exit. Your commented code would exit no matter what single word you typed, not just exit. Without the comments, the parser expects two words, and raises an error when you only provide one.

If you are going to (ab)use argparse in this fashion, I suggest you read up on subcommands in the documentation.

chepner
  • 497,756
  • 71
  • 530
  • 681
0

Both arguments read and exit expect one string.

Parsing the first string results in the ReadAction.__call__ being called, setting a value in the args Namespace. If you provide only one string, the parser raises the error because the exit argument has not been supplied. If you'd provided two strings, the 2nd string would be given to the ExitAction.__call__.

If the parser finishes without an error, your code continues with the while loop, without any exit. The only way out of that loop is for the parser to raise an error.

With this error, the parser prints usage and the message, and then calls sys.exit(2). A -h command will cause the -h action to be called. That prints the help and also calls sys.exit(2).

While argparse can be used to parse input strings, it's not designed for that task. Normally it parses sys.argv[1:]. That said, ipython magic commands often use argparse to parse parameters, behaving much like sys shell commands.

argparse does not allocate positional arguments based on value - it allocates them strictly on position. If you define both read and exit, then the first string goes to read regardless of its value, and second to exit. (See my recent answer: https://stackoverflow.com/a/53605878/901925)

Flagged (optionals) are allocated based on value. But to use those you have to give commands like

--read foo --exit bar

hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • I may come back around and investigate using the optional arguments and see if that suits my purposes, but time is short, so I have to build my own simple parser. Thanks! – Matt Minton Dec 07 '18 at 15:51