Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Correct way to get allowed arguments from ArgumentParser

Question: What is the intended / official way of accessing possible arguments from an existing argparse.ArgumentParser object?

Example: Let's assume the following context:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--foo', '-f', type=str)

Here I'd like to get the following list of allowed arguments:

['-h', '--foo', '--help', '-f']

I found the following workaround which does the trick for me

parser._option_string_actions.keys()

But I'm not happy with it, as it involves accessing a _-member that is not officially documented. Whats the correct alternative for this task?

like image 882
m8mble Avatar asked Oct 12 '16 10:10

m8mble


People also ask

What command line argument does the ArgumentParser provide by default?

Parsing arguments In a script, parse_args() will typically be called with no arguments, and the ArgumentParser will automatically determine the command-line arguments from sys.argv .

How do you construct an argument parser?

# construct the argument parser and parse the arguments ap = argparse. ArgumentParser() ap. add_argument("-d", "--dataset", required=True, help="Path to the directory of indexed images") ap. add_argument("-f", "--features-db", required=True, help="Path to the features database") ap.

What does Argparse ArgumentParser () do?

After importing the library, argparse. ArgumentParser() initializes the parser so that you can start to add custom arguments. To add your arguments, use parser.


2 Answers

I don't think there is a "better" way to achieve what you want.


If you really don't want to use the _option_string_actions attribute, you could process the parser.format_usage() to retrieve the options, but doing this, you will get only the short options names.

If you want both short and long options names, you could process the parser.format_help() instead.

This process can be done with a very simple regular expression: -+\w+

import re

OPTION_RE = re.compile(r"-+\w+")
PARSER_HELP = """usage: test_args_2.py [-h] [--foo FOO] [--bar BAR]

optional arguments:
  -h, --help         show this help message and exit
  --foo FOO, -f FOO  a random options
  --bar BAR, -b BAR  a more random option
"""

options = set(OPTION_RE.findall(PARSER_HELP))

print(options)
# set(['-f', '-b', '--bar', '-h', '--help', '--foo'])

Or you could first make a dictionnary which contains the argument parser configuration and then build the argmuent parser from it. Such a dictionnary could have the option names as key and the option configuration as value. Doing this, you can access the options list via the dictionnary keys flattened with itertools.chain:

import argparse
import itertools

parser_config = {
    ('--foo', '-f'): {"help": "a random options", "type": str},
    ('--bar', '-b'): {"help": "a more random option", "type": int, "default": 0}
}

parser = argparse.ArgumentParser()
for option, config in parser_config.items():
    parser.add_argument(*option, **config)

print(parser.format_help())
# usage: test_args_2.py [-h] [--foo FOO] [--bar BAR]
# 
# optional arguments:
#   -h, --help         show this help message and exit
#   --foo FOO, -f FOO  a random options
#   --bar BAR, -b BAR  a more random option

print(list(itertools.chain(*parser_config.keys())))
# ['--foo', '-f', '--bar', '-b']

This last way is what I would do, if I was reluctant to use _option_string_actions.

like image 190
Tryph Avatar answered Oct 17 '22 03:10

Tryph


This started as a joke answer, but I've learned something since - so I'll post it.

Assume, we know the maximum length of an option allowed. Here is a nice answer to the question in this situation:

from itertools import combinations

def parsable(option):
    try:
        return len(parser.parse_known_args(option.split())[1]) != 2
    except:
        return False

def test(tester, option):
    return any([tester(str(option) + ' ' + str(v)) for v in ['0', '0.0']])

def allowed_options(parser, max_len=3, min_len=1):
    acceptable = []
    for l in range(min_len, max_len + 1):
        for option in combinations([c for c in [chr(i) for i in range(33, 127)] if c != '-'], l):
            option = ''.join(option)
            acceptable += [p + option for p in ['-', '--'] if test(parsable, p + option)]
    return acceptable

Of course this is very pedantic as the question doesn't require any specific runtime. So I'll ignore that here. I'll also disregard, that the above version produces a mess of output because one can get rid of it easily.

But more importantly, this method detected the following interesting argparse "features":

  • In in the OP example, argparse would also allow --fo. This has to be a bug.
  • But further, in the OP example again, argparse would also allow -fo (ie. setting foo to o without space or anything). This is documented and intended, but I didn't know it.

Because of this, a correct solution is a bit longer and would look something like this (only parsable changes, I'll omit the other methods):

def parsable(option):
    try:
        default = vars(parser.parse_known_args(['--' + '0' * 200])[0])
        parsed, remaining = parser.parse_known_args(option.split())
        if len(remaining)  == 2:
            return False
        parsed = vars(parsed)
        for k in parsed.keys():
            try:
                if k in default and default[k] != parsed[k] and float(parsed[k]) != 0.0:
                    return False  # Filter '-fx' cases where '-f' is the argument and 'x' the value.
            except:
                return False
        return True
    except:
        return False

Summary: Besides all the restrictions (runtime and fixed maximum option length), this is the only answer that correctly respects the real parser behavior - however buggy it may even be. So here you are, a perfect answer that is absolutely useless.

like image 30
m8mble Avatar answered Oct 17 '22 01:10

m8mble