简体   繁体   中英

python argparse store --foo=bar as args.key='foo', args.value='bar'

I'd like to parse a command line that has a mutually exclusive group of options. Normally, I'd just use --foo bar which would produce, in the namespace, args.foo = 'bar'

However, since all of these options are mutually exclusive, and I'm interested in both the option name and the argument passed to the option, and I have several of these options that need to be fed downstream, what I'd really like is to get back args.option_name = 'foo', args.option_value = 'bar' in my namespace instead of args.foo='bar' .

What I've done is:

class KeyAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        setattr(namespace, self.dest, values) 
        setattr(namespace, self.dest+'_key', option_string)

frob = parser.add_mutually_exclusive_group()
frob.add_argument('--foo', dest='thing', nargs='?', action=KeyAction)
frob.add_argument('--nar', dest='thing', nargs='?', action=KeyAction)

when run, my namespace will look like:

Namespace(thing_key='--foo', thing='bar')

when --foo=bar is parsed. Of course, sadly, if --foo or --nar is never passed in, namespace.thing_key doesn't get defined, so I have to use a getattr() .

The action override, while functional, doesn't seem really right.

I suspect the brilliant minds behind argparse already got this right somehow, and I'm just missing it in the documentation and from my read of argparse.py.

What's the best, right, pythonic, way to do this? Subparsers? I'm using python 3.5.

So I ended up using data from both of your answers to construct this, which handles the option, it's argument, and sets everything sanely at initialization time.

Thank you very much for the hints, clues, and validation. I'm surprised this hasn't come up in argparse before and become something standardized. It is a corner case, but isn't that much of a corner case when using mutually exclusive options .

    class ValueAction(argparse.Action):
        """Override to store both the format type as well as the argument"""
        # pylint: disable=too-few-public-methods
        def __init__(self, option_strings, dest, **kwargs):
            self._dest = dest
            dest = dest + '_arguments'
            container = kwargs.pop('container')
            kwargs['action'] = kwargs.pop('subaction')
            action_class = container._pop_action_class(kwargs)
            if not callable(action_class):
                raise ValueError('unknown action "%s"' % (action_class,))
            self._action = action_class(option_strings, dest, **kwargs)
            super().__init__(option_strings, dest, **kwargs)

        def __call__(self, parser, namespace, values, option_string=None):
            self._action(parser, namespace, values, option_string)
            if isinstance(option_string, str):
                while option_string[0] in parser.prefix_chars:
                    option_string = option_string[1:]
            setattr(namespace, self._dest, option_string)

As I understand it, you would like to be able to specify a composite action so that you both save the option name used in one Namespace slot and have the updates for some other action performed. Eg, you want to be able to write something like:

group = argparse.mutually_exclusive_group()
group.add_argument('--foo', action=StoreOption, subaction='store_true')
group.add_argument('--bar', nargs='?', action=StoreOption, subaction='store')

The action class is StoreOption , but it will call the action specified by subaction to perform additional updates to the Namespace object.

The code I got working (with very limited testing) is shown below:

import argparse

class StoreOption(argparse.Action):
  def __init__(self, **kwargs):
    kwargs['action'] = kwargs.pop('subaction')
    container = kwargs.pop('container')

    action_class = container._pop_action_class(kwargs)
    if not callable(action_class):
      raise ValueError('unknown action "%s"' % (action_class,))

    kwargs['dest'] = 'thing'
    self._action = action_class(**kwargs)
    print "_action:", self._action

    super(StoreOption, self).__init__(
        option_strings= self._action.option_strings,
        dest= self._action.dest,
        nargs= self._action.nargs,
        const= self._action.const,
        default= self._action.default,
        type= self._action.type,
        choices= self._action.choices,
        required= self._action.required,
        help= self._action.help,
        metavar= self._action.metavar)

  def __call__(self, parser, namespace, values, option_string=None):
    print "got here:", option_string, namespace
    setattr(namespace, 'key', option_string)
    self._action(parser, namespace, values, option_string)

A test:

parser = argparse.ArgumentParser(prog='PROG')
group = parser.add_mutually_exclusive_group()
group.add_argument('--foo', container=group, action=StoreOption, subaction='store_true')
group.add_argument('--bar', container=group, nargs=2, action=StoreOption, subaction='store')

print parser.parse_args(['--bar', 'asd', 'qwe'])
-- prints: Namespace(key='--bar', thing=['asd', 'qwe'])

Basically StoreOption is an Action which wraps another Action (the one specified by subaction ). When adding arguments you need to pass the container= parameter so that it can construct the sub-action. Also, there is some fiddling with the keyword arguments to set them up correctly for the sub-action.

Again, this has undergone very limited testing, so it may not work for all sub-actions, but it should point you in the right direction.

Subclassing Action is exactly what the developers expect you to do. Even the default, most common action, 'store' is a subclass, argparse._StoreAction . argparse._StoreTrueAction is a subclass of argparse._StoreConstAction , differing only in the default and const defaults.

To deal with the missing attribute case you could initialize them in a couple of ways:

set_defaults lets you define any defaults, regardless of whether any arguments use them or not. In the documentation that is used to add function objects to the namespace when using subparsers.

parser.set_defaults(thing_key=None, thing=None)

You could also create a namespace with the defaults, and pass that as an argument to parse_args .

myNamespace = argparse.Namespace(thing_key=None, thing=None)
parser.parse_args(names=myNamespace)

If you don't want a foo=None default in the namespace, define its default as argparse.SUPPRESS .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM