В Python argparse възможно ли е да има сдвоени аргументи --no-something/--something?

Пиша програма, в която бих искал да има аргументи като този:

--[no-]foo   Do (or do not) foo. Default is do.

Има ли начин да накарам argparse да направи това вместо мен?

Използвам Python 3.2


person Omnifarious    schedule 10.02.2012    source източник
comment
Не. Префиксът no- е силно локализиран. Не е последователно на английски (un- също е доста често срещано.)   -  person S.Lott    schedule 11.02.2012
comment
Мисля, че трябва да го напишете сами. Иска ми се да го има вграден.   -  person jterrace    schedule 11.02.2012
comment
@S.Lott: Това е вярно. Тази програма обаче няма да има глобална аудитория. :-) И ако имаше такава възможност, очаквах префиксът да може да се персонализира по някакъв начин.   -  person Omnifarious    schedule 11.02.2012
comment
Проблемът не е глобален. Езикът е проблемът. За единия език, който познавам добре, има безброй нередности. Ето защо няма автоматична функция.   -  person S.Lott    schedule 11.02.2012
comment
@jterrace: Иска ми се _add_action API да е документиран и Action да е нещо повече от обикновен контейнер с атрибути.   -  person Omnifarious    schedule 11.02.2012


Отговори (7)


Модифицирах решението на @Omnifarious, за да го направя по-подобно на стандартните действия:

import argparse

class ActionNoYes(argparse.Action):
    def __init__(self, option_strings, dest, default=None, required=False, help=None):

        if default is None:
            raise ValueError('You must provide a default with Yes/No action')
        if len(option_strings)!=1:
            raise ValueError('Only single argument is allowed with YesNo action')
        opt = option_strings[0]
        if not opt.startswith('--'):
            raise ValueError('Yes/No arguments must be prefixed with --')

        opt = opt[2:]
        opts = ['--' + opt, '--no-' + opt]
        super(ActionNoYes, self).__init__(opts, dest, nargs=0, const=None, 
                                          default=default, required=required, help=help)
    def __call__(self, parser, namespace, values, option_strings=None):
        if option_strings.startswith('--no-'):
            setattr(namespace, self.dest, False)
        else:
            setattr(namespace, self.dest, True)

Можете да добавите аргумента Да/Не, както бихте добавили всяка стандартна опция. Просто трябва да предадете ActionNoYes клас в аргумента action:

parser = argparse.ArgumentParser()
parser.add_argument('--foo', action=ActionNoYes, default=False)

Сега, когато го наричате:

>> args = parser.parse_args(['--foo'])
Namespace(foo=True)
>> args = parser.parse_args(['--no-foo'])
Namespace(foo=False)
>> args = parser.parse_args([])
Namespace(foo=False)  
person btel    schedule 06.12.2013

Е, нито един от отговорите досега не е напълно задоволителен поради различни причини. И така, ето моят собствен отговор:

class ActionNoYes(argparse.Action):
    def __init__(self, opt_name, dest, default=True, required=False, help=None):
        super(ActionNoYes, self).__init__(['--' + opt_name, '--no-' + opt_name], dest, nargs=0, const=None, default=default, required=required, help=help)
    def __call__(self, parser, namespace, values, option_string=None):
        if option_string.starts_with('--no-'):
            setattr(namespace, self.dest, False)
        else:
            setattr(namespace, self.dest, True)

И пример за употреба:

>>> p = argparse.ArgumentParser()
>>> p._add_action(ActionNoYes('foo', 'foo', help="Do (or do not) foo. (default do)"))
ActionNoYes(option_strings=['--foo', '--no-foo'], dest='foo', nargs=0, const=None, default=True, type=None, choices=None, help='Do (or do not) foo. (default do)', metavar=None)
>>> p.parse_args(['--no-foo', '--foo', '--no-foo'])
Namespace(foo=False)
>>> p.print_help()
usage: -c [-h] [--foo]

optional arguments:
  -h, --help       show this help message and exit
  --foo, --no-foo  Do (or do not) foo. (default do)

