45

I'd like to have a program that takes a --action= flag, where the valid choices are dump and upload, with upload being the default. If (and only if) dump is selected, I'd like there to also be a --dump-format= option. Is there a way to express this using argparse, or do I need to just accept all the arguments and do the logic myself.

Alex Gaynor
  • 14,353
  • 9
  • 63
  • 113
  • Is it a viable option (aesthetically speaking) to do something like `--action=dump-csv` or `--action=dump-some-other-format`? This would alleviate the problem of having "required options" entirely. – dcrosta Feb 29 '12 at 20:32
  • @dcrosta it would obviously work, but I prefer not to go that way, I find it unwieldy. – Alex Gaynor Feb 29 '12 at 20:35
  • Fair enough, just wanted to make sure you've covered the obvious bases. – dcrosta Feb 29 '12 at 20:37
  • He needed default to be upload -- parser.add_argument('--action', choices=['upload', 'dump'], default='dump') but I did not think of parser.error. – Iman Feb 29 '12 at 20:53

5 Answers5

81

The argparse module offers a way to do this without implementing your own requiredness checks. The example below uses "subparsers" or "sub commands". I've implemented a subparser for "dump" and one for "format".

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('file', help='The file you want to act on.')
subparsers = parser.add_subparsers(dest='subcommand')
subparsers.required = True  # required since 3.7

#  subparser for dump
parser_dump = subparsers.add_parser('dump')
# add a required argument
parser_dump.add_argument(
    'format',
    choices=['csv', 'json'],
    help='Dump the file in this format.')

#  subparser for upload
parser_upload = subparsers.add_parser('upload')
# add a required argument
parser_upload.add_argument(
    'server',
    choices=['amazon', 'imgur'],
    help='Upload the file to this service.')

args = parser.parse_args()
print args
if args.subcommand == 'dump':
    print 'I will now dump "%s" in the %s format' % (args.file, args.format)
if args.subcommand == 'upload':
    print 'I will now upload "%s" to %s' % (args.file, args.server)

That looks like this on the command line:

$ python ap.py 
usage: ap.py [-h] file {upload,dump} ...
ap.py: error: too few arguments

$ python ap.py tmp.txt 
usage: ap.py [-h] file {upload,dump} ...
ap.py: error: too few arguments

Upload:

$ python ap.py tmp.txt upload
usage: ap.py file upload [-h] {amazon,imgur}
ap.py file upload: error: too few arguments

$ python ap.py tmp.txt upload amazo
usage: ap.py file upload [-h] {amazon,imgur}
ap.py file upload: error: argument server: invalid choice: 'amazo' (choose from 'amazon', 'imgur')

$ python ap.py tmp.txt upload amazon
Namespace(file='tmp.txt', server='amazon', subcommand='upload')
I will now upload "tmp.txt" to amazon

$ python ap.py tmp.txt upload imgur
Namespace(file='tmp.txt', server='imgur', subcommand='upload')
I will now upload "tmp.txt" to imgur

Dump:

$ python ap.py tmp.txt dump
usage: ap.py file dump [-h] {csv,json}
ap.py file dump: error: too few arguments

$ python ap.py tmp.txt dump csv
Namespace(file='tmp.txt', format='csv', subcommand='dump')
I will now dump "tmp.txt" in the csv format

$ python ap.py tmp.txt dump json
Namespace(file='tmp.txt', format='json', subcommand='dump')
I will now dump "tmp.txt" in the json format

More info: ArgumentParser.add_subparsers()

wjandrea
  • 28,235
  • 9
  • 60
  • 81
Niels Bom
  • 8,728
  • 11
  • 46
  • 62
43

