How to accept positional argument in main parser as well as subparser? - python

I am making a CLI that will accept Fortran code file and some functions in the following manner:-
prog -m module_name example.f only: add sub
How can I accept values after only: as separate positional arguments?
Currently argparse is parsing all arguments after module_name as Fortran files. My code is as follows.
parser = argparse.ArgumentParser(prog="prog", help = "Help text)
parser.add_argument(
"Fortran Files",
metavar="<fortran files>",
action="extend", # List storage
nargs="*",
type=check_fortran, # Returns a pathlib.Path
help="""Paths to fortranfiles that will be scanned for
<fortran functions>.""",
)
parser.add_argument(
"Keep functions",
metavar="only:",
action="extend",
type=str,
nargs="*",
help="Use only fortran functions that follow.",
)
parser.add_argument(
"-m",
"--module",
metavar="<modulename>",
type=str,
nargs=1,
help="""Name of the module""",
)

I don't see any use of subparsers.
Use of phases like "Keep functions" as the dest is awkward and unnecessary when defining positionals. They can't be used as
args."Keep functions"
Simple names like "keep" can be used as args.keep.
action="extend"
has no value for positionals. A positional cannot be reused. Stick with the default "store".
Using two "nargs='*'" positionals sequentially doesn't work. The first gets all the strings, leaving none for the second. They have to be separated by an optional like your '-m'.
I'd suggest using
parser.add_argument('--only', nargs='+', ....)
to specify the functions. Then args.only will be list of function names.

Related

How to constrain that the Python script can only be invoked with one of the optional arguments at a time

My goal is that no optional arguments can be used simultaneously, this means the script can be invoked with one of the options at a time only, here is my code in Python:
parser = argparse.ArgumentParser()
parser.add_argument("-a", help="List out all the hostnames", action='store_true')
parser.add_argument("-v", help="List out student information", action='store_true')
parser.add_argument("-d", help="List out all the qualified domain name")
parser.add_argument("-c", help="List out all the qualified IPV4", choices=['A', 'B', 'C'])
parser.add_argument("filename", help="dataset name")
args = parser.parse_args()
And also, how to create a customized output if the optional arguments are given beyond the choices (-a, -v,-d, -c) instead of using args = parser.parse_args() to show the default error
Thank you guys, I really appreciate any help!
When this sort of behaviour is described in English, the expression "mutually exclusive" tends to pop up, and a quick look at the docs confirms that Argparse has an add_mutually_exclusive_group method that looks useful.

Argparse: mixing parent parser with subparsers

I want to write a simple tool that takes an arbitrary number of input files and performs one operation on each of them. The syntax is stupidly simple:
mytool operation input1 input2 ... inputN
Some of these operations may require an extra argument
mytool operation op_argument input1 input2 ... inputN
In addition to this I'd like the users to be able to specify whether the operations should be performed in place, and to specify the target directory of the output.
mytool -t $TARGET --in-place operation op_argument input1 input2 input3
And as a very last requirement, I'd like users to be able to get help on each operation individually, as well as on the usage of the tool as a whole.
Here's my attempt at designing an Argument Parser for said tool, together with a Minimal, Complete, Verifiable Example:
#!/bin/env python
import argparse
from collections import namedtuple
Operations = namedtuple('Ops', 'name, argument, description')
IMPLEMENTED_OPERATIONS = {'echo': Operations('echo',
None,
'Echo inputs'),
'fancy': Operations('fancy',
'fancyarg',
'Do fancy stuff')}
if __name__ == "__main__":
# Parent parser with common stuff.
parent = argparse.ArgumentParser(add_help=False)
parent.add_argument('-t', '--target-directory', type=str, default='.',
help="An output directory to store output files.")
parent.add_argument('-i', '--in-place', action='store_true',
help="After succesful execution, delete the inputs.")
# The inputfiles should be the very last positional argument.
parent.add_argument('inputfiles', nargs='*', type=argparse.FileType('r'),
help="A list of input files to operate on.")
# Top level parser.
top_description = "This is mytool. It does stuff"
parser = argparse.ArgumentParser(prog="mytool",
description=top_description,
parents=[parent])
# Operation parsers.
subparsers = parser.add_subparsers(help='Sub-command help', dest='chosen_op')
op_parsers = {}
for op_name, op in IMPLEMENTED_OPERATIONS.items():
op_parsers[op_name] = subparsers.add_parser(op_name,
description=op.description,
parents=[parent])
if op.argument is not None:
op_parsers[op_name].add_argument(op.argument)
args = parser.parse_args()
op_args = {}
for key, subparser in op_parsers.items():
op_args[key] = subparser.parse_args()
print(args.chosen_op)
The problem I have is that the order of the positional arguments is wrong. Somehow, the way I implemented this makes Argparse think that the operation (and its op_argument) should come after the input files, which is obviously not the case.
How can I have the parent positional argument, in my case the inputfiles, as the last positional argument?
To the main parser, subparsers is just another positional argument, but with a unique nargs ('+...'). So it will look for the inputfiles arguments first, and then allocate any left overs to subparsers.
Mixing positionals with subparsers is tricky. It is best to define inputfiles as an argument for each subparser.
parents can make it easy to add the same set of arguments to several subparsers- however those arguments will added first.
So I think you want:
for op_name, op in IMPLEMENTED_OPERATIONS.items():
op_parsers[op_name] = subparsers.add_parser(op_name,
description=op.description,
parents=[parent])
if op.argument is not None:
op_parsers[op_name].add_argument(op.argument)
op_parsers[op_name].add_argument('inputfiles', nargs='*', type=argparse.FileType('r'),
help="A list of input files to operate on.")
As for the help, the normal behavior is to get help for the main parser, or for each subparser. Combining those into one display has been the topic of several SO questions. It's possible but not easy.
The main parser handles input strings in order - flags, positionals etc. When it handles the subparsers positional, it hands the task of to the name subparser, along with all remaining commandline strings. The subparser then acts like a new independent parser, and returns a namespace to the main parser to be incorporated into the main namespace. The main parser does not resume parsing the commandline. So the subparser action is always last.

How ca I get Python ArgParse to stop overwritting positional arguments in child parser

I am attempting to get my script working, but argparse keeps overwriting my positional arguments from the parent parser. How can I get argparse to honor the parent's value for these? It does keep values from optional args.
Here is a very simplified version of what I need. If you run this, you will see that the args are overwritten.
testargs.py
#! /usr/bin/env python3
import argparse
import sys
def main():
preparser = argparse.ArgumentParser(add_help=False)
preparser.add_argument('first',
nargs='?')
preparser.add_argument('outfile',
nargs='?',
type=argparse.FileType('w', encoding='utf-8'),
default=sys.stdout,
help='Output file')
preparser.add_argument(
'--do-something','-d',
action='store_true')
# Parse args with preparser, and find config file
args, remaining_argv = preparser.parse_known_args()
print(args)
parser = argparse.ArgumentParser(
parents=[preparser],
description=__doc__)
parser.add_argument(
'--clear-screen', '-c',
action='store_true')
args = parser.parse_args(args=remaining_argv,namespace=args )
print(args)
if __name__ == '__main__':
main()
And call it with testargs.py something /tmp/test.txt -d -c
You will see it keeps the -d but drops both the positional args and reverts them to defaults.
EDIT: see additional comments in the accepted answer for some caveats.
When you specify parents=[preparser] it means that parser is an extension of preparser, and will parse all arguments relevent to preparser which it is never given.
Lets say the preparser only has one positional argument first and the parser only has one positional argument second, when you make parser a child of preparser it expects both arguments:
import argparse
parser1 = argparse.ArgumentParser(add_help=False)
parser1.add_argument("first")
parser2 = argparse.ArgumentParser(parents=[parser1])
parser2.add_argument("second")
args2 = parser2.parse_args(["arg1","arg2"])
assert args2.first == "arg1" and args2.second == "arg2"
However passing only the remaining arguments that are left over from parser1 would just be ['second'] which is not the correct arguments to parser2:
parser1 = argparse.ArgumentParser(add_help=False)
parser1.add_argument("first")
args1, remaining_args = parser1.parse_known_args(["arg1","arg2"])
parser2 = argparse.ArgumentParser(parents=[parser1])
parser2.add_argument("second")
>>> args1
Namespace(first='arg1')
>>> remaining_args
['arg2']
>>> parser2.parse_args(remaining_args)
usage: test.py [-h] first second
test.py: error: the following arguments are required: second
To only process the arguments that were not handled by the first pass, do not specify it as the parent to the second parser:
parser1 = argparse.ArgumentParser(add_help=False)
parser1.add_argument("first")
args1, remaining_args = parser1.parse_known_args(["arg1","arg2"])
parser2 = argparse.ArgumentParser() #parents=[parser1]) #NO PARENT!
parser2.add_argument("second")
args2 = parser2.parse_args(remaining_args,args1)
assert args2.first == "arg1" and args2.second == "arg2"
The 2 positionals are nargs='?'. A positional like that is always 'seen', since an empty list matches that nargs.
First time through 'text.txt' matches with first and is put in the Namespace. Second time through there isn't any string to match, so the default is used - same as if you had not given that string the first time.
If I change first to have the default nargs, I get
error: the following arguments are required: first
from the 2nd parser. Even though there's a value in the Namespace it still tries to get a value from the argv. (it's like a default, but not quite).
Defaults for positionals with nargs='?' (or *) are tricky. They are optional, but not in quite the same way as optionals. The positional Actions are still called, but with a empty list of values.
I don't think the parents feature does anything for you. preparser already handles that set of arguments; there's no need to handle them again in parser, especially since all the relevant argument strings have been stripped out.
Another option is to leave the parents in, but use the default sys.argv[1:] in the 2nd parser. (but beware of side effects like opening files)
args = parser.parse_args(namespace=args )
A third option is to parse the arguments independently and merge them with a dictionary update.
adict = vars(preparse_args)
adict.update(vars(parser_args))
# taking some care in who overrides who
For more details look in argparse.py file at ArgumentParser._get_values, specifically the not arg_strings cases.
A note about the FileType. That type works nicely for small scripts where you will use the files right away and exit. It isn't so good on large programs where you might want to close the file after use (close stdout???), or use files in a with context.
edit - note on parents
add_argument creates an Action object, and adds it to the parser's list of actions. parse_args basically matches input strings with these actions.
parents just copies those Action objects (by reference) from parent to child. To the child parser it is just as though the actions were created with add_argument directly.
parents is most useful when you are importing a parser and don't have direct access to its definition. If you are defining both parent and child, then parents just saves you some typing/cut-n-paste.
This and other SO questions (mostly triggered the by-reference copy) show that the developers did not intend you to use both the parent and child to do parsing. It can be done, but there are glitches that the they did not consider.
===================
I can imagine defining a custom Action class that would 'behave' in a situation like this. It might, for example, check the namespace for some not default value before adding its own (possibly default) value.
Consider, for example if I changed the action of first to 'append':
preparser.add_argument('first', action='append', nargs='?')
The result is:
1840:~/mypy$ python3 stack37147683.py /tmp/test.txt -d -c
Namespace(do_something=True, first=['/tmp/test.txt'], outfile=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>)
Namespace(clear_screen=True, do_something=True, first=['/tmp/test.txt', None], outfile=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>)
From the first parser, first=['/tmp/test.txt']; from the second, first=['/tmp/test.txt', None].
Because of the append, the item from the first is preserved, and a new default has been added by the second parser.

