Adding sub-subcommands to a command - python

I have a python script that takes input from CLI, parses it and runs the appropriate function:
command subcommand subsubcommand1 --arg1 <value1>
When user doesn't provide subsubcommand I want the script to return an error, saying that at least one of the supported subcommand must be provided, instead it returns:
Namespace object has no attribute func
This is the code:
parser = argparse.ArgumentParser(description='The highlevel command')
parser.add_argument("-v", help="some help text", action='store_true', default=False)
subparsers = parser.add_subparsers(dest='action')
subparsers.required = True
subcommand_parser = subparsers.add_parser('subcommand', help='some help text for the subcommand')
subsub_subparser = subcommand_parser.add_subparsers()
subsubparser1 = subsub_subparser.add_parser('subsubcommand1', help='some help text for the subsubcommand1')
subsubparser1.set_defaults(action='subsubcommand_action', func=mylib.subsub1)
subsubparser1.add_argument('--arg1', required=True, help='arg1')
subsubparser1.add_argument('--arg2', required=False, default='hello', help='arg2')
How can I fix the current error and make the new error show up?

Correcting the omissions mentioned in my comments:
import argparse
parser = argparse.ArgumentParser(description='The highlevel command')
parser.add_argument("-v", help="some help text", action='store_true', default=False)
subparsers = parser.add_subparsers(dest='action')
subparsers.required = True
subcommand_parser = subparsers.add_parser('subcommand', help='some help text for the subcommand')
subsub_subparser = subcommand_parser.add_subparsers(dest='subaction') # EDIT
subsub_subparser.required = True # EDIT
subsubparser1 = subsub_subparser.add_parser('subsubcommand1', help='some help text for the subsubcommand1')
subsubparser1.set_defaults(func="mylib.subsub1") # EDIT
subsubparser1.add_argument('--arg1', required=True, help='arg1')
subsubparser1.add_argument('--arg2', required=False, default='hello', help='arg2')
subsubparser2 = subsub_subparser.add_parser('subsubcommand2') # EDIT
args = parser.parse_args()
print(args) # EDIT
if hasattr(args, 'func'):
print('func: ',args.func)
else:
print('func not defined')
sample runs:
2020:~/mypy$ python3 stack56435945.py
usage: stack56435945.py [-h] [-v] {subcommand} ...
stack56435945.py: error: the following arguments are required: action
2021:~/mypy$ python3 stack56435945.py subcommand
usage: stack56435945.py subcommand [-h] {subsubcommand1,subsubcommand2} ...
stack56435945.py subcommand: error: the following arguments are required: subaction
2021:~/mypy$ python3 stack56435945.py subcommand subsubcommand1 --arg1 foobar
Namespace(action='subcommand', arg1='foobar', arg2='hello', func='mylib.subsub1', subaction='subsubcommand1', v=False)
func: mylib.subsub1
2022:~/mypy$ python3 stack56435945.py subcommand subsubcommand2
Namespace(action='subcommand', subaction='subsubcommand2', v=False)
func not defined

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.

Argparse: exception for option required=True

I use the following code to parse argument to my script (simplified version):
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("-l", "--library", required=True)
ap.add_argument("--csv2fasta", required=False)
args = vars(ap.parse_args())
For every way the script can be run, the -l/--library flag should be required (required=True), but is there a way that it can use the setting required=False when you only use the --csv2fasta flag?
You have to write your test after parsing arguments, here's what I do for such cases:
def parse_args():
ap = argparse.ArgumentParser()
ap.add_argument("-l", "--library")
ap.add_argument("--csv2fasta")
args = ap.parse_args()
if not args.library and not args.csv2fasta:
ap.error("--library is required unless you provide --csv2fasta argument")
return args
$ python3 test-args.py
usage: test-args.py [-h] [-l LIBRARY] [--csv2fasta CSV2FASTA]
test-args.py: error: --library is required unless you provide --csv2fasta argument
$ python3 test-args.py --csv2fasta value

