Mutual exclusion between argument groups - python

I'm trying to implement the following argument dependency using the argparse module:
./prog [-h | [-v schema] file]
meaning the user must pass either -h or a file, if a file is passed the user can optionally pass -v schema.
That's what I have now but that doesn't seem to be working:
import argparse
parser = argparse.ArgumentParser()
mtx = parser.add_mutually_exclusive_group()
mtx.add_argument('-h', ...)
grp = mtx.add_argument_group()
grp.add_argument('-v', ...)
grp.add_argument('file', ...)
args = parser.parse_args()
It looks like you can't add an arg group to a mutex group or am I missing something?

If -h means the default help, then this is all you need (this help is already exclusive)
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('file')
parser.add_argument('-s','--schema')
parser.parse_args('-h'.split()) # parser.print_help()
producing
usage: stack23951543.py [-h] [-s SCHEMA] file
...
If by -h you mean some other action, lets rename it -x. This would come close to what you describe
parser = argparse.ArgumentParser()
parser.add_argument('-s','--schema', default='meaningful default value')
mxg = parser.add_mutually_exclusive_group(required=True)
mxg.add_argument('-x','--xxx', action='store_true')
mxg.add_argument('file', nargs='?')
parser.parse_args('-h'.split())
usage is:
usage: stack23951543.py [-h] [-s SCHEMA] (-x | file)
Now -x or file is required (but not both). -s is optional in either case, but with a meaningful default, it doesn't matter if it is omitted. And if -x is given, you can just ignore the -s value.
If necessary you could test args after parsing, to confirm that if args.file is not None, then args.schema can't be either.
Earlier I wrote (maybe over thinking the question):
An argument_group cannot be added to a mutually_exclusive_group. The two kinds of groups have different purposes and functions. There are previous SO discussions of this (see 'related'), as well as a couple of relevant Python bug issues. If you want tests that go beyond a simple mutually exclusive group, you probably should do your own testing after parse_args. That may also require your own usage line.
An argument_group is just a means of grouping and labeling arguments in the help section.
A mutually_exclusive_group affects the usage formatting (if it can), and also runs tests during parse_args. The use of 'group' for both implies that they are more connected than they really are.
http://bugs.python.org/issue11588 asks for nested groups, and the ability to test for 'inclusivity' as well. I tried to make the case that 'groups' aren't general enough to express all the kinds of testing that users want. But it's one thing to generalize the testing mechanism, and quite another to come up with an intuitive API. Questions like this suggest that argparse does need some sort of 'nested group' syntax.

Related

Python - argparse - (Singleton) Argument w/ optional parameter [duplicate]

I'm trying to implement the following argument dependency using the argparse module:
./prog [-h | [-v schema] file]
meaning the user must pass either -h or a file, if a file is passed the user can optionally pass -v schema.
That's what I have now but that doesn't seem to be working:
import argparse
parser = argparse.ArgumentParser()
mtx = parser.add_mutually_exclusive_group()
mtx.add_argument('-h', ...)
grp = mtx.add_argument_group()
grp.add_argument('-v', ...)
grp.add_argument('file', ...)
args = parser.parse_args()
It looks like you can't add an arg group to a mutex group or am I missing something?
If -h means the default help, then this is all you need (this help is already exclusive)
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('file')
parser.add_argument('-s','--schema')
parser.parse_args('-h'.split()) # parser.print_help()
producing
usage: stack23951543.py [-h] [-s SCHEMA] file
...
If by -h you mean some other action, lets rename it -x. This would come close to what you describe
parser = argparse.ArgumentParser()
parser.add_argument('-s','--schema', default='meaningful default value')
mxg = parser.add_mutually_exclusive_group(required=True)
mxg.add_argument('-x','--xxx', action='store_true')
mxg.add_argument('file', nargs='?')
parser.parse_args('-h'.split())
usage is:
usage: stack23951543.py [-h] [-s SCHEMA] (-x | file)
Now -x or file is required (but not both). -s is optional in either case, but with a meaningful default, it doesn't matter if it is omitted. And if -x is given, you can just ignore the -s value.
If necessary you could test args after parsing, to confirm that if args.file is not None, then args.schema can't be either.
Earlier I wrote (maybe over thinking the question):
An argument_group cannot be added to a mutually_exclusive_group. The two kinds of groups have different purposes and functions. There are previous SO discussions of this (see 'related'), as well as a couple of relevant Python bug issues. If you want tests that go beyond a simple mutually exclusive group, you probably should do your own testing after parse_args. That may also require your own usage line.
An argument_group is just a means of grouping and labeling arguments in the help section.
A mutually_exclusive_group affects the usage formatting (if it can), and also runs tests during parse_args. The use of 'group' for both implies that they are more connected than they really are.
http://bugs.python.org/issue11588 asks for nested groups, and the ability to test for 'inclusivity' as well. I tried to make the case that 'groups' aren't general enough to express all the kinds of testing that users want. But it's one thing to generalize the testing mechanism, and quite another to come up with an intuitive API. Questions like this suggest that argparse does need some sort of 'nested group' syntax.

