3

In the main function, I have a parser which validates optional inputs:

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--platform',required=True)
    parser.add_argument('--foo')
    parser.add_argument('--bar')
    parser.add_argument('--baz')
    parser.parse_args()

The above snippet is an example which only works when I supply --platform and either --foo,--bar or --baz.

This code is used by different components, let's call them components A, B and C.

Component A actually only specifies --foo and --bar:

python script.py --platform A --foo first_example --bar first_example

Component B actually only specifies --bar and --baz:

python script.py --platform B --bar second_example --baz second_exmaple

Component C actually only specifies --baz:

python script.py --platform C --baz third_example

As I introduce more components, which provide different arguments, the number of arguments I have to add to the parser increases. The above is just an example and I am currently dealing with 20 or so arguments (will likely be more in the future).

I have been thinking about having a configuration file (.yaml) where I define which arguments each component needs:

# parameters.yaml
A:
  - foo
  - bar

B:
  - bar
  - baz

C:
  - baz

I would like to simplify the main function to look at the --platform argument and, based on which platform has been passed as argument, read the configuration and add additional arguments to the parser.

Here's what I have tried:

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--platform',required=True)
    
    # Read from .yaml file
    with open('parameters.yaml') as parameter_file:
        parameters = yaml.safe_load(parameter_file)
    
    for argument in parameters[sys.argv[sys.argv.index('--platform') + 1]]:
        parser.add_argument(
           '--' + argument
    )

    parser.parse_args()

Calling the function:

python script.py --platform C --baz third_example

the above code works but I am looking for other Pythonic solutions as I am a beginner with Python. I don't really like having to look at sys.argv to determine what --platform has been specified. Are there any better solutions to this problem?

3
  • 1
    You should have a look on argparse' subparsers. Commented Nov 28, 2019 at 10:49
  • There are some questions about the behavior you want to have. 1) You want to have a certain set of optional parameters required for each platform? 2) Is it fine, if you have other parameters as None (without the error in command line if they are called mistakenly)? Commented Nov 28, 2019 at 10:52
  • @Fomalhaut 1) The set of optional parameters for each platform is defined in the configuration .yaml file. Each platform has its own set of optional parameters, which should be added to the parser only if a certain platform was specified when calling the function. 2) There should be an error if for a certain platform, e.g. C in my example, foo is specified. Commented Nov 28, 2019 at 11:13

2 Answers 2

4

With subparsers as sub-commands:

import argparse

def run_command(parser, args):
    if args.command == 'A':
        print(args)
    elif args.command == 'B':
        print(args)
    elif args.command == 'C':
        print(args)

parser = argparse.ArgumentParser(
    prog='PROG', 
    epilog="See '<command> --help' to read about a specific sub-command."
)
subparsers = parser.add_subparsers(dest='command', help='Sub-commands')

A_parser = subparsers.add_parser('A', help='Platform A')
A_parser.add_argument("--foo")
A_parser.add_argument('--bar')
A_parser.set_defaults(func=run_command)

B_parser = subparsers.add_parser('B', help='Platform B')
B_parser.add_argument('--bar')
B_parser.add_argument('--baz')
B_parser.set_defaults(func=run_command)

C_parser = subparsers.add_parser('C', help='Platform C')
C_parser.add_argument('--baz')
C_parser.set_defaults(func=run_command)

args = parser.parse_args()
if args.command is not None:
    args.func(parser, args)
else:
    parser.print_help()

This generates:

~ python args.py -h
usage: PROG [-h] {A,B,C} ...

positional arguments:
  {A,B,C}     Sub-commands
    A         Platform A
    B         Platform B
    C         Platform C

optional arguments:
  -h, --help  show this help message and exit

See '<command> --help' to read about a specific sub-command.

and

~ python args.py B -h
usage: PROG B [-h] [--bar BAR] [--baz BAZ]

optional arguments:
  -h, --help  show this help message and exit
  --bar BAR
  --baz BAZ
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks, this way I would have to create a parser for each component. I have tried implementing it this way and when I run the parser: args = parser.parse_args(['A']) every argument is None. How can I fix it?
You need to set default values for the arguments e.g. A_parser.add_argument("--foo", default=12) or give values to the arguments: args = parser.parse_args(['A', '--foo', 'abc', '--bar', '12'])
Thanks Alex, I've used your solution and it works nicely.
1

Using assert.

You can add assert if you want to forbid some parameters. The example:

parser = argparse.ArgumentParser()
parser.add_argument('--platform',required=True)
parser.add_argument('--foo')
parser.add_argument('--bar')
parser.add_argument('--baz')
args = parser.parse_args()

if args.platform == 'A':
    assert args.baz is None, "no argument baz for the platform A"
...

Using del

Excluding the parameters, that you don't need, with del:

parser = argparse.ArgumentParser()
parser.add_argument('--platform',required=True)
parser.add_argument('--foo')
parser.add_argument('--bar')
parser.add_argument('--baz')
args = parser.parse_args()

if args.platform == 'A':
    del args.baz
...

I wouldn't recommend you to use argparse and sys.argv in the same script, because they are for the same purpose in your case. It is better to choose one of them.

argparge parses your arguments before your script knows the values. Thus you can't change the list of parameters depending on the value of one of them. So if you want to keep argparse, you have to add the whole set of arguments with .add_argument and to do something extra below when the values are gotten.

5 Comments

Thanks for your answer, but it seems to me that what you are proposing would require all of the arguments to be added to the parser? That is what I am trying to avoid. "So if you want to keep argparse, you have to add the whole set of arguments with .add_argument and to do something extra below when the values are gotten." so you think what I am trying to accomplish cannot be achieved?
If you want to have an error if a wrong parameter is specified, use my assert way. It would be enough, wouldn't it?
Perhaps you misunderstood my question. I want to remove the adding of all the arguments, and only have the --platform argument. Depending on which value is given to the --platform argument, the values are read from the .yaml file and arguments are then added to the parser dynamically.
"so you think what I am trying to accomplish cannot be achieved" - literally yes. Because in argparse the set of names of the parameters are specified BEFORE parsing the command line, and the values come AFTER.
Thank you. I think the same about the answer you have accepted.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.