From 6a517c674907c195660fa9178a7b561de49cc721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Lapeyre?= Date: Fri, 13 Sep 2019 12:17:43 +0200 Subject: [PATCH] bpo-8538: Add support for boolean actions to argparse (GH-11478) Co-Authored-By: remilapeyre --- Doc/library/argparse.rst | 19 ++++++- Lib/argparse.py | 47 +++++++++++++++- Lib/test/test_argparse.py | 56 +++++++++++++++---- .../2019-01-09-16-18-52.bpo-8538.PfVZia.rst | 2 + 4 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2019-01-09-16-18-52.bpo-8538.PfVZia.rst diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 6dffd2e9325e11..a8aeca41a70a6c 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -839,9 +839,19 @@ how the command-line arguments should be handled. The supplied actions are: Namespace(foo=['f1', 'f2', 'f3', 'f4']) You may also specify an arbitrary action by passing an Action subclass or -other object that implements the same interface. The recommended way to do -this is to extend :class:`Action`, overriding the ``__call__`` method -and optionally the ``__init__`` method. +other object that implements the same interface. The ``BooleanOptionalAction`` +is available in ``argparse`` and adds support for boolean actions such as +``--foo`` and ``--no-foo``:: + + >>> import argparse + >>> parser = argparse.ArgumentParser() + >>> parser.add_argument('--foo', action=argparse.BooleanOptionalAction) + >>> parser.parse_args(['--no-foo']) + Namespace(foo=False) + +The recommended way to create a custom action is to extend :class:`Action`, +overriding the ``__call__`` method and optionally the ``__init__`` and +``format_usage`` methods. An example of a custom action:: @@ -1361,6 +1371,9 @@ Action instances should be callable, so subclasses must override the The ``__call__`` method may perform arbitrary actions, but will typically set attributes on the ``namespace`` based on ``dest`` and ``values``. +Action subclasses can define a ``format_usage`` method that takes no argument +and return a string which will be used when printing the usage of the program. +If such method is not provided, a sensible default will be used. The parse_args() method ----------------------- diff --git a/Lib/argparse.py b/Lib/argparse.py index 370692bd1bf4e7..13af7ac2392174 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -67,6 +67,7 @@ 'ArgumentParser', 'ArgumentError', 'ArgumentTypeError', + 'BooleanOptionalAction', 'FileType', 'HelpFormatter', 'ArgumentDefaultsHelpFormatter', @@ -454,7 +455,7 @@ def _format_actions_usage(self, actions, groups): # if the Optional doesn't take a value, format is: # -s or --long if action.nargs == 0: - part = '%s' % option_string + part = action.format_usage() # if the Optional takes a value, format is: # -s ARGS or --long ARGS @@ -842,9 +843,53 @@ def _get_kwargs(self): ] return [(name, getattr(self, name)) for name in names] + def format_usage(self): + return self.option_strings[0] + def __call__(self, parser, namespace, values, option_string=None): raise NotImplementedError(_('.__call__() not defined')) +class BooleanOptionalAction(Action): + def __init__(self, + option_strings, + dest, + const=None, + default=None, + type=None, + choices=None, + required=False, + help=None, + metavar=None): + + _option_strings = [] + for option_string in option_strings: + _option_strings.append(option_string) + + if option_string.startswith('--'): + option_string = '--no-' + option_string[2:] + _option_strings.append(option_string) + + if help is not None and default is not None: + help += f" (default: {default})" + + super().__init__( + option_strings=_option_strings, + dest=dest, + nargs=0, + default=default, + type=type, + choices=choices, + required=required, + help=help, + metavar=metavar) + + def __call__(self, parser, namespace, values, option_string=None): + if option_string in self.option_strings: + setattr(namespace, self.dest, not option_string.startswith('--no-')) + + def format_usage(self): + return ' | '.join(self.option_strings) + class _StoreAction(Action): diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 464db2906998c8..a97c921852c7bf 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -686,6 +686,30 @@ class TestOptionalsActionStoreTrue(ParserTestCase): ('--apple', NS(apple=True)), ] +class TestBooleanOptionalAction(ParserTestCase): + """Tests BooleanOptionalAction""" + + argument_signatures = [Sig('--foo', action=argparse.BooleanOptionalAction)] + failures = ['--foo bar', '--foo=bar'] + successes = [ + ('', NS(foo=None)), + ('--foo', NS(foo=True)), + ('--no-foo', NS(foo=False)), + ('--foo --no-foo', NS(foo=False)), # useful for aliases + ('--no-foo --foo', NS(foo=True)), + ] + +class TestBooleanOptionalActionRequired(ParserTestCase): + """Tests BooleanOptionalAction required""" + + argument_signatures = [ + Sig('--foo', required=True, action=argparse.BooleanOptionalAction) + ] + failures = [''] + successes = [ + ('--foo', NS(foo=True)), + ('--no-foo', NS(foo=False)), + ] class TestOptionalsActionAppend(ParserTestCase): """Tests the append action for an Optional""" @@ -3456,6 +3480,10 @@ class TestHelpUsage(HelpTestCase): Sig('a', help='a'), Sig('b', help='b', nargs=2), Sig('c', help='c', nargs='?'), + Sig('--foo', help='Whether to foo', action=argparse.BooleanOptionalAction), + Sig('--bar', help='Whether to bar', default=True, + action=argparse.BooleanOptionalAction), + Sig('-f', '--foobar', '--barfoo', action=argparse.BooleanOptionalAction), ] argument_group_signatures = [ (Sig('group'), [ @@ -3466,26 +3494,32 @@ class TestHelpUsage(HelpTestCase): ]) ] usage = '''\ - usage: PROG [-h] [-w W [W ...]] [-x [X [X ...]]] [-y [Y]] [-z Z Z Z] + usage: PROG [-h] [-w W [W ...]] [-x [X [X ...]]] [--foo | --no-foo] + [--bar | --no-bar] + [-f | --foobar | --no-foobar | --barfoo | --no-barfoo] [-y [Y]] + [-z Z Z Z] a b b [c] [d [d ...]] e [e ...] ''' help = usage + '''\ positional arguments: - a a - b b - c c + a a + b b + c c optional arguments: - -h, --help show this help message and exit - -w W [W ...] w - -x [X [X ...]] x + -h, --help show this help message and exit + -w W [W ...] w + -x [X [X ...]] x + --foo, --no-foo Whether to foo + --bar, --no-bar Whether to bar (default: True) + -f, --foobar, --no-foobar, --barfoo, --no-barfoo group: - -y [Y] y - -z Z Z Z z - d d - e e + -y [Y] y + -z Z Z Z z + d d + e e ''' version = '' diff --git a/Misc/NEWS.d/next/Library/2019-01-09-16-18-52.bpo-8538.PfVZia.rst b/Misc/NEWS.d/next/Library/2019-01-09-16-18-52.bpo-8538.PfVZia.rst new file mode 100644 index 00000000000000..94249ab1e43464 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-01-09-16-18-52.bpo-8538.PfVZia.rst @@ -0,0 +1,2 @@ +Add support for boolean actions like ``--foo`` and ``--no-foo`` to argparse. +Patch contributed by RĂ©mi Lapeyre.