argparse option of options

I am trying to add option of options in argparse.
Currently I have:
group = parser.add_mutually_exclusive_group()
group.add_argument("--md", help="Create xyz file for each ionic step for"
" visualization", action='store_true')
group.add_argument("--force", help="See which atom has maximum force",
action='store_true')
group.add_argument("--opt", help="grep string from file",
nargs=2, metavar=("str", "file"))
parser.add_argument("--xsf", help="Create xsf file for md(default is xyz)"
" visualization", action='store_true')
parser.add_argument("-N", help="Showing first N line",
metavar='integer', type=int)
parser.add_argument("-n", help="Showing last n line",
metavar='integer', type=int)
args = parser.parse_args()
which gives:
./foo.py --h
usage: foo.py [-h]
[--md | --force | --opt str file]
[--xsf] [-N integer] [-n integer]
But I want --xsf as a suboption for --md, -N,-n for --opt; e.g.
./foo.py --h
usage: foo.py [-h]
[--md [--xsf]| --force | --opt str file [-N integer] [-n integer]]
But I dont know how to achieve that. May be I am missing something, but there is no option like that in argparse doc
Is there any other way of getting that?
The mutually_exclusive_group mechanism is quite simple, and does not work with any kind of nesting, or subgrouping.
There is a Python bug/issue requesting a more comprehensive grouping mechanism, but the proposed patch is rather complicated. The problem isn't just with testing, it's with defining the groups in a user friendly way, and with generating the usage line. It's nice that you included a desired usage, but that format is well beyond the capabilities of the current help formatter.
You might look into recasting your problem as a subparser one. subparsers are mutually exclusive (you can only give one command name), and you could specify --xsf as an argument for md, and -N as argument for --opt. But subparsers has its own help issues.
Another route is to write your own usage, and do your own testing of arguments after parsing. With a suitable choice of defaults you can usually tell whether an argument has been provided or not (the user can't specify None) or you can ignore unnecessary ones.

How to require one command line action argument among several possible but exclusive?

Using argparse, is there a simple way to specify arguments which are mutually exclusive so that the application asks for one of these arguments have to be provided but only one of them?
Example of fictive use-case:
> myapp.py foo --bar
"Foo(bar) - DONE"
> myapp.py read truc.txt
"Read: truc.txt - DONE"
>myapp.py foo read
Error: use "myapp.py foo [options]" or "myapp.py read [options]" (or something similar).
> myapp.py foo truc.txt
Error: "foo" action don't need additional info.
> myapp.py read --bar
Error: "read" action don't have a "--bar" option.
My goal is to have a "driver" application(1) that would internally apply one action depending on the first command line argument and have arguments depending on the action.
So far I see no obvious ways to do this with argparse without manually processing the arguments myself, but maybe I missed something Pythonic? (I'm not a Python3 expert...yet)
I call it "driver" because it might be implemented by calling another application, like gcc does with different compilers.
What you're trying to do is actually supported quite well in Python.
See Mutual Exclusion
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('foo', dest='foo', nargs=1)
group.add_argument('read', dest='read', nargs=1)
args = parser.parse_args()
return args

argparse mutually_exclusive_group with sub groups

I am trying to make a script with usage as follows:
my_script [-p parg -l larg] | [-s sarg]
i.e the script either takes -p and -l argument OR -s argument. It is an error if both -p and -s are specified. I tried the following but doesn't seem to work
import argparse
parser = argparse.ArgumentParser(description='Some Desc')
gp = parser.add_mutually_exclusive_group()
num_gp = gp.add_argument_group()
num_gp.add_argument('-p')
num_gp.add_argument('-l')
gp.add_argument('-s')
In [18]: parser.parse_args(['-p blahp', '-l blahl', '-s blahs'])
Out[18]: Namespace(l=' blahl', p=' blahp', s=' blahs') #ERROR Should have failed as I specify both `-p` and `-s` which belong to a mutually_exclusive_group
I think you're misusing the mutually exclusive groups. Groups are not mutually exclusive with other groups, members of a group are mutually exclusive with each other. Also, as far as I can tell from the docs, standard groups do not affect parsing logic, they only help to organize the help message generated by the parser.
Here is how you could make two options, -p and -s for example, mutually exclusive:
import argparse
parser = argparse.ArgumentParser(description='Some Desc')
group1 = parser.add_mutually_exclusive_group()
group1.add_argument("-p")
group1.add_argument("-s")
# This works
args = parser.parse_args(["-p", "argForP"])
# This will throw an error
args = parser.parse_args(["-p", "argForP", "-s", "argForS"])
But I'm not sure if this functionality will allow you to implement your use case because I'm not sure that an argument can belong to two mutually exclusive groups. However you can always do error checking yourself, and use parser.error. That would look something like this:
message = "invalid options"
# Tells us p is used without l or vise versa
if (args.p is None) != (args.l is None):
parser.error(message)
# Tells is if p-l is used with s
if (args.p is None) == (args.s is None):
# Either both are used or neigher
parser.error(message)

Parsing command-line arguments similar to archlinux pacman

I'm creating a python script with usage in the same style as pacman in Arch Linux, summarized by:
prog <operation> [options] [targets]
operations are of the form -X (hyphen, uppercase letter), and one is required when calling the script.
options are of the form -x (hyphen, lowercase letter), and can mean different things for different operations.
For example:
pacman -Syu means perform the sync operation with refresh and sysupgrade options, upgrading the entire system with fresh packages.
pacman -Qu means perform the query operation with the upgrades option, listing all outdated packages.
pacman -Ss <arg> means perform the sync operation with the search option, which expects another argument as the pattern to search for in the sync packages.
The punchline:
I've been looking into the argparse library for python, trying to figure out how to implement this. I've run into some problems/design issues so far:
argparse only accepts hyphen-prefixed arguments as optional arguments. All my "operations" would show up as optional arguments, when one is definitely required.
I could make my script have one "positional"/required argument, which would be the operation (I would have to switch operations to words, like upgrade or add), followed by optional arguments. This, however, still wouldn't solve the same-option-symbol-working-differently issue, and also wouldn't let me easily list all the supported operations in the --help text.
What's the smoothest way to handle this argument parsing? I'm not against changing my command's usage, but as I said above, it doesn't seem to help my situation as far as I can tell.
Thanks
One option would be to make -S and -Q part of a mutually exclusive option group with the required keyword argument set to True. This wouldn't enforce the requirement to make those the first arguments given, nor would it restrict which other options could be used with each. You'd have to enforce the latter after calling parse_args.
Another option I thought of was to make -S and -Q subcommands. Calling add_parser with a first argument starting with '-' seems to be legal, but the errors you get when you actually try to call your script makes me think that either the support is buggy/unintended, or that the error reporting is buggy.
Yet another option would be to use getopt: http://docs.python.org/library/getopt.html
So I found this support for sub-commands buried in the argparse help. It's exactly what I need, with the only caveat being I am not using -X as the format for operations; I am just using words like add and search instead.
For completeness here's an example of using sub-parsers from the link above:
>>> # 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')
>>>
>>> # parse some argument lists
>>> parser.parse_args(['a', '12'])
Namespace(bar=12, foo=False)
>>> parser.parse_args(['--foo', 'b', '--baz', 'Z'])
Namespace(baz='Z', foo=True)

Categories

Resources