Parameter dependencies in Python - can't make it work

六眼飞鱼酱① 提交于 2020-01-23 02:45:10

问题


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?


回答1:


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.



来源:https://stackoverflow.com/questions/39437461/parameter-dependencies-in-python-cant-make-it-work

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!