За съжаление функцията член _add_action не е документирана, така че това не е „официално“ по отношение на поддръжката от API. Освен това Action е предимно клас притежател. Има много малко поведение самостоятелно. Би било хубаво, ако беше възможно да го използвате, за да персонализирате помощното съобщение малко повече. Например да кажете --[no-]foo в началото. Но тази част се генерира автоматично от неща извън класа Action.

person Omnifarious    schedule 10.02.2012
comment
Може да успеете да използвате опцията metavar по някакъв начин, за да получите --[no-]foo. - person Mad Physicist; 01.06.2016
comment
Не трябва ли да е startswith вместо starts_with? - person sauerburger; 31.05.2017
comment
Това е страхотно! Но защо се обадихте на ActionNoYes? Имах моментна грешка в мозъка :D - person corwin.amber; 29.01.2021
comment
@corwin.amber - Мислиш ли, че трябваше да е ActionYesNo? :-) Не помня защо избрах тази поръчка. Все пак беше преди 8 години. - person Omnifarious; 02.02.2021
comment
Да, LoL... Опитвах се да създам ActionYesNo в моя код, взирайки се в съобщението за грешка, без да виждам защо не може да го намери... може би това е само аз. Независимо от това, вашият отговор и адаптацията на @btel бяха изключително полезни. - person corwin.amber; 03.02.2021

add_mutually_exclusive_group() от argparse помага ли?

parser = argparse.ArgumentParser()
exclusive_grp = parser.add_mutually_exclusive_group()
exclusive_grp.add_argument('--foo', action='store_true', help='do foo')
exclusive_grp.add_argument('--no-foo', action='store_true', help='do not do foo')
args = parser.parse_args()

print 'Starting program', 'with' if args.foo else 'without', 'foo'
print 'Starting program', 'with' if args.no_foo else 'without', 'no_foo'

Ето как изглежда при стартиране:

./so.py --help
usage: so.py [-h] [--foo | --no-foo]

optional arguments:
  -h, --help  show this help message and exit
  --foo       do foo
  --no-foo    do not do foo

./so.py
Starting program without foo
Starting program without no_foo

./so.py --no-foo --foo
usage: so.py [-h] [--foo | --no-foo]
so.py: error: argument --foo: not allowed with argument --no-foo

Това е различно от следното във взаимно изключващата се група, която позволява нито една опция във вашата програма (и предполагам, че искате опции поради синтаксиса --). Това предполага едно или друго:

parser.add_argument('--foo=', choices=('y', 'n'), default='y',
                    help="Do foo? (default y)")

Ако те са задължителни (незадължителни), може би с помощта на add_subparsers() е това, което търсите.

Актуализация 1

Логически различно, но може би по-чисто:

...
exclusive_grp.add_argument('--foo', action='store_true', dest='foo', help='do foo')
exclusive_grp.add_argument('--no-foo', action='store_false', dest='foo', help='do not do foo')
args = parser.parse_args()

print 'Starting program', 'with' if args.foo else 'without', 'foo'

И го стартирате:

./so.py --foo
Starting program with foo
./so.py --no-foo
Starting program without foo
./so.py
Starting program without foo
person Zach Young    schedule 10.02.2012
comment
Можете ли да зададете action='store_false' за --no-foo и да зададете dest='foo' и за двете, така че да се показва в една променлива? - person jterrace; 11.02.2012
comment
@jterrace Да. Интересно предложение. Добавих актуализирано решение. - person Zach Young; 11.02.2012
comment
хубаво. можете да го увиете във функция като в отговора на @s-lott и би било наистина хубаво - person jterrace; 11.02.2012
comment
Това е добре, освен че помощта е малко ненужно многословна. Но поне нещото с групата аргументи поддържа свързаните аргументи свързани. - person Omnifarious; 11.02.2012
comment
Освен това не позволява '--foo' и '--no-foo' да бъдат посочени едновременно и последният зададен да има предимство. - person Omnifarious; 11.02.2012
comment
@Omnifarious True. Защо искате да позволите на потребителя да въведе и двете, а интерпретаторът (детерминистично) да избере кой да използва? - person Zach Young; 11.02.2012
comment
Това е предназначено за среди, в които аргументите могат да се предават през множество слоеве, като всеки от тях предварява своите собствени стойности по подразбиране. Както и да е, измислих добро решение, след като сам разглобих модула. - person Omnifarious; 11.02.2012

