how to get argparse to read arguments from a file with an option rather than prefix

17,757

Solution 1

You can solve this by using a custom argparse.Action that opens the file, parses the file contents and then adds the arguments then.

For example this would be a very simple action:

class LoadFromFile (argparse.Action):
    def __call__ (self, parser, namespace, values, option_string = None):
        with values as f:
            # parse arguments in the file and store them in the target namespace
            parser.parse_args(f.read().split(), namespace)

Which you can the use like this:

parser = argparse.ArgumentParser()
# other arguments
parser.add_argument('--file', type=open, action=LoadFromFile)
args = parser.parse_args()

The resulting namespace in args will then also contain any configuration that was also loaded from the file where the file contained arguments in the same syntax as on the command line (e.g. --foo 1 --bar 2).

If you need a more sophisticated parsing, you can also parse the in-file configuration separately first and then selectively choose which values should be taken over. For example, since the arguments are evalutated in the order they are specified, it might make sense to prevent the configurations in the file from overwriting values that have been explicitly specified ont the command line. This would allow using the configuration file for defaults:

def __call__ (self, parser, namespace, values, option_string=None):
    with values as f:
        contents = f.read()
    # parse arguments in the file and store them in a blank namespace
    data = parser.parse_args(contents.split(), namespace=None)
    for k, v in vars(data).items():
        # set arguments in the target namespace if they haven’t been set yet
        if getattr(namespace, k, None) is not None:
            setattr(namespace, k, v)

Of course, you could also make the file reading a bit more complicated, for example read from JSON first.

Solution 2

You commented that

I need to be able to write my own function to read that file and return the arguments (it's not in a one-argument-per-line format) –

There is a provision in the existing prefix-file handler to change how the file is read. The file is read by a 'private' method, parser._read_args_from_files, but it calls a simple public method that converts a line to strings, default one-argument-per-line action:

def convert_arg_line_to_args(self, arg_line):
    return [arg_line]

It was written this way so you could easily customize it. https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.convert_arg_line_to_args

A useful override of this method is one that treats each space-separated word as an argument:

def convert_arg_line_to_args(self, arg_line):
    for arg in arg_line.split():
        if not arg.strip():
            continue
        yield arg

In the test_argparse.py unittesting file there is a test case for this alternative.


But if you still want to trigger this read with an argument option, instead of a prefix character, then the custom Action approach is a good one.

You could though write your own function that processes argv before it is passed to the parser. It could be modeled on parser._read_args_from_files.

So you could write a function like:

def read_my_file(argv):
    # if there is a '-A' string in argv, replace it, and the following filename
    # with the contents of the file (as strings)
    # you can adapt code from _read_args_from_files
    new_argv = []
    for a in argv:
        ....
        # details left to user
    return new_argv

Then invoke your parser with:

parser.parse_args(read_my_file(sys.argv[1:]))

And yes, this could be wrapped in a ArgumentParser subclass.

Solution 3

An Action, when called, gets parser and namespace among its arguments.

So you can put your file through the former to update the latter:

class ArgfileAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        extra_args = <parse_the_file>(values)
        #`namespace' is updated in-place when specified
        parser.parse_args(extra_args,namespace)
Share:
17,757

Related videos on Youtube

Russell Smith
Author by

Russell Smith

I started as a FORTRAN programmer, paid my dues writing C and X11/Motif, switched to Perl, discovered Tk, and from that, Tcl, and spent the next decade plus writing cross-platform GUIs in Tcl/Tk. I then spent three years using python and a smattering of ruby to create a cross-platform automated testing framework. After that I had two more gigs in windows shows, building automated testing frameworks and leading automation teams. I am now back to being a python developer at a small company in the independent publishing business. I was profiled as python developer of the week at http://www.blog.pythonlibrary.org/2015/03/02/pydev-of-the-week-bryan-oakley/ The open source projects I currently am active on are: The Robot Framework Hub - A web app and RESTful API for accessing robot framework test assets Robot Framework Lint - a static analysis tool for robot framework test files. Page Object Library - an implementation of the page object pattern for robot framework Robot framework extension for the brackets open source editor. I also maintain a sporadically-updated blog at boakley.github.io, focused mainly on my work with the robot framework.

Updated on July 25, 2022

Comments

  • Russell Smith
    Russell Smith 5 months

    I would like to know how to use python's argparse module to read arguments both from the command line and possibly from text files. I know of argparse's fromfile_prefix_chars but that's not exactly what I want. I want the behavior, but I don't want the syntax. I want an interface that looks like this:

    $ python myprogram.py --foo 1 -A somefile.txt --bar 2
    

    When argparse sees -A, it should stop reading from sys.argv or whatever I give it, and call a function I write that will read somefile.text and return a list of arguments. When the file is exhausted it should resume parsing sys.argv or whatever. It's important that the processing of the arguments in the file happen in order (ie: -foo should be processed, then the arguments in the file, then -bar, so that the arguments in the file may override --foo, and --bar might override what's in the file).

    Is such a thing possible? Can I write a custom function that pushes new arguments onto argparse's stack, or something to that effect?

  • mgilson
    mgilson about 8 years
    I believe that OP already knows about this feature as indicated by the statement "I know of argparse's fromfile_prefix_chars but that's not exactly what I want"
  • Russell Smith
    Russell Smith about 8 years
    Ah-ha! The key is realizing that the parser is passed to the custom action, and you can use that parser to process the contents of the file. Thank you.
  • hpaulj
    hpaulj about 6 years
    But if the parser had a different name in the global environment, it would still be available here. So any parser could be used here. There's nothing special about the parser passed as parameter.
  • Hack Saw
    Hack Saw over 5 years
    Alas, this fails if any of your parameters are set to Required.
  • poke
    poke about 2 years
    @RovshanMusayev The file format would be identical to what you provide to the command line. E.g. --foo 1 --bar 2. But you can customize the logic in your action to your liking to support any kind of format.
  • Rovshan Musayev
    Rovshan Musayev about 2 years
    Thanks you very much. It was helpful.
  • kdubs
    kdubs almost 2 years
    if you change the line to this : parser.add_argument('--file', type=open, action=LoadFromFile, default=argparse.SUPPRESS) then you don't have to deal with the blank file argument

Related