163

I have a requirement as follows:

./xyifier --prox --lport lport --rport rport

for the argument prox , I use action='store_true' to check if it is present or not. I do not require any of the arguments. But, if --prox is set I require rport and lport as well. Is there an easy way of doing this with argparse without writing custom conditional coding.

More Code:

non_int.add_argument('--prox', action='store_true', help='Flag to turn on proxy')
non_int.add_argument('--lport', type=int, help='Listen Port.')
non_int.add_argument('--rport', type=int, help='Proxy port.')
asudhak
  • 2,929
  • 4
  • 22
  • 27
  • 1
    Plugging, but I wanted to mention my library [joffrey](https://github.com/supposedly/joffrey). Lets you do what this question wants, for example, without making you validate everything yourself (as in the accepted answer) or rely on a loopholey hack (as in the second-highest-voted response). –  Oct 01 '19 at 22:04
  • For anyone arriving here, another amazing solution: https://stackoverflow.com/a/44210638/6045800 – Tomerikoo May 31 '20 at 15:56

6 Answers6

183

No, there isn't any option in argparse to make mutually inclusive sets of options.

The simplest way to deal with this would be:

if args.prox and (args.lport is None or args.rport is None):
    parser.error("--prox requires --lport and --rport.")

Actually there's already an open PR with an enhancement proposal : https://github.com/python/cpython/issues/55797

rkachach
  • 16,517
  • 6
  • 42
  • 66
borntyping
  • 2,931
  • 2
  • 23
  • 30
  • 3
    Thats what I ended up doing – asudhak Oct 16 '13 at 22:41
  • 33
    Thank you for `parser.error` method, this is what I was looking for! – MarSoft Dec 09 '15 at 21:21
  • 8
    shouldn't you use 'or'? after all you require both args `if args.prox and (args.lport is None or args.rport is None):` – yossiz74 Oct 18 '16 at 09:11
  • 2
    Instead of `args.lport is None`, you can simply use `not args.lport`. I think it's a little more pythonic. – stefanbschneider Mar 01 '18 at 13:52
  • 13
    That would stop you from setting `--lport` or `--rport` to `0`, which might be a valid input to the program. – borntyping Mar 01 '18 at 14:34
  • 1
    Interdependence of fields seems like a good feature to have within argparse itself though. – John Jiang Sep 13 '19 at 18:13
  • What is "parser" an instance of? I assumed it was a RequestParser instance, but doesn't seem so? Thanks. – KillerKode Jan 23 '20 at 15:03
  • 2
    The "parser" object is an instance of [argparse.ArgumentParser](https://docs.python.org/3/library/argparse.html#argumentparser-objects). – borntyping Jan 24 '20 at 17:58
  • @rkachach: your edit mentioning an MR that adds this to argparse might be better as a comment than an addition to the answer - it's older than this question and doesn't seem to have any likelyhood of being merged. – borntyping Apr 30 '22 at 14:36
  • @borntyping I think it adds value in the sense it's something that they didn't merge for some reason. I can convert it my edit o answer if more people think that it adds no value. – rkachach May 01 '22 at 10:53
84

You're talking about having conditionally required arguments. Like @borntyping said you could check for the error and do parser.error(), or you could just apply a requirement related to --prox when you add a new argument.

A simple solution for your example could be:

non_int.add_argument('--prox', action='store_true', help='Flag to turn on proxy')
non_int.add_argument('--lport', required='--prox' in sys.argv, type=int)
non_int.add_argument('--rport', required='--prox' in sys.argv, type=int)

This way required receives either True or False depending on whether the user as used --prox. This also guarantees that -lport and -rport have an independent behavior between each other.

Mira
  • 1,983
  • 1
  • 11
  • 10
  • 13
    Note that `ArgumentParser` can be used to parse arguments from a list other than `sys.argv`, which case this would fail. – BallpointBen Aug 15 '18 at 17:58
  • 2
    Also this will fail if the `--prox=` syntax is used. – fnkr Aug 01 '20 at 11:02
  • 2
    then `required='--prox' in " ".join(sys.argv)` – debuti Sep 15 '20 at 15:38
  • There also several cases where the parser is constructed before its usage, as in [Django commands](https://docs.djangoproject.com/en/3.2/howto/custom-management-commands/#django.core.management.BaseCommand.create_parser), preventing this approach to be useful – artu-hnrq Oct 16 '21 at 03:05
20

How about using parser.parse_known_args() method and then adding the --lport and --rport args as required args if --prox is present.

# just add --prox arg now
non_int = argparse.ArgumentParser(description="stackoverflow question", 
                                  usage="%(prog)s [-h] [--prox --lport port --rport port]")
non_int.add_argument('--prox', action='store_true', 
                     help='Flag to turn on proxy, requires additional args lport and rport')
opts, rem_args = non_int.parse_known_args()
if opts.prox:
    non_int.add_argument('--lport', required=True, type=int, help='Listen Port.')
    non_int.add_argument('--rport', required=True, type=int, help='Proxy port.')
    # use options and namespace from first parsing
    non_int.parse_args(rem_args, namespace = opts)

Also keep in mind that you can supply the namespace opts generated after the first parsing while parsing the remaining arguments the second time. That way, in the the end, after all the parsing is done, you'll have a single namespace with all the options.

Drawbacks:

  • If --prox is not present the other two dependent options aren't even present in the namespace. Although based on your use-case, if --prox is not present, what happens to the other options is irrelevant.
  • Need to modify usage message as parser doesn't know full structure
  • --lport and --rport don't show up in help message
Aditya Sriram
  • 443
  • 6
  • 17
  • This looks very neat. Unfortunately in cases where `--prox` is a required parameter, the call to `parse_args(…)` fails since the required parameter is not in the list of `rem_args`. – Hermann Jun 29 '23 at 14:15
  • I'm not sure what you mean by "`--prox` is a required parameter", do you mean `--prox` isn't an optional argument? Could you please elaborate (maybe with an example)? – Aditya Sriram Jul 20 '23 at 18:11
  • Certainly, [here](https://gist.github.com/hoehermann/08d56f62a0b04e88697feba2f362884b) you are. This illustrates the case where the common parameter is not a flag, but a choice. I now realize that the question specifically asks for the `store_true` variant, so the answer is still very good. – Hermann Jul 27 '23 at 21:34
8

Do you use lport when prox is not set. If not, why not make lport and rport arguments of prox? e.g.

parser.add_argument('--prox', nargs=2, type=int, help='Prox: listen and proxy ports')

That saves your users typing. It is just as easy to test if args.prox is not None: as if args.prox:.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • 3
    For completeness of the example, when your nargs is > 1, you'll get a list in the parsed argument etc., which you can address in the usual manners. E.g., `a,b = args.prox`, `a = args.prox[0]`, etc. – Dannid Jul 10 '17 at 21:15
2

The accepted answer worked great for me! Since all code is broken without tests here is how I tested the accepted answer. parser.error() does not raise an argparse.ArgumentError error it instead exits the process. You have to test for SystemExit.

with pytest

import pytest
from . import parse_arguments  # code that rasises parse.error()


def test_args_parsed_raises_error():
    with pytest.raises(SystemExit):
        parse_arguments(["argument that raises error"])

with unittests

from unittest import TestCase
from . import parse_arguments  # code that rasises parse.error()

class TestArgs(TestCase):

    def test_args_parsed_raises_error():
        with self.assertRaises(SystemExit) as cm:
            parse_arguments(["argument that raises error"])

inspired from: Using unittest to test argparse - exit errors

Daniel Butler
  • 3,239
  • 2
  • 24
  • 37
0

I did it like this:

if t or x or y:
    assert t and x and y, f"args: -t, -x and -y should be given together"
John
  • 1,139
  • 3
  • 16
  • 33