How to conditionally make python's argparse module require additional arguments - python

Basic intended usage:
my_framework create Project_title /path/to/project
OR
my_framework create Project_title (ie. use current working directory)
OR
my_framework update (ie. update my_framework rather than creating a new project)
I know I can make name optional by providing it with a default, but, in reality name is not optional provided the user has entered create as the first argument.
Best solution I've come up with is to use a default value for name and then, if the argument name equals its default value, throw an error. But if there's a way to make argparse do this work for me I'd rather learn to do it.
Writing two scripts, my_framework_create and my_framework_update doesn't appeal to me aesthetically.
#!/usr/bin/env python
import argparse
import os
import shutil
from subprocess import call
template_path = "/usr/local/klibs/template"
parser = argparse.ArgumentParser("MY_FRAMEWORK CLI", description='Creates a new MY_FRAMEWORK project or updates MY_FRAMEWORK')
parser.add_argument('action', choices=['create', 'update'], type=str, help='<help text>')
parser.add_argument('name', type=str, help='<help text>')
parser.add_argument('path', default=os.getcwd(), nargs="?", type=str, help='<help text>')
args = parser.parse_args()
if args.action == "create":
# do the create stuff
if args.action == "update":
# do the update stuff

The best way to do this is with a subparser
An example from the docs:
>>> parser = argparse.ArgumentParser()
>>> subparsers = parser.add_subparsers(title='subcommands',
... description='valid subcommands',
... help='additional help')
>>> subparsers.add_parser('foo')
>>> subparsers.add_parser('bar')
>>> parser.parse_args(['-h'])
usage: [-h] {foo,bar} ...
optional arguments:
-h, --help show this help message and exit
subcommands:
valid subcommands
{foo,bar} additional help
In your case you would have create and update as separate subparsers.
Example:
def create(args):
# do the create stuff
print(args)
def update(args):
# do the update stuff
print(args)
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title='subcommands',
description='valid subcommands',
help='additional help')
create_parser = subparsers.add_parser('create')
create_parser.add_argument('name', type=str)
create_parser.set_defaults(func=create)
update_parser = subparsers.add_parser('update')
update_parser.set_defaults(func=update)

Related

argparse: unable to get subparser_name after declaring a global argument

I've got a parser with some sub-parsers. I setup a global argument to be used on all subparser. Here's the relevant snippet
parser = argparse.ArgumentParser(prog="my_prog", add_help=False)
parser.add_argument('-d', '--debug', action='store_true', help='debug flag')
subparsers = parser.add_subparsers(dest="subparser_name", help='some help notes')
parser_cmd1 = subparsers.add_parser('cmd1', parents=[parser])
parser_cmd1.add_argument('-f', '-foo', type=str, action=foo, required=False, help='foo command')
parser_cmd2 = subparsers.add_parser('cmd2', parents=[parser])
parser_cmd2.add_argument('-b', '-bar', type=str, action=bar, required=False, help='bar command')
args = parser.parse_args()
parser = args.subparser_name
print(args)
if args.debug:
logging.basicConfig(level=logging.INFO)
if parser == 'cmd1':
if args.foo:
//do foo stuff
if parser == 'cmd2':
if args.bar:
//do bar stuff
So you can a command like such my_prog.py cmd1 -d -f inp_str. Here's the problem: subparser_name is None. The output of print(args) looks kind of like this
Namespace(debug=True, foo="inp_str", subparser_name=None)
Before I added the global debug argument, subparser_name would be the name of the command I ran, i.e. 'cmd1' or 'cmd2'. Now, it's 'None'. Even with the parents=[parser] addition in the subparser creation. How can I fix this? How do I know which command was called?
Split out the common args to a separate ArgumentParser, which is then used as parent for the sub parsers. Also your foo and bar options were specified using -foo and -bar whi should be --foo and --bar. Also you didn't have default values for these so e.g. when -f/--foo wasn't specified args.foo correctly didn't exist.
This works better:
import argparse
common_args = argparse.ArgumentParser(prog="my_prog", add_help=False)
common_args.add_argument('-d', '--debug', action='store_true', help='debug flag')
parser = argparse.ArgumentParser(prog="my_prog", add_help=True)
subparsers = parser.add_subparsers(dest="subparser_name", help='some help notes')
parser_cmd1 = subparsers.add_parser('cmd1', parents=[common_args])
parser_cmd1.add_argument('-f', '--foo', type=str, default='', required=False, help='foo command')
parser_cmd2 = subparsers.add_parser('cmd2', parents=[common_args])
parser_cmd2.add_argument('-b', '--bar', type=str, default='', required=False, help='bar command')
args = parser.parse_args()
parser = args.subparser_name
print(args)
if args.debug:
logging.basicConfig(level=logging.INFO)
if parser == 'cmd1':
if args.foo:
#//do foo stuff
print( f"foo {args.foo}" )
if parser == 'cmd2':
if args.bar:
#//do bar stuff
print( f"bar {args.bar}" )
run with:
args.py cmd1 -f asd
output:
Namespace(subparser_name='cmd1', debug=False, foo='asd')
foo asd
Update:
If you want to be able to use e.g. args.py -d cmd1 then on the creation of parser, specify parents=[common_args]
parser = argparse.ArgumentParser(prog="my_prog", add_help=True, parents=[common_args])
Next time you ask a question ensure you only post code as a minimal reproducible example - i.e. that can be run without adding anything
The subparser's defaults have priority of any values set by the main parser - default or user input. The main does set the subparser_name to 'cmd1', but the subparser changes it back to the default None.
While not evident in your test case, defining debug at both levels has the same problem. The subparser's default overwrites anything set in the main.
In general, it is not a good idea to use the same dest in the main and subparsers. Flags can be the same, but the dest should be different - at least if you want to see anything set by the main.
And using the main parser as a parent to the sub, is just asking for confusion.