Python argparse : how to detect duplicated optional argument?

I'm using argparse with optional parameter, but I want to avoid having something like this : script.py -a 1 -b -a 2
Here we have twice the optional parameter 'a', and only the second parameter is returned. I want either to get both values or get an error message.
How should I define the argument ?
[Edit]
This is the code:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-a', dest='alpha', action='store', nargs='?')
parser.add_argument('-b', dest='beta', action='store', nargs='?')
params, undefParams = self.parser.parse_known_args()
append action will collect the values from repeated use in a list
parser.add_argument('-a', '--alpha', action='append')
producing an args namespace like:
namespace(alpha=['1','3'], b='4')
After parsing you can check args.alpha, and accept or complain about the number of values. parser.error('repeated -a') can be used to issue an argparse style error message.
You could implement similar functionality in a custom Action class, but that requires understanding the basic structure and operation of such a class. I can't think anything that can be done in an Action that can't just as well be done in the appended list after.
https://stackoverflow.com/a/23032953/901925 is an answer with a no-repeats custom Action.
Why are you using nargs='?' with flagged arguments like this? Without a const parameter this is nearly useless (see the nargs=? section in the docs).
Another similar SO: Python argparse with nargs behaviour incorrect

Argparse positional arguments with prefix

My python3 script works with the input and the output file which is specified on the command line.
The usage should looks like this
xxxx.py [-h] --input=file --output=file
In code I am using
parser.add_argument("input", help='Input file');
parser.add_argument("output", help='Output file');
but the arguments are without the necessary prefix. Is there a way to specify the prefix for each argument?
Simply include the double-dash:
parser.add_argument("--input", help='Input file');
parser.add_argument("--output", help='Output file');
Arguments are either positional or optional; arguments starting with -- are always optional. You cannot create positional arguments with a -- prefix and you really should not. The -- prefix is a user interface convention you really do not want to break.

Categories