В 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
@jтерраса Да. Интересное предложение. Я добавил обновленное решение. - 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
@Всезнающая правда. Почему вы хотите, чтобы пользователь ввел оба, а интерпретатор (детерминистически) выбрал, какой из них использовать? - 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
Я повторю это, так как вы игнорируете это. вам может быть проще просто прочитать исходный код, чтобы аргументировать себя и посмотреть, как он работает внутри - person S.Lott; 11.02.2012
comment
Я полностью понял, о чем вы меня просили. Так я сделал это и нашел свой ответ. Я добавил это как ответ на этот вопрос. Я надеялся, что кто-нибудь узнает, не разбирая модуль самостоятельно, но, похоже, никто этого не сделал. - person Omnifarious; 11.02.2012

Ради интереса вот полная реализация ответа С.Лотта:

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-версия здесь, запросы на включение приветствуются :) а>

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