3

I am trying to add a parameter dependency to my script. The idea is that --clone argument will require non-empty --gituser.

After perusing this example, I tried the following

In [93]: class CloneAction(argparse.Action):
    ...:     def __call__(self, parser, namespace, _):
    ...:         if not namespace.git_user and namespace.clone:
    ...:             parser.error('"--clone" requires legal git user')
    ...:             
In [94]: parser = argparse.ArgumentParser()

In [95]: parser.add_argument('-g', '--gituser', dest='git_user', type=str, default='', action=CloneAction)
Out[95]: CloneAction(option_strings=['-g', '--gituser'], dest='git_user', nargs=None, const=None, default='', type=<type 'str'>, choices=None, help=None, metavar=None)

In [96]: parser.add_argument('--clone', action='store_true', default=False)
Out[96]: _StoreTrueAction(option_strings=['--clone'], dest='clone', nargs=0, const=True, default=False, type=None, choices=None, help=None, metavar=None)

Alas, it did not work

In [97]: parser.parse_args(['--clone'])
Out[97]: Namespace(clone=True, git_user='')

What did I do wrong?

Community
  • 1
  • 1
volcano
  • 3,578
  • 21
  • 28
  • 1
    Can you please include your implementation of `CloneAction`? – Martijn Pieters Sep 11 '16 at 15:08
  • Also, note that the `--clone` action is just `_StoreTrueAction`, which won't check for any dependencies. You'd have to use a special action type *there* to check it, as there is no `-g` argument passed in triggering the `CloneAction` being checked. – Martijn Pieters Sep 11 '16 at 15:10
  • @MartijnPieters Ooops, missed that. Added. Do I need to define custom actions for both parameters? – volcano Sep 11 '16 at 15:15
  • That depends; is it an error to provide `--gituser` if no `--clone` has been set? – Martijn Pieters Sep 11 '16 at 15:28

1 Answers1

8

This kind of inter argument dependency is easier to implement after parsing.

args = parser.parse_args()
if not namespace.git_user and namespace.clone:
    parser.error('"--clone" requires legal git user')

At that point, both git_user and clone have been parsed, and have their final values.

As you implemented it, the custom action is run only when there's a --gituser argument. So I think it will raise the error when you give it --gituser without --clone.

You could give --clone a similar custom action, but it would also have to handle the store_true details. And what should happen with the --clone --gituser value sequence? The clone action will be run before the gituser value has been parsed. Tests like this run into some tough argument order problems.

A couple of other issues:

  • your custom action does not store any value, regardless of error not. It's better to customize the store subclass.

  • custom actions should raise argparse.ArgumentError rather than call parser.error directly.

The unittest file, test/test_argparse.py has an example of custom actions with mutual tests like this. But it's just a toy, verifying that such code is allowed.

==================

You could, in theory, implement a --clone action that sets the required attribute of the --gituser Action. That way, if --gituser is not used, the final required actions test of parse_args will raise an error. But that requires saving a reference to the Action displayed in out[95] (or finding that in the parse._actions list. Feasible but messy.

===================

Here's an example of a pair of interacting custom action classes from test/test_argparse.py.

class OptionalAction(argparse.Action):

    def __call__(self, parser, namespace, value, option_string=None):
        try:
            # check destination and option string
            assert self.dest == 'spam', 'dest: %s' % self.dest
            assert option_string == '-s', 'flag: %s' % option_string
            # when option is before argument, badger=2, and when
            # option is after argument, badger=<whatever was set>
            expected_ns = NS(spam=0.25)
            if value in [0.125, 0.625]:
                expected_ns.badger = 2
            elif value in [2.0]:
                expected_ns.badger = 84
            else:
                raise AssertionError('value: %s' % value)
            assert expected_ns == namespace, ('expected %s, got %s' %
                                              (expected_ns, namespace))
        except AssertionError:
            e = sys.exc_info()[1]
            raise ArgumentParserError('opt_action failed: %s' % e)
        setattr(namespace, 'spam', value)

NS is a shorthand for argparse.Namespace.

class PositionalAction(argparse.Action):

    def __call__(self, parser, namespace, value, option_string=None):
        try:
            assert option_string is None, ('option_string: %s' %
                                           option_string)
            # check destination
            assert self.dest == 'badger', 'dest: %s' % self.dest
            # when argument is before option, spam=0.25, and when
            # option is after argument, spam=<whatever was set>
            expected_ns = NS(badger=2)
            if value in [42, 84]:
                expected_ns.spam = 0.25
            elif value in [1]:
                expected_ns.spam = 0.625
            elif value in [2]:
                expected_ns.spam = 0.125
            else:
                raise AssertionError('value: %s' % value)
            assert expected_ns == namespace, ('expected %s, got %s' %
                                              (expected_ns, namespace))
        except AssertionError:
            e = sys.exc_info()[1]
            raise ArgumentParserError('arg_action failed: %s' % e)
        setattr(namespace, 'badger', value)

They are used in

parser = argparse.ArgumentParser()
parser.add_argument('-s', dest='spam', action=OptionalAction,
        type=float, default=0.25)
parser.add_argument('badger', action=PositionalAction,
        type=int, nargs='?', default=2)

And supposed to work with:

'-s0.125' producing: NS(spam=0.125, badger=2)),
'42',                NS(spam=0.25, badger=42)),
'-s 0.625 1',        NS(spam=0.625, badger=1)),
'84 -s2',            NS(spam=2.0, badger=84)),

This is an example of the kind of cross checking that can be done. But I'll repeat that generally interactions are best handled after parsing, not during.

As to the implementation question - if the user does not give you --gituser, your custom Action is never called. The Action.__call__ of an optional is only used when that argument is used. positionals are always used, but not optionals.

wim
  • 338,267
  • 99
  • 616
  • 750
hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • Thanks for the answer - but it's not was I looking for. I was trying to understand specifics of implementation – volcano Sep 12 '16 at 03:58
  • Specifics about how `argparse` implements the parsing, or how you should link two arguments via a custom action? – hpaulj Sep 12 '16 at 04:19
  • 2
    I've added an example of interacting action classes taken from the `argparse` unittest. Maybe it will give you ideas of how to implement such a pair yourself. – hpaulj Sep 12 '16 at 04:32
  • Thanks! That's what I was looking for – volcano Sep 12 '16 at 05:03