Argparse using subcommands/subparsers -- AttributeError: 'Namespace' object has no attribute

I need to use a python script having the following usages:
script.py ( commands ) ( options )
My problem is how i add arguments for "commands" and "options"?
What i did now is this:
parser = argparse.ArgumentParser()
parser._optionals.title = "Options"
parser.add_argument('-help','--help', action="store_true", dest="help", help='help')
subparsers = parser.add_subparsers(help="All available commands", title="Commands")
parser_start = subparsers.add_parser('start', help='Starts the script', add_help=False)
parser_start._optionals.title = "Options"
parser_start.add_argument('--help', action="store_true", dest="help_start")
parser_start.add_argument('-f', type=str, dest="file", help='simulation file to start')
parser_ls = subparsers.add_parser('ls', help='Lists running simulations', add_help=False)
parser_ls._optionals.title = "Options"
parser_ls.add_argument('--help', action="store_true", dest="help_ls")
parser_ls.add_argument('--all', action="store_true", help='Display all simulations')
parser_stop = subparsers.add_parser('stop', help='Stops simulation', add_help=False)
parser_stop._optionals.title = "Options"
parser_stop.add_argument('--help', action="store_true", dest="help_down")
parser_stop.add_argument('--sim-name', type=str, dest="sim_name")
args = parser.parse_args()
If i try to access args.help_up i receive: AttributeError: 'Namespace' object has no attribute 'help_start'
How do i pass the parser_up, parser_stop and parser_ls to the parse_args?
And how do i access them afterwards?
Objective is to have custom help messages ( which i have atm that is why i disabled the help ) and to run the script like this:
script.py start -f (name of file)
script.py stop --sim-name (name of simulation)
EDIT:
If i add args2 = parser_start.parse_args() i am able to get a read on args2.help_start, but i am not able to find any of the start, ls or down arguments!
I've made a few changes to your code; hopefully it will clarify what's going on:
import argparse
parser = argparse.ArgumentParser()
parser._optionals.title = "Options"
#parser.add_argument('-help','--help', action="store_true", dest="help", help='help')
# conflicts with original help
subparsers = parser.add_subparsers(help="All available commands", title="Commands",
dest='cmd') # NEW
parser_start = subparsers.add_parser('start', help='Starts the script', add_help=False)
parser_start._optionals.title = "Options"
parser_start.add_argument('--help', action="store_true", dest="help_start")
parser_start.add_argument('-f', type=str, dest="file", help='simulation file to start')
parser_ls = subparsers.add_parser('ls', help='Lists running simulations', add_help=False)
parser_ls._optionals.title = "Options"
parser_ls.add_argument('--help', action="store_true", dest="help_ls")
parser_ls.add_argument('--all', action="store_true", help='Display all simulations')
parser_stop = subparsers.add_parser('stop', help='Stops simulation', add_help=False)
parser_stop._optionals.title = "Options"
parser_stop.add_argument('--help', action="store_true", dest="help_down")
parser_stop.add_argument('--sim-name', type=str, dest="sim_name")
args = parser.parse_args()
print(args) # NEW
and sample runs:
0939:~/mypy$ python3 stack62716530.py
Namespace(cmd=None)
0939:~/mypy$ python3 stack62716530.py --help
usage: stack62716530.py [-h] {start,ls,stop} ...
Options:
-h, --help show this help message and exit
Commands:
{start,ls,stop} All available commands
start Starts the script
ls Lists running simulations
stop Stops simulation
0939:~/mypy$ python3 stack62716530.py start
Namespace(cmd='start', file=None, help_start=False)
0939:~/mypy$ python3 stack62716530.py start --help
Namespace(cmd='start', file=None, help_start=True)
and if I add:
if getattr(args, 'help_start',False):
parser_start.print_help()
I get
0940:~/mypy$ python3 stack62716530.py start --help
Namespace(cmd='start', file=None, help_start=True)
usage: stack62716530.py start [--help] [-f FILE]
Options:
--help
-f FILE simulation file to start
The key is that help_start is an attribute only if the start subparser is invoked.
In first read(s) of your code I missed the dest='help_start' parameters. Thus I couldn't tell why you expected to see such an attribute in args.

python's argument parser printing the argument name in upper case

I am trying to write usage/help for my python script using the argparse library.
This is my sample code:
import argparse
parser = argparse.ArgumentParser(
description='My description')
parser.add_argument(
"-r", "--remote",
help="help message")
parser.print_help()
Output:
usage: [-h] [-r REMOTE]
My description
optional arguments:
-h, --help show this help message and exit
-r REMOTE, --remote REMOTE
help message
I have no idea why it is printing REMOTE after the -r and --remote switches in the above output.
Can anyone tell me what am I doing wrong here or what should I do to get rid of it?
You are looking at the metavar; it is autogenerated from the option string to form a placeholder. It tells the user that it is there that they need to fill in a value.
You can set an explicit metavar value with the metavar keyword argument:
When ArgumentParser generates help messages, it needs some way to refer to each expected argument. By default, ArgumentParser objects use the dest value as the “name” of each object. By default, for positional argument actions, the dest value is used directly, and for optional argument actions, the dest value is uppercased.
You see it because your argument takes a value; if you expected it to be a toggle, use action='store_true'; in that case the option defaults to False unless the user specifies the switch.
Demo of the latter:
>>> import argparse
>>> parser = argparse.ArgumentParser(
... description='My description')
>>> parser.add_argument("-r", "--remote", action='store_true', help="help message")
_StoreTrueAction(option_strings=['-r', '--remote'], dest='remote', nargs=0, const=True, default=False, type=None, choices=None, help='help message', metavar=None)
>>> parser.print_help()
usage: [-h] [-r]
My description
optional arguments:
-h, --help show this help message and exit
-r, --remote help message
>>> opts = parser.parse_args([])
>>> opts.remote
False
>>> opts = parser.parse_args(['-r'])
>>> opts.remote
True
You are missing action.
import argparse
parser = argparse.ArgumentParser(
description='My description')
parser.add_argument(
"-r", "--remote", action="store_true", # add action
help="help message")
parser.print_help()
usage: -c [-h] [-r]
My description
optional arguments:
-h, --help show this help message and exit
-r, --remote help message

Remove long sub-command help output?

I'm using argparse to produce my CLI and using the add_subparsers function. This works exception the --help output is really ugly. It lists all the commands in the overall syntax. For example:
usage: redid [-h] [--config CONFIG] [--verbose] [--show-curl] [--save [SAVE]]
{setup,show-config,check-auth,version,get-profile,put-profile,delete-profile,get-profile-signature,list-profiles,list-resources,ls-resources,get-resource-record,delete-resource,get-resource,upload-resource,get-resource-url}
...
I'd much prefer to have a more traditional and clean output similar to:
usage: redid [OPTIONS...] Command ...
How can I do this?
Try adding the metavar argument to your subparser definition and give it no value:
parser.add_subparsers(title="the_commands", metavar="")
From the documentation:
Description of parameters:
....
metavar - string presenting available sub-commands in help; by default it is None and presents sub-commands in form {cmd1, cmd2, ..}
Here's an example, I'm not sure how you've set up your sub-parsers but:
parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('--foo', action='store_true', help='foo help')
subparsers = parser.add_subparsers(help="sub-command help", metavar="sub command")
parser_a = subparsers.add_parser('a', help='a help')
parser_a.add_argument('bar', type=int, help='bar help')
parser_b = subparsers.add_parser('b', help='b help')
parser_b.add_argument('--baz', choices='XYZ', help='baz help')
>>> parser.print_help()
usage: PROG [-h] [--foo] sub command ...
positional arguments:
sub command sub-command help
a a help
b b help
optional arguments:
-h, --help show this help message and exit
--foo foo help

Categories