Extending Help in Argparse to Subcommands [duplicate]

My argparse has only 3 flags (store_true) on the top level, everything else is handled through subparsers. When I run myprog.py --help, the output shows a list of all subcommands like normal, {sub1, sub2, sub3, sub4, ...}. So, the default is working great...
I usually can't remember the exact subcommand name I need, and all of its options. So I end up doing 2 help lookups:
myprog.py --help
myprog.py sub1 --help
I do this so often, I decided to cram this into one step. I would rather have my toplevel help output a huge summary, and then I scroll through the list manually. I find it is much faster (for me at least).
I was using a RawDescriptionHelpFormatter, and typing the long help output by hand. But now I have lots of subcommands, and its becoming a pain to manage.
Is there a way to get a verbose help output with just one program call?
If not, how can I iterate the subparsers of my argparse instance, and then retrieve the help output individually from each one (which I will then later glue together)?
Here is a quick outline of my argparse setup. I cleaned/stripped the code a fair bit, so this may not run without a bit of help.
parser = argparse.ArgumentParser(
prog='myprog.py',
formatter_class=argparse.RawDescriptionHelpFormatter,
description=textwrap.dedent(""" You can manually type Help here """) )
parser.add_argument('--debuglog', action='store_true', help='Verbose logging for debug purposes.')
parser.add_argument('--ipyonexit', action='store_true', help='Drop into an embeded Ipython session instead of exiting command.')
subparser = parser.add_subparsers()
### --- Subparser B
parser_b = subparser.add_parser('pdfreport', description="Used to output reports in PDF format.")
parser_b.add_argument('type', type=str, choices=['flatlist', 'nested', 'custom'],
help="The type of PDF report to generate.")
parser_b.add_argument('--of', type=str, default='',
help="Override the path/name of the output file.")
parser_b.add_argument('--pagesize', type=str, choices=['letter', '3x5', '5x7'], default='letter',
help="Override page size in output PDF.")
parser_b.set_defaults(func=cmd_pdf_report)
### ---- Subparser C
parser_c = subparser.add_parser('dbtables', description="Used to perform direct DB import/export using XLS files.")
parser_c.add_argument('action', type=str, choices=['push', 'pull', 'append', 'update'],
help="The action to perform on the Database Tables.")
parser_c.add_argument('tablename', nargs="+",
help="The name(s) of the DB-Table to operate on.")
parser_c.set_defaults(func=cmd_db_tables)
args = parser.parse_args()
args.func(args)
This is a bit tricky, as argparse does not expose a list of defined sub-parsers directly. But it can be done:
import argparse
# create the top-level parser
parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('--foo', action='store_true', help='foo help')
subparsers = parser.add_subparsers(help='sub-command help')
# create the parser for the "a" command
parser_a = subparsers.add_parser('a', help='a help')
parser_a.add_argument('bar', type=int, help='bar help')
# create the parser for the "b" command
parser_b = subparsers.add_parser('b', help='b help')
parser_b.add_argument('--baz', choices='XYZ', help='baz help')
# print main help
print(parser.format_help())
# retrieve subparsers from parser
subparsers_actions = [
action for action in parser._actions
if isinstance(action, argparse._SubParsersAction)]
# there will probably only be one subparser_action,
# but better safe than sorry
for subparsers_action in subparsers_actions:
# get all subparsers and print help
for choice, subparser in subparsers_action.choices.items():
print("Subparser '{}'".format(choice))
print(subparser.format_help())
This example should work for python 2.7 and python 3. The example parser is from Python 2.7 documentation on argparse sub-commands.
The only thing left to do is adding a new argument for the complete help, or replacing the built in -h/--help.
Here is complete soulution with custom help handler (almost all code from #Adaephon answer):
import argparse
class _HelpAction(argparse._HelpAction):
def __call__(self, parser, namespace, values, option_string=None):
parser.print_help()
# retrieve subparsers from parser
subparsers_actions = [
action for action in parser._actions
if isinstance(action, argparse._SubParsersAction)]
# there will probably only be one subparser_action,
# but better save than sorry
for subparsers_action in subparsers_actions:
# get all subparsers and print help
for choice, subparser in subparsers_action.choices.items():
print("Subparser '{}'".format(choice))
print(subparser.format_help())
parser.exit()
# create the top-level parser
parser = argparse.ArgumentParser(prog='PROG', add_help=False) # here we turn off default help action
parser.add_argument('--help', action=_HelpAction, help='help for help if you need some help') # add custom help
parser.add_argument('--foo', action='store_true', help='foo help')
subparsers = parser.add_subparsers(help='sub-command help')
# create the parser for the "a" command
parser_a = subparsers.add_parser('a', help='a help')
parser_a.add_argument('bar', type=int, help='bar help')
# create the parser for the "b" command
parser_b = subparsers.add_parser('b', help='b help')
parser_b.add_argument('--baz', choices='XYZ', help='baz help')
parsed_args = parser.parse_args()
Perhaps an easier approach is to use parser.epilog:
def define_parser():
import argparse
parser = argparse.ArgumentParser(
prog='main',
formatter_class=argparse.RawDescriptionHelpFormatter,
)
commands = parser.add_subparsers(
title="required commands",
help='Select one of:',
)
command_list = commands.add_parser(
'list',
help='List included services',
)
command_ensure = commands.add_parser(
'ensure',
help='Provision included service',
)
command_ensure.add_argument(
"service",
help='Service name',
)
import textwrap
parser.epilog = textwrap.dedent(
f"""\
commands usage:\n
{command_list.format_usage()}
{command_ensure.format_usage()}
"""
)
return parser
parser = define_parser()
parser.print_help()
which results in the following output:
usage: main [-h] {list,ensure} ...
optional arguments:
-h, --help show this help message and exit
required commands:
{list,ensure} Select one of:
list List included services
ensure Provision included service
commands usage:
usage: main list [-h]
usage: main ensure [-h] service
A simpler way to iterate over the subparsers in Adaephon's example is
for subparser in [parser_a, parser_b]:
subparser.format_help()
Python does allow you to access hidden attributes like parser._actions, but that's not encouraged. It is just as easy to build your own list while defining the parser. Same goes for doing special things with the arguments. add_argument and add_subparser return their respective Action and Parser objects for a reason.
If I were making a subclass of ArgumentParser I would feel free to use _actions. But for a one off application, building my own list would be clearer.
An example:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('mainpos')
parser.add_argument('--mainopt')
sp = parser.add_subparsers()
splist = [] # list to collect subparsers
sp1 = sp.add_parser('cmd1')
splist.append(sp1)
sp1.add_argument('--sp1opt')
sp2 = sp.add_parser('cmd2')
splist.append(sp2)
sp2.add_argument('--sp2opt')
# collect and display for helps
helps = []
helps.append(parser.format_help())
for p in splist:
helps.append(p.format_help())
print('\n'.join(helps))
# or to show just the usage
helps = []
helps.append(parser.format_usage())
for p in splist:
helps.append(p.format_usage())
print(''.join(helps))
The combined 'usage' display is:
usage: stack32607706.py [-h] [--mainopt MAINOPT] mainpos {cmd1,cmd2} ...
usage: stack32607706.py mainpos cmd1 [-h] [--sp1opt SP1OPT]
usage: stack32607706.py mainpos cmd2 [-h] [--sp2opt SP2OPT]
The display of the combined helps is long and redundant. It could be edited in various ways, either after formatting, or with special help formatters. But who is going make such choices?
I was also able to print a short help for commands using _choices_actions.
def print_help(parser):
print(parser.description)
print('\ncommands:\n')
# retrieve subparsers from parser
subparsers_actions = [
action for action in parser._actions
if isinstance(action, argparse._SubParsersAction)]
# there will probably only be one subparser_action,
# but better save than sorry
for subparsers_action in subparsers_actions:
# get all subparsers and print help
for choice in subparsers_action._choices_actions:
print(' {:<19} {}'.format(choice.dest, choice.help))
add_subparsers().add_parser() accepts not only a description, which shows up in the help of the subcommand, but also a help= which is used as one-line description in the top-level parsers' help.
The docs have this hidden in the formulation
(A help message for each subparser command, however, can be given by supplying the help= argument to add_parser() as above.)
and even in the sample code around that sentence:
>>> # create the parser for the "b" command
>>> parser_b = subparsers.add_parser('b', help='b help')
>>> parser_b.add_argument('--baz', choices='XYZ', help='baz help')
[...]
usage: PROG [-h] [--foo] {a,b} ...
positional arguments:
{a,b} sub-command help
a a help
b b help
Yes, this is not the full help for everthing, but IMHO covers the basic use case very well and is not easily discoverable.
if __name__ == '__main__':
parser = argparse.ArgumentParser("TOML FILE OVERWRITE SCRIPT")
subparsers = parser.add_subparsers()
parser_set = subparsers.add_parser('set', help='Set Toml')
parser_set.add_argument('set', help='TOMl file edit set action', action='store_true')
parser_set.add_argument('-n', '--name', type=str, help='Service Name', required=True)
parser_set.add_argument('-s', '--section', type=str, help='Toml Section Name', required=True)
parser_set.add_argument('-k', '--key', type=str, help='Toml Key of Section', required=True)
parser_set.add_argument('-v', '--value', help='New Value', required=True)
args = parser.parse_args()
if args.set:
setter = ConfigurationSetter(args.name, args.section, args.key, args.value)
setter.execute()
else:
print("Ops! Something is wrong, type --help or -h")
You can check my code maybe inspires you!

python argparse - How to prevent by using a combination of arguments

In argparse, I want to prevent a particular combination of arguments. Lets see the sample code.
Sample:
import argparse
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--firstname', dest='fn', action='store')
parser.add_argument('--lastname', dest='ln', action='store')
parser.add_argument('--fullname', dest='full', action='store')
args = parser.parse_args()
For eg: --firstname --lastname --fullname
The user can run the code in 2 days.
Way 1:
code.py --firstname myfirst --lastname mylast
Way 2:
code.py --fullname myfullname
Prevent
We should not use the combination fistname, fullname or lastname, fullname. If we use both, then it should not execute.
Can someone help me to fix this?
Not sure that is an argparse specific behavior that is possible. But as long as those items are going to their own variables in the argparse resposes its a simple set of programming to check which ones are set and issue a message and exit.
example (assuming the result of parsing is in argsvalue):
if argsvalue.fullname and (argsvalue.firstname or argsvalue.lastname):
print ("missuse of name options.....")
This assumes the argparse default for the vars is None (then if anything is set in them they will test to true with the logic above...
Like this answer proposes (on a similar question) you can do something like the following by using subparsers for both cases:
# create the top-level parser
parser = argparse.ArgumentParser(add_help=false)
subparsers = parser.add_subparsers(help='help for subcommands')
# create the parser for the "full_name" command
full_name = subparsers.add_parser('full_name', help='full_name help')
full_name.add_argument('--fullname', dest='full', action='store')
# create the parser for the "separate_names" command
separate_names = subparsers.add_parser('separate_names', help='separate_names help')
separate_names.add_argument('--firstname', dest='fn', action='store')
separate_names.add_argument('--lastname', dest='ln', action='store')
args = parser.parse_args()
You can improve it even further by requiring both the first and last name of the user as it generally makes sense.
You can split your arguments into separate commands and use subparsers, for example:
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
parser_clean = subparsers.add_parser('clean', description='clean build folder')
parser_clean.add_argument('--all', help='clean build folder and logs', action='store_true')
parser_deploy = subparsers.add_parser('deploy')
parser_deploy.add_argument('object')
parser_deploy.add_argument('--nc', help='no cleanup', action='store_true')
args = parser.parse_args()

How to set optional arguments to positional arguments in Python's Argparse?

I have the following code:
# Get parsed arguments
args = argparse.ArgumentParser(description=Messages().Get(112))
# Get the arguments for sinit
args.add_argument('init', help=Messages().Get(100), action="store_true")
args.add_argument('--url', default=None, help=Messages().Get(101))
# Get the arguments for schema import
args.add_argument('schema-import', help=Messages().Get(104), action="store_true")
args.add_argument('--file', default=None, help=Messages().Get(104))
The --url argument should only be used with init. For example: script.py schema-import --url should not be accepted but script.py schema-import --file should.
How to set arguments as child arguments?
As mentioned there might be a way to do this with argparse, I'm not sure, but in any event I find it more transparent to explicitly handle argument dependencies in application logic. This should achieve what I think you want:
import argparse
import sys
args = argparse.ArgumentParser(description="please only use the '--url' argument if you also use the 'init' argument")
# Going to use aliases here it's more conventional. So user can use, eg,
# -i or --init for the first argument.
args.add_argument('-i', '--init', help='init help', action="store_true")
args.add_argument('-u', '--url', default=None, help='init help')
args.add_argument('-s', '--schema-import', help='schema-import help', action="store_true")
args.add_argument('-f', '--file', help='file help')
def main():
arguments = args.parse_args()
if arguments.url and not arguments.init:
# You can log an output or raise an exception if you want
# But most likely a print statment is most appropriate
# Since you are interacting with the CLI.
print("You can only use the URL option with init. Exiting")
sys.exit(0)
print("gaurd clauses passed. Here is my code...")
...
if __name__ == "__main__":
main()
Test results (my file called temp.py):
$python temp.py -u https://www.google.com
You can only use the URL option with init. Exiting
$
$python temp.py -i -u https://www.google.com
Gaurd clauses passed. Here is my code...
Why bother with doing all the logic when you can let argparse do all the work for you?
Simply use Sub-commands to define different "branches" of execution:
args = argparse.ArgumentParser(description=Messages().Get(112))
subparsers = args.add_subparsers()
parser_init = subparsers.add_parser('init', help=Messages().Get(100))
parser_init.add_argument('--url', default=None, help=Messages().Get(101))
parser_schema = subparsers.add_parser('schema-import', help=Messages().Get(104))
parser_schema.add_argument('--file', default=None, help=Messages().Get(104))
And this will give you what you want without any logic added:
>>> print(args.parse_args(['schema-import', '--url', "some.url"]))
usage: args.py [-h] {init,schema-import} ...
args.py: error: unrecognized arguments: --url some.url
>>> print(args.parse_args(['schema-import', '--file', "some.file"]))
Namespace(file='some.file')

Arguments that are dependent on other arguments with Argparse

I want to accomplish something like this:
-LoadFiles
-SourceFile "" -DestPath ""
-SourceFolder "" -DestPath ""
-GenericOperation
-SpecificOperation -Arga "" -Argb ""
-OtherOperation -Argc "" -Argb "" -Argc ""
A user should be able to run things like:
-LoadFiles -SourceFile "somePath" -DestPath "somePath"
or
-LoadFiles -SourceFolder "somePath" -DestPath "somePath"
Basically, if you have -LoadFiles, you are required to have either -SourceFile or -SourceFolder after. If you have -SourceFile, you are required to have -DestPath, etc.
Is this chain of required arguments for other arguments possible? If not, can I at least do something like, if you have -SourceFile, you MUST have -DestPath?
You can use subparsers in argparse
import argparse
parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('--foo', required=True, help='foo help')
subparsers = parser.add_subparsers(help='sub-command help')
# create the parser for the "bar" command
parser_a = subparsers.add_parser('bar', help='a help')
parser_a.add_argument('bar', type=int, help='bar help')
print(parser.parse_args())
After you call parse_args on the ArgumentParser instance you've created, it'll give you a Namespace object. Simply check that if one of the arguments is present then the other one has to be there too. Like:
args = parser.parse_args()
if ('LoadFiles' in vars(args) and
'SourceFolder' not in vars(args) and
'SourceFile' not in vars(args)):
parser.error('The -LoadFiles argument requires the -SourceFolder or -SourceFile')
There are some argparse alternatives which you can easily manage cases like what you mentioned.
packages like:
click or
docopt.
If we want to get around the manual implementation of chain arguments in argparse, check out the Commands and Groups in click for instance.
Here is a sample that in case you specify --makeDependency, forces you to specify --dependency with a value as well.
This is not done by argparse alone, but also by by the program that later on validates what the user specified.
#!/usr/bin/env python
import argparse
import sys
parser = argparse.ArgumentParser()
parser.add_argument('--makeDependency', help='create dependency on --dependency', action='store_true')
parser.add_argument('--dependency', help='dependency example')
args = parser.parse_args()
if args.makeDependency and not args.dependency:
print "error on dependency"
sys.exit(1)
print "ok!"

Categories