Напишете свой собствен подклас.

class MyArgParse(argparse.ArgumentParser):
    def magical_add_paired_arguments( self, *args, **kw ):
        self.add_argument( *args, **kw )
        self.add_argument( '--no'+args[0][2:], *args[1:], **kw )
person S.Lott    schedule 10.02.2012
comment
Хм... това е интересна идея. Има ли идея за „аргументен обект“, който може сам да анализира нещата и може би да генерира свое собствено помощно съобщение? Това наистина би свършило работа. - person Omnifarious; 11.02.2012
comment
@Omnifarious: генерира собствено помощно съобщение? Какво може да означава това? Какво не е наред с добавянето на още код, както е показано по-горе? Ако искате да се случат още по-магически неща, може да ви е по-лесно просто да прочетете източника на argparse сами и да видите как работи вътрешно. - person S.Lott; 11.02.2012
comment
Е, това е едно от големите предимства на argparse. Той генерира помощни съобщения и други неща за вас. add_argument може да се разглежда като функция, която конструира някакъв аргументен обект, който представя всички характеристики на аргумент... как да го анализирате, в коя променлива да го поставите, стойности по подразбиране, как да генерирате помощ, всички тези неща, и го поставя в хубав списък вътре в анализатора. Но си прав, трябва да се поровя във вътрешността сам и да видя дали мога да го свиря така, както искам. Ако не работи както си го представям, трябва. Много по-гъвкав е. - person Omnifarious; 11.02.2012
comment
add_argument може да се разглежда като функция? Това е метод, който конструира аргументен обект. Това всъщност прави. Не разбирам коментара. Какво казваш? - person S.Lott; 11.02.2012
comment
Така че работи така, както си го представям. Има „аргументен обект“, който е конструиран. Което означава, че можете да инстанциирате различен аргументен обект, който имплементира методите по различен начин. Може например просто да добави речник с всички стойности, дадени на метода add_argument към списък с тези речници. - person Omnifarious; 11.02.2012
comment
Ще го повторя, тъй като го пренебрегвате. може да ви е по-лесно просто да прочетете източника, за да направите argparse и да видите как работи вътрешно - person S.Lott; 11.02.2012
comment
Напълно разбрах какво точно искаш да направя. Така че го направих и намерих своя отговор. Добавих го като отговор на този въпрос. Надявах се някой да знае, без аз да разглобявам модула сам, но изглежда никой не го е разбрал. - person Omnifarious; 11.02.2012

За забавление, ето пълна реализация на отговора на S.Lott:

import argparse

class MyArgParse(argparse.ArgumentParser):
    def magical_add_paired_arguments( self, *args, **kw ):
        exclusive_grp = self.add_mutually_exclusive_group()
        exclusive_grp.add_argument( *args, **kw )
        new_action = 'store_false' if kw['action'] == 'store_true' else 'store_true'
        del kw['action']
        new_help = 'not({})'.format(kw['help'])
        del kw['help']
        exclusive_grp.add_argument( '--no-'+args[0][2:], *args[1:], 
                           action=new_action,
                           help=new_help, **kw )

parser = MyArgParse()
parser.magical_add_paired_arguments('--foo', action='store_true',
                                    dest='foo', help='do foo')
args = parser.parse_args()

print 'Starting program', 'with' if args.foo else 'without', 'foo'

Ето изхода:

./so.py --help
usage: so.py [-h] [--foo | --no-foo]

optional arguments:
  -h, --help  show this help message and exit
  --foo       do foo
  --no-foo    not(do foo)
person Zach Young    schedule 10.02.2012
comment
Това е много хубаво, но има няколко недостатъка. Първо, той позволява указване както на --foo, така и на --no-foo в командния ред и последният да има предимство. Второ, помощта е ненужно многословна, въпреки че взаимно изключващите се групови неща ги обединяват. Тръгнах по свой собствен път и описах подхода си в отговор на този въпрос. - person Omnifarious; 11.02.2012

Разширяване на отговора на https://stackoverflow.com/a/9236426/1695680

import argparse