Another way to approach the problem is by using subcommands (a'la git) with "action" as the first argument:

script dump --dump-format="foo"
script upload
codysoyland
  • 633
  • 1
  • 5
  • 7
  • One limitation is that `argparse` does not allow multiple `subparsers`. – Liang Dec 17 '18 at 15:17
  • 1
    This is wrong. You can add as many subparsers as you want. See subcommands link referenced in answer. See also [Niels Bom's answer](https://stackoverflow.com/a/13706448/2311167) – Adrian W Jun 16 '19 at 20:41
  • ... or did you mean _nesting_ of subcommands? Something like `dump dumpsubcommand1` , `dump subcommand2`, `upload subcommand3` etc..? – Adrian W Jun 16 '19 at 21:05
22

You could use parser.error:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--action', choices=['upload', 'dump'], default='dump')
parser.add_argument('--dump-format')
args = parser.parse_args()
if args.action != 'dump' and args.dump_format:
    parser.error('--dump-format can only be set when --action=dump.')
Jakub Roztocil
  • 15,930
  • 5
  • 50
  • 52
  • 12
    This is only describing the "argparse way" of reporting an error. The logic is implemented manually. Not an "argparse" way of conditionally requiring arguments. – Niels Bom Dec 04 '12 at 15:07
  • 1
    If instead of "dump" and "format" there are 5 more options that all have their own set of required or non-required arguments this way of checking for requiredness can get unclear fast. – Niels Bom Dec 04 '12 at 15:22
  • Besides, things like argparse.FileType will get problems. For certain selected actions when the FileType argument is not needed, non-existence of the file causes a false error. – Joshua Chia Dec 26 '12 at 15:56
5

It depends what you consider "doing all the logic yourself". You can still use argparse and add the dump option as follows with minimal effort without resorting to sub-commands:

from argparse import ArgumentParser
from sys import argv

parser = ArgumentParser()
action_choices = ['upload', 'dump']
parser.add_argument('--action', choices=action_choices, default=action_choices[1])
parser.add_argument('--dump-format', required=(action_choices[1] in argv))

This way argparse won't care about the dump format if the dump action wasn't selected

Dmytro Bugayev
  • 606
  • 9
  • 13
1

Try this.

Suppose you have a tool that lists, adds and deletes records in a table with the following structure:

id sitekey response status
1 123456 valid a
2 234567 invalid
3 345678 invalid c
4 456789 valid d

And you want to have the following operations and arguments:

  • list
    • from: optional
    • to: optional
    • short-response: optional
  • add
    • sitekey: required
    • response: required
    • status: optional
  • remove
    • id: required

Then, you can have a code similar to the following:

import argparse
import sys

operation_choices = ['list', 'add', 'remove']
parser = argparse.ArgumentParser()
parser.add_argument("--operation",
                    choices = operation_choices,
                    default = operation_choices[0],
                    help = "Your help!",
                    required = True)

# list operation subarguments
if True in list(map(lambda x: operation_choices[0] in x, sys.argv)):
    parser.add_argument("--from",
                        type = int,
                        default = 1,
                        help = "Your help!",
                        required = False)
    parser.add_argument("--to",
                        type = int,
                        help = "Your help!",
                        required = False)
    parser.add_argument("--short-response",
                        type = bool,
                        default = True,
                        help = "Your help!",
                        required = False)

# add operation subarguments
if True in list(map(lambda x: operation_choices[1] in x, sys.argv)):
    parser.add_argument("--sitekey",
                        type = str,
                        help = "Your help!",
                        required = True)
    parser.add_argument("--response",
                        type = str,
                        help = "Your help!",
                        required = True)
    parser.add_argument("--status",
                        type = str,
                        help = "Your help!",
                        required = False)

# remove operation subarguments
if True in list(map(lambda x: operation_choices[2] in x, sys.argv)):
    parser.add_argument("--id",
                        type = int,
                        help = "Your help!",
                        required = True)

args = parser.parse_args()

# Your operations...

So when you run:

$ python tool.py --operation=list

This run, no required arguments

$ python tool.py --operation=add

usage: tool.py [-h] --operation {list,add,remove} --sitekey SITEKEY --response RESPONSE [--status STATUS]
tool.py: error: the following arguments are required: --sitekey, --response

$ python tool.py --operation=remove

usage: tool.py [-h] --operation {list,add,remove} --id ID
tool.py: error: the following arguments are required: --id

$ python tool.py --help

usage: tool.py [-h] --operation {list,add,remove}

options:
  -h, --help            show this help message and exit
  --operation {list,add,remove}
                        Your help!

$ python tool.py --operation=list --help

usage: tool.py [-h] --operation {list,add,remove} [--from FROM] [--to TO] [--short-response SHORT_RESPONSE]

options:
  -h, --help            show this help message and exit
  --operation {list,add,remove}
                        Your help!
  --from FROM           Your help!
  --to TO               Your help!
  --short-response SHORT_RESPONSE
                        Your help!

$ python tool.py --operation=add --help

usage: tool.py [-h] --operation {list,add,remove} --sitekey SITEKEY --response RESPONSE [--status STATUS]

options:
  -h, --help            show this help message and exit
  --operation {list,add,remove}
                        Your help!
  --sitekey SITEKEY     Your help!
  --response RESPONSE   Your help!
  --status STATUS       Your help!

$ python tool.py --operation=remove --help

usage: tool.py [-h] --operation {list,add,remove} --id ID

options:
  -h, --help            show this help message and exit
  --operation {list,add,remove}
                        Your help!
  --id ID               Your help!