From d5d92a1504ed34ff4d6f9f2abd04d4759bcd144f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Lapeyre?= Date: Wed, 9 Jan 2019 16:19:04 +0100 Subject: [PATCH 1/4] Add support for boolean actions to argparse --- Doc/library/argparse.rst | 19 ++++++-- Lib/argparse.py | 43 ++++++++++++++++++- Lib/test/test_argparse.py | 40 ++++++++++++----- .../2019-01-09-16-18-52.bpo-8538.PfVZia.rst | 2 + 4 files changed, 89 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 cef197f3055581..692499518c5bc0 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -798,9 +798,19 @@ how the command-line arguments should be handled. The supplied actions are: PROG 2.0 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 do 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:: @@ -1321,6 +1331,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 798766f6c4086a..7bf6bffdba5afd 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -66,6 +66,7 @@ 'ArgumentParser', 'ArgumentError', 'ArgumentTypeError', + 'BooleanOptionalAction', 'FileType', 'HelpFormatter', 'ArgumentDefaultsHelpFormatter', @@ -447,7 +448,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 @@ -832,9 +833,49 @@ 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 += [ + '--no-' + option_string.lstrip('--') + for option_string in option_strings + ] + + 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 c0c7cb05940ba9..4abb144cda5863 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -687,6 +687,18 @@ 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 TestOptionalsActionAppend(ParserTestCase): """Tests the append action for an Optional""" @@ -3375,6 +3387,9 @@ 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), ] argument_group_signatures = [ (Sig('group'), [ @@ -3385,26 +3400,29 @@ 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] [-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) 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. From 3567f326b705f984ce872f51bf01e63c21016710 Mon Sep 17 00:00:00 2001 From: Ken Williams Date: Thu, 10 Jan 2019 16:31:16 +0100 Subject: [PATCH 2/4] Fix doc typo in Doc/library/argparse.rst Co-Authored-By: remilapeyre --- Doc/library/argparse.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 692499518c5bc0..71fa1b6a690102 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -808,7 +808,7 @@ is available in ``argparse`` and adds support for boolean actions such as >>> parser.parse_args(['--no-foo']) Namespace(foo=False) -The recommended way to do create a custom action is to extend :class:`Action`, +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. From b0652c9da0ce0d8cb81698970deb006b00da6854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Lapeyre?= Date: Wed, 9 Jan 2019 17:14:14 +0100 Subject: [PATCH 3/4] Fix option ordering in help and short options for BooleanOptionalAction --- Lib/argparse.py | 14 ++++++++----- Lib/test/test_argparse.py | 44 +++++++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 7bf6bffdba5afd..ac9aba6b76df19 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -850,16 +850,20 @@ def __init__(self, required=False, help=None, metavar=None): - option_strings += [ - '--no-' + option_string.lstrip('--') - for option_string in option_strings - ] + + _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, + option_strings=_option_strings, dest=dest, nargs=0, default=default, diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 4abb144cda5863..d6cbb5888a4cc1 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -700,6 +700,18 @@ class TestBooleanOptionalAction(ParserTestCase): ('--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""" @@ -3380,6 +3392,8 @@ class TestHelpWrappingLongNames(HelpTestCase): class TestHelpUsage(HelpTestCase): """Test basic usage messages""" + maxDiff = None + parser_signature = Sig(prog='PROG') argument_signatures = [ Sig('-w', nargs='+', help='w'), @@ -3390,6 +3404,7 @@ class TestHelpUsage(HelpTestCase): 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'), [ @@ -3401,28 +3416,31 @@ class TestHelpUsage(HelpTestCase): ] usage = '''\ usage: PROG [-h] [-w W [W ...]] [-x [X [X ...]]] [--foo | --no-foo] - [--bar | --no-bar] [-y [Y]] [-z Z Z Z] + [--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 - --foo, --no-foo Whether to foo - --bar, --no-bar Whether to bar (default: True) + -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 = '' From 9d226ecec49e8774207c991893556821dacc6689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Wirtel?= Date: Fri, 13 Sep 2019 11:47:48 +0200 Subject: [PATCH 4/4] Remove useless maxDiff in test_argparse.py --- Lib/test/test_argparse.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index d6cbb5888a4cc1..86c5025bd622f3 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -3392,8 +3392,6 @@ class TestHelpWrappingLongNames(HelpTestCase): class TestHelpUsage(HelpTestCase): """Test basic usage messages""" - maxDiff = None - parser_signature = Sig(prog='PROG') argument_signatures = [ Sig('-w', nargs='+', help='w'),