class ActionFlagWithNo(argparse.Action):
    """
        Allows a 'no' prefix to disable store_true actions.
        For example, --debug will have an additional --no-debug to explicitly disable it.
    """
    def __init__(self, opt_name, dest=None, default=True, required=False, help=None):
        super(ActionFlagWithNo, self).__init__(
            [
                '--' + opt_name[0],
                '--no-' + opt_name[0],
            ] + opt_name[1:],
            dest=(opt_name[0].replace('-', '_') if dest is None else dest),
            nargs=0, const=None, default=default, required=required, help=help,
        )

    def __call__(self, parser, namespace, values, option_string=None):
        if option_string.startswith('--no-'):
            setattr(namespace, self.dest, False)
        else:
            setattr(namespace, self.dest, True)

class ActionFlagWithNoFormatter(argparse.HelpFormatter):
    """
        This changes the --help output, what is originally this:

            --file, --no-file, -f

        Will be condensed like this:

            --[no-]file, -f
    """

    def _format_action_invocation(self, action):
        if action.option_strings[1].startswith('--no-'):
            return ', '.join(
                [action.option_strings[0][:2] + '[no-]' + action.option_strings[0][2:]]
                + action.option_strings[2:]
            )
        return super(ActionFlagWithNoFormatter, self)._format_action_invocation(action)


def main(argp=None):
    if argp is None:
        argp = argparse.ArgumentParser(
            formatter_class=ActionFlagWithNoFormatter,
        )
        argp._add_action(ActionFlagWithNo(['flaga', '-a'], default=False, help='...'))
        argp._add_action(ActionFlagWithNo(['flabb', '-b'], default=False, help='...'))

        argp = argp.parse_args()

Това дава помощен изход по следния начин:

usage: myscript.py [-h] [--flaga] [--flabb]

optional arguments:
  -h, --help        show this help message and exit
  --[no-]flaga, -a  ...
  --[no-]flabb, -b  ...

Gist версия тук, заявките за изтегляне са добре дошли :) https://gist.github.com/thorsummoner/9850b5d6cd5e6bb5a3b9b7792b69b0a5

person ThorSummoner    schedule 22.04.2016

Преди да видя този въпрос и отговорите, написах своя собствена функция, за да се справя с това:

def on_off(item):
    return 'on' if item else 'off'

def argparse_add_toggle(parser, name, **kwargs):
    """Given a basename of an argument, add --name and --no-name to parser

    All standard ArgumentParser.add_argument parameters are supported
    and fed through to add_argument as is with the following exceptions:
    name     is used to generate both an on and an off
             switch: --<name>/--no-<name>
    help     by default is a simple 'Switch on/off <name>' text for the
             two options. If you provide it make sure it fits english
             language wise into the template
               'Switch on <help>. Default: <default>'
             If you need more control, use help_on and help_off
    help_on  Literally used to provide the help text for  --<name>
    help_off Literally used to provide the help text for  --no-<name>
    """
    default = bool(kwargs.pop('default', 0))
    dest = kwargs.pop('dest', name)
    help = kwargs.pop('help', name)
    help_on  = kwargs.pop('help_on',  'Switch on {}. Default: {}'.format(help, on_off(defaults)))
    help_off = kwargs.pop('help_off', 'Switch off {}.'.format(help))

    parser.add_argument('--' + name,    action='store_true',  dest=dest, default=default, help=help_on)
    parser.add_argument('--no-' + name, action='store_false', dest=dest, help=help_off)

Може да се използва така:

defaults = {
    'dry_run' : 0,
    }

parser = argparse.ArgumentParser(description="Fancy Script",
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
argparse_add_toggle(parser, 'dry_run', default=defaults['dry_run'],
                    help_on='No modifications on the filesystem. No jobs started.',
                    help_off='Normal operation')
parser.set_defaults(**defaults)

args = parser.parse_args()

Резултатът от помощта изглежда така:

  --dry_run             No modifications on the filesystem. No jobs started.
  --no-dry_run          Normal operation

Предпочитам подхода на подкласиране argparse.Action, който другите отговори предлагат пред моята проста функция, защото прави кода, който я използва, по-чист и по-лесен за четене.

Този код има предимството да има стандартна помощ по подразбиране, но също и help_on и help_off за преконфигуриране на доста глупавите настройки по подразбиране.

Може би някой може да се интегрира.

person cfi    schedule 01.11.2012