Python Argparse conditionally required arguments

后端 未结 6 493
梦谈多话
梦谈多话 2020-11-28 09:35

I have done as much research as possible but I haven\'t found the best way to make certain cmdline arguments necessary only under certain conditions, in this case only if ot

相关标签:
6条回答
  • 2020-11-28 10:13

    I've been searching for a simple answer to this kind of question for some time. All you need to do is check if '--argument' is in sys.argv, so basically for your code sample you could just do:

    import argparse
    import sys
    
    if __name__ == '__main__':
        p = argparse.ArgumentParser(description='...')
        p.add_argument('--argument', required=False)
        p.add_argument('-a', required='--argument' in sys.argv) #only required if --argument is given
        p.add_argument('-b', required='--argument' in sys.argv) #only required if --argument is given
        args = p.parse_args()
    

    This way required receives either True or False depending on whether the user as used --argument. Already tested it, seems to work and guarantees that -a and -b have an independent behavior between each other.

    0 讨论(0)
  • 2020-11-28 10:19

    You can implement a check by providing a custom action for --argument, which will take an additional keyword argument to specify which other action(s) should become required if --argument is used.

    import argparse
    
    class CondAction(argparse.Action):
        def __init__(self, option_strings, dest, nargs=None, **kwargs):
            x = kwargs.pop('to_be_required', [])
            super(CondAction, self).__init__(option_strings, dest, **kwargs)
            self.make_required = x
    
        def __call__(self, parser, namespace, values, option_string=None):
            for x in self.make_required:
                x.required = True
            try:
                return super(CondAction, self).__call__(parser, namespace, values, option_string)
            except NotImplementedError:
                pass
    
    p = argparse.ArgumentParser()
    x = p.add_argument("--a")
    p.add_argument("--argument", action=CondAction, to_be_required=[x])
    

    The exact definition of CondAction will depend on what, exactly, --argument should do. But, for example, if --argument is a regular, take-one-argument-and-save-it type of action, then just inheriting from argparse._StoreAction should be sufficient.

    In the example parser, we save a reference to the --a option inside the --argument option, and when --argument is seen on the command line, it sets the required flag on --a to True. Once all the options are processed, argparse verifies that any option marked as required has been set.

    0 讨论(0)
  • 2020-11-28 10:19

    This is really the same as @Mira 's answer but I wanted to show it for the case where when an option is given that an extra arg is required:

    For instance, if --option foo is given then some args are also required that are not required if --option bar is given:

    if __name__ == "__main__":
        parser = argparse.ArgumentParser()
        parser.add_argument('--option', required=True,
            help='foo and bar need different args')
    
        if 'foo' in sys.argv:
            parser.add_argument('--foo_opt1', required=True,
               help='--option foo requires "--foo_opt1"')
            parser.add_argument('--foo_opt2', required=True,
               help='--option foo requires "--foo_opt2"')
            ...
    
        if 'bar' in sys.argv:
            parser.add_argument('--bar_opt', required=True,
               help='--option bar requires "--bar_opt"')
            ...
    
    

    It's not perfect - for instance proggy --option foo --foo_opt1 bar is ambiguous but for what I needed to do its ok.

    0 讨论(0)
  • 2020-11-28 10:21

    Your post parsing test is fine, especially if testing for defaults with is None suits your needs.

    http://bugs.python.org/issue11588 'Add "necessarily inclusive" groups to argparse' looks into implementing tests like this using the groups mechanism (a generalization of mutuall_exclusive_groups).

    I've written a set of UsageGroups that implement tests like xor (mutually exclusive), and, or, and not. I thought those where comprehensive, but I haven't been able to express your case in terms of those operations. (looks like I need nand - not and, see below)

    This script uses a custom Test class, that essentially implements your post-parsing test. seen_actions is a list of Actions that the parse has seen.

    class Test(argparse.UsageGroup):
        def _add_test(self):
            self.usage = '(if --argument then -a and -b are required)'
            def testfn(parser, seen_actions, *vargs, **kwargs):
                "custom error"
                actions = self._group_actions
                if actions[0] in seen_actions:
                    if actions[1] not in seen_actions or actions[2] not in seen_actions:
                        msg = '%s - 2nd and 3rd required with 1st'
                        self.raise_error(parser, msg)
                return True
            self.testfn = testfn
            self.dest = 'Test'
    p = argparse.ArgumentParser(formatter_class=argparse.UsageGroupHelpFormatter)
    g1 = p.add_usage_group(kind=Test)
    g1.add_argument('--argument')
    g1.add_argument('-a')
    g1.add_argument('-b')
    print(p.parse_args())
    

    Sample output is:

    1646:~/mypy/argdev/usage_groups$ python3 issue25626109.py --arg=1 -a1
    usage: issue25626109.py [-h] [--argument ARGUMENT] [-a A] [-b B]
                            (if --argument then -a and -b are required)
    issue25626109.py: error: group Test: argument, a, b - 2nd and 3rd required with 1st
    

    usage and error messages still need work. And it doesn't do anything that post-parsing test can't.


    Your test raises an error if (argument & (!a or !b)). Conversely, what is allowed is !(argument & (!a or !b)) = !(argument & !(a and b)). By adding a nand test to my UsageGroup classes, I can implement your case as:

    p = argparse.ArgumentParser(formatter_class=argparse.UsageGroupHelpFormatter)
    g1 = p.add_usage_group(kind='nand', dest='nand1')
    arg = g1.add_argument('--arg', metavar='C')
    g11 = g1.add_usage_group(kind='nand', dest='nand2')
    g11.add_argument('-a')
    g11.add_argument('-b')
    

    The usage is (using !() to mark a 'nand' test):

    usage: issue25626109.py [-h] !(--arg C & !(-a A & -b B))
    

    I think this is the shortest and clearest way of expressing this problem using general purpose usage groups.


    In my tests, inputs that parse successfully are:

    ''
    '-a1'
    '-a1 -b2'
    '--arg=3 -a1 -b2'
    

    Ones that are supposed to raise errors are:

    '--arg=3'
    '--arg=3 -a1'
    '--arg=3 -b2'
    
    0 讨论(0)
  • 2020-11-28 10:26

    Until http://bugs.python.org/issue11588 is solved, I'd just use nargs:

    p = argparse.ArgumentParser(description='...')
    p.add_argument('--arguments', required=False, nargs=2, metavar=('A', 'B'))
    

    This way, if anybody supplies --arguments, it will have 2 values.

    Maybe its CLI result is less readable, but code is much smaller. You can fix that with good docs/help.

    0 讨论(0)
  • 2020-11-28 10:31

    For arguments I've come up with a quick-n-dirty solution like this. Assumptions: (1) '--help' should display help and not complain about required argument and (2) we're parsing sys.argv

    p = argparse.ArgumentParser(...)
    p.add_argument('-required', ..., required = '--help' not in sys.argv )
    

    This can easily be modified to match a specific setting. For required positionals (which will become unrequired if e.g. '--help' is given on the command line) I've come up with the following: [positionals do not allow for a required=... keyword arg!]

    p.add_argument('pattern', ..., narg = '+' if '--help' not in sys.argv else '*' )
    

    basically this turns the number of required occurrences of 'pattern' on the command line from one-or-more into zero-or-more in case '--help' is specified.

    0 讨论(0)
提交回复
热议问题