Optional arguments across all subparsers - python-3.x

I'm currently testing an argparse usage, but it's not working as expected. I have a couple of subparsers and optional arguments, being called the following way:
python3 myprogram.py positional argument --optional something
# Outcome
Namespace(optional='something')
The program works as expected if the optional is the last, but if it is in any other order, it is discarded.
python3 myprogram.py positional --optional argument
python3 myprogram.py --optional positional argument
# Outcome
Namespace(optional=None)
By looking at the argparse documentation I wasn't able to find a way to make the optional argument global.
I'm creating the the positional arguments for each positional in a for loop, which doesn't seem to be the best way. Because otherwise, it would add the optional arguments only to the last subparser.
import argparse
class Parsing(object):
def __init__(self):
parser = argparse.ArgumentParser(prog='python3 myprogram.py',
formatter_class=argparse.RawDescriptionHelpFormatter,
description='some description')
self.subparser = parser.add_subparsers(title='Positional', help='help description')
for sub in self.Generate(): # Method with a bunch of subparsers
self.Subparser(sub)
def Subparser(self, parsers):
for each in sorted(parsers):
positional = subparser.add_parser(each)
self.Optional(positional) # Method with some optional arguments for each of the second subparsers
self.Optional(parser) # Adding the optional arguments to the first subparser
def Optional(self, parser):
# ... Optional arguments
def Generate(self):
# ... Subparsers
I might be missing some code in the example above, tried to simplify the best I could and hope it to be perceptible.
Question: Is there a way to make the optional arguments across all subparsers?

Your description and code is hard to follow, but I've concluded that your problem lies with how defaults are handled when the main and subparsers share an argument dest.
I condensed your code a bit so I could make a test run:
import argparse
class Parsing(object):
def __init__(self):
self.parser = argparse.ArgumentParser(prog='prog',
description='some description')
self.subparser = self.parser.add_subparsers(dest='cmd', title='Cmds', help='help description')
self.make_subparsers(['cmd1','cmd2'])
def make_subparsers(self, parsers):
for each in parsers:
subp = self.subparser.add_parser(each)
self.optional(subp, default='sub')
self.optional(self.parser, default='main')
def optional(self, parser, default=None):
parser.add_argument('--foo', default=default)
args = Parsing().parser.parse_args()
print(args)
I get for 2 runs
1315:~/mypy$ python3.5 stack41431025.py cmd1 --foo 1
Namespace(cmd='cmd1', foo='1')
1316:~/mypy$ python3.5 stack41431025.py --foo 1 cmd1
Namespace(cmd='cmd1', foo='sub')
In the first, foo is set by the strings parsed by the cmd1 subparser.
In the second, foo gets the default value set by the subparser. The main parser parsed --foo, but its value was over written by the subparser.
There has been some discussion of this in bug/issues. http://bugs.python.org/issue9351 changed handling so that the subparser default has priority over main parser values. I think there are problems with that patch, but it's been in effect for a couple of years.
You retain more control if they are given different dest.
def make_subparsers(self, parsers):
for each in parsers:
subp = self.subparser.add_parser(each)
self.optional(subp, default='sub')
self.optional(self.parser, default='main', dest='main_foo')
def optional(self, parser, default=None, dest=None):
parser.add_argument('--foo', default=default, dest=dest)
1325:~/mypy$ python3.5 stack41431025.py --foo 1 cmd1
Namespace(cmd='cmd1', foo='sub', main_foo='1')
1325:~/mypy$ python3.5 stack41431025.py cmd1
Namespace(cmd='cmd1', foo='sub', main_foo='main')
1325:~/mypy$ python3.5 stack41431025.py --foo 1 cmd1 --foo 2
Namespace(cmd='cmd1', foo='2', main_foo='1')
====================
(earlier answer)
I'll try to sketch the possible combinations of arguments
parser = argparse.ArgumentParser()
parser.add_argument('mainpos', help='positional for main')
parser.add_argument('--mainopt', help='optional defined for main')
sp = parser.add_subparser(dest='cmd')
p1 = sp.add_parser('cmd1')
p1.add_argument('subpos', help='postional for sub')
p1.add_argument('--subopt', help='optional defined for sub')
A composite usage would look like:
python prog.py foo [--mainopt bar] cmd1 sfoo [--subopt baz]
The respective positionals have to be given in the right order. The subparser cmd is effectively a positional for the main.
The optional defined for main has to occur before the subparser name. The optional defined for the subparser has to occur after. They could have the same flag or dest, but they have to be defined separately. And if they have the same dest, there could be a conflict over values, especially defaults.
parser.parse_args() starts matching the input strings with its arguments. If it sees --mainopt is parses that optional argument. Otherwise it expects two postionals. The 2nd has to be one of the subparser names.
Once it gets a subparser name it passes the remaining strings to that subparser. The subparser handles the rest, and puts the values in the main namespace. And the first thing the subparser does is set its defaults. Whether that action overwrites values set by the main parser or not depends on just how the namespace is passed between the two.
================
Parsing is driven by the order of arguments in the command line. It tries to allow flagged arguments in any order. But once parsing is passed to the subparser, the main parser does not get another go at parsing. It just does a few clean up tasks.
But if I use parse_known_args, I can collect the strings that neither parser handled, and take another stab a parsing them.
parser1 = argparse.ArgumentParser()
parser1.add_argument('--foo')
sp = parser1.add_subparsers(dest='cmd')
sp1 = sp.add_parser('cmd1')
args, extra = parser1.parse_known_args()
parser2 = argparse.ArgumentParser()
parser2.add_argument('--foo')
if extra:
args = parser2.parse_args(extra)
print(args)
runs
1815:~/mypy$ python stack41431025.py --foo 1 cmd1
Namespace(cmd='cmd1', foo='1')
1815:~/mypy$ python stack41431025.py cmd1 --foo 2
Namespace(foo='2')
1815:~/mypy$ python stack41431025.py --foo 1 cmd1 --foo 3
Namespace(foo='3')
I haven't tested this idea in anything more complex, so there might be some interactions that I haven't thought of. But this is the closest I can come to a flagged argument that can occur anywhere, and not be subject to conflicting default problems.

Related

Python3 argparse nargs="+" get number of arguments

I'm now googling for quite a while and I just don't find any solution to my problem.
I am using argparse to parse some command line arguments. I want to be able to parse an arbitrary number of arguments > 1 (therefore nargs="+") and I want to know afterwards how many arguments I have parsed. The arguments are all strings. But I get a problem when I just have one argument, because then the length of the list is the number of characters of the word and not 1 as in 1 argument. And I want to know how many arguments were parsed. Does anyone know how I could solve this problem?
examples:
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument("--test", type=str, nargs="+", required=False, default="hi")
args = parser.parse_args()
test = args.test
print(test)
print(len(test))
So with python3 example.py --test hello hallo hey the output is:
['hello', 'hallo', 'hey']
3
But with python3 example.py --test servus the output is:
servus
6
What I already know is that I could do print([test]) but I only want to do that if I have 1 argument. Because if I have more than one arguments and use print([test]) I get a double array... So I just want to know the number of parsed arguments for "test".
I cannot imagine that I am the only one with such a problem, but I could not find anything in the internet. Is there a quick and clean solution?
You left off the test=args.test line.
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument("--test", type=str, nargs="+", required=False, default="hi")
args = parser.parse_args()
print(args)
print(len(args.test))
test cases
0856:~/mypy$ python3 stack68020846.py --test one two three
Namespace(test=['one', 'two', 'three'])
3
0856:~/mypy$ python3 stack68020846.py --test one
Namespace(test=['one'])
1
0856:~/mypy$ python3 stack68020846.py
Namespace(test='hi')
2
change the default to default=[]
0856:~/mypy$ python3 stack68020846.py
Namespace(test=[])
0
Theres probably a better solution, but try this:
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument("--test", type=str, nargs="+", required=False, default="hi")
args = parser.parse_args()
print("Number of arguments:", len(args._get_kwargs()[-1][-1]))
By the way, i figured this out by checking the content of args with print(dir(args))

python argparse if argument selected then another argument required =True

Is there a way to make an argument required to be true if another specific argument choice is present otherwise argument required is false?
For example the following code if argument -act choice select is 'copy' then the argument dp required is true otherwise required is false:
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("-act", "--act", required=True, choices=['tidy','copy'],type=str.lower,
help="Action Type")
ap.add_argument("-sp", "--sp", required=True,
help="Source Path")
args = vars(ap.parse_args())
if args["act"] == 'copy':
ap.add_argument("-dp", "--dp", required=True,
help="Destination Path")
else:
ap.add_argument("-dp", "--dp", required=False,
help="Destination Path")
args = vars(ap.parse_args())
### Tidy Function
def tidy():
print("tidy Function")
### Copy Function
def copy():
print("copy Function")
### Call Functions
if args["act"] == 'tidy':
tidy()
if args["act"] == 'copy':
copy()
I am currently getting an error unrecognized arguments: -dp with the above code.
The expected result would be to move on to call function. Thanks
I would use ArgumentParser.add_subparsers to define the action type {tidy, copy} and give the command specific arguments. Using a base parser with parents allows you to define arguments that are shared by both (or all) your sub-commands.
import argparse
parser = argparse.ArgumentParser(
prog='PROG',
epilog="See '<command> --help' to read about a specific sub-command."
)
base_parser = argparse.ArgumentParser(add_help=False)
base_parser.add_argument("--sp", required=True, help="source")
subparsers = parser.add_subparsers(dest='act', help='Sub-commands')
A_parser = subparsers.add_parser('copy', help='copy command', parents=[base_parser])
A_parser.add_argument('--dp', required=True, help="dest, required")
B_parser = subparsers.add_parser('tidy', help='tidy command', parents=[base_parser])
B_parser.add_argument('--dp', required=False, help="dest, optional")
args = parser.parse_args()
if args.act == 'copy':
pass
elif args.act == 'tidy':
pass
print(args)
Which produces the following help pages, note that instead of needing to use -act the command is given as a positional parameter.
~ python args.py -h
usage: PROG [-h] {tidy,copy} ...
positional arguments:
{tidy,copy} Sub-commands
tidy tidy command
copy copy command
optional arguments:
-h, --help show this help message and exit
See '<command> --help' to read about a specific sub-command.
~ python args.py copy -h
usage: PROG copy [-h] --sp SP [--dp DP]
optional arguments:
-h, --help show this help message and exit
--sp SP source
--dp DP dest, optional
~ python args.py tidy -h
usage: PROG tidy [-h] --sp SP --dp DP
optional arguments:
-h, --help show this help message and exit
--sp SP source
--dp DP dest, required
ap.add_argument("-dp", "--dp", help="Destination Path")
args = parser.parse_args()
if args.copy is in ['copy']:
if args.dp is None:
parser.error('With copy, a dp value is required')
Since a user can't set a value to None, is None is a good test for arguments that haven't been used.
The parser has a list of defined arguments, parser._actions. Each has a required attribute. During parsing it maintains a set of seen-actions. At the end of parsing it just checks this set against the actions for which required is True, and issues the error message if there are any.
I would argue that testing after parsing is simpler than trying to set the required parameter before hand.
An alternative is to provide dp with a reasonable default. Then you won't care whether the user provides a value or not.
I can imagine defining a custom Action class for copy that would set the required attribute of dp, but overall that would be more work.

Python argparse with possibly empty string value

I would like to use argparse to pass some values throughout my main function. When calling the python file, I would always like to include the flag for the argument, while either including or excluding its string argument. This is because some external code, where the python file is being called, becomes a lot simpler if this would be possible.
When adding the arguments by calling parser.add_argument, I've tried setting the default value to default=None and also setting it to default=''. I can't make this work on my own it seems..
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-p', '--projects_to_build', default='')
args = parser.parse_args()
This call works fine:
py .\python_file.py -p proj_1,proj_2,proj_3
This call does not:
py .\python_file.py -p
python_file.py: error: argument -p/--projects_to_build: expected one argument
You need to pass a nargs value of '?' with const=''
parser.add_argument('-p', '--projects_to_build', nargs='?', const='')
You should also consider adding required=True so you don't have to pass default='' as well.

Is it possible to inherit required options in argparse subparsers?

I am trying to write a command line application that has several modes in which it can run (similar to git having clone, pull, etc.). Each of my subcommands have their own options, but I also wanted them to share a set of required options, so I tried using a parent parser to implement this. However, it seems that inheriting a required option is causing the subparser to keep asking for it. Here is an example recreating the behavior:
import argparse
parent_parser = argparse.ArgumentParser(description="The parent parser")
parent_parser.add_argument("-p", type=int, required=True,
help="set the parent required parameter")
subparsers = parent_parser.add_subparsers(title="actions", required=True, dest='command')
parser_command1 = subparsers.add_parser("command1", parents=[parent_parser],
add_help=False,
description="The command1 parser",
help="Do command1")
parser_command1.add_argument("--option1", help="Run with option1")
parser_command2 = subparsers.add_parser("command2", parents=[parent_parser],
add_help=False,
description="The command2 parser",
help="Do command2")
args = parent_parser.parse_args()
So now if I run python test.py I get:
usage: test.py [-h] -p P {command1,command2} ...
test.py: error: the following arguments are required: -p, command
Ok, so far so good. Then if I try to specify just the -p option with python test.py -p 3 I get:
usage: test.py [-h] -p P {command1,command2} ...
test.py: error: the following arguments are required: command
But then if I run python test.py -p 3 command1
I get:
usage: test.py command1 [-h] -p P [--option1 OPTION1] {command1,command2} ...
test.py command1: error: the following arguments are required: -p, command
If I add another -p 3 it still asks to specify command again, and then if I add that again it asks for another -p 3 etc.
If I don't make the -p option required the problem is fixed, but is there a way to share required options among multiple subparsers without just copy pasting them within each subparser? Or am I going about this entirely the wrong way?
Separate the main parser and parent parser functionality:
import argparse
parent_parser = argparse.ArgumentParser(description="The parent parser", add_help=False)
parent_parser.add_argument("-p", type=int, required=True,
help="set the parent required parameter")
main_parser = argparse.ArgumentParser()
subparsers = main_parser.add_subparsers(title="actions", required=True, dest='command')
parser_command1 = subparsers.add_parser("command1", parents=[parent_parser],
description="The command1 parser",
help="Do command1")
parser_command1.add_argument("--option1", help="Run with option1")
parser_command2 = subparsers.add_parser("command2", parents=[parent_parser],
description="The command2 parser",
help="Do command2")
args = main_parser.parse_args()
The main parser and subparser both write to the same args namespace. If both define a 'p' argument, the subparser's value (or default) will overwrite any value set by the main. So it's possible to use the same 'dest' in both, but it is generally not a good idea. And each parser has to meet its own 'required' specifications. There's no 'sharing'.
The parents mechanism copies arguments from the parent to the child. So it saves typing or copy-n-paste, but little else. It's most useful when the parent is defined elsewhere and imported. Actually it copies by reference, which sometimes raises problems. In general it isn't a good idea to run both the parent and child. Use a 'dummy' parent.

How do I know if add_subparsers() has already been called on a parser

I'd like to create a function that will add a subcommand to a parser.
def add_subparser(parser, command):
sub_parsers = parser.add_subparsers('more commands')
new_parser = sub_parsers.add_parser('free')
return new_parser
It seems to me that the first line needs to check whether parser already has subparsers. What is a good way to do that check?
(Side note: A nice future feature would be get_subparsers that returns a singleton.)
Look at the argparse.py code. The add_subparsers method starts with:
def add_subparsers(self, **kwargs):
if self._subparsers is not None:
self.error(_('cannot have multiple subparser arguments'))
and a bit later sets self._subparsers to a new value.
But if you don't want to look at parser._subparsers you could just wrap the new add_subparses command in a try/except block.
add_subparsers creates a positional Action with a special subparser subclass. That's what we normally assign to a variable, and use in the next lines. (as a side note, add_argument also returns an Action subclass object, the action that was just created).
It's instructive to set up a parser in an interactive session, and look at the objects that each command returns. Most have a basic str method displaying some of the attributes. As with any Python class object, you can explore the attributes in detail, even change some of them.
In [1]: import argparse
In [2]: p = argparse.ArgumentParser()
In [3]: sp = p.add_subparsers(dest='cmd')
_actions is a list of all Action class objects that were created. Here there are two, the default help and the newly created subparser one.
In [4]: p._actions
Out[4]:
[_HelpAction(option_strings=['-h', '--help'], dest='help', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help='show this help message and exit', metavar=None),
_SubParsersAction(option_strings=[], dest='cmd', nargs='A...', const=None, default=None, type=None, choices=OrderedDict(), help=None, metavar=None)]
In [5]: type(p._actions[1])
Out[5]: argparse._SubParsersAction
The _subparsers attribute is now set to an ArgumentGroup, in this case the first default one, the 'positionals'.
In [6]: p._subparsers
Out[6]: <argparse._ArgumentGroup at 0x7f3d8ede31d0>
In [7]: p._action_groups
Out[7]:
[<argparse._ArgumentGroup at 0x7f3d8ede31d0>,
<argparse._ArgumentGroup at 0x7f3d8ede3cf8>]
In [9]: p.print_help()
usage: ipython3 [-h] {} ...
positional arguments:
{} # the subparsers argument
optional arguments:
-h, --help show this help message and exit
And the error caused by trying to add another subparsers (caught in this case by ipython):
In [10]: sp = p.add_subparsers(dest='cmd')
usage: ipython3 [-h] {} ...
ipython3: error: cannot have multiple subparser arguments
An exception has occurred, use %tb to see the full traceback.
SystemExit: 2
/usr/local/lib/python3.6/dist-packages/IPython/core/interactiveshell.py:3304: UserWarning: To exit: use 'exit', 'quit', or Ctrl-D.
warn("To exit: use 'exit', 'quit', or Ctrl-D.", stacklevel=1)

Resources