Skip to content
Merged
10 changes: 10 additions & 0 deletions Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1445,8 +1445,18 @@ this API may be passed as the ``action`` parameter to
>>> parser.parse_args(['--no-foo'])
Namespace(foo=False)

Single-dash long options are also supported.
For example, negative option ``-nofoo`` is automatically added for
positive option ``-foo``.
But no additional options are added for short options such as ``-f``.

.. versionadded:: 3.9

.. versionchanged:: next
Added support for single-dash options.

Added support for alternate prefix_chars_.


The parse_args() method
-----------------------
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,10 @@ Improved modules
argparse
--------

* The :class:`~argparse.BooleanOptionalAction` action supports now single-dash
long options and alternate prefix characters.
(Contributed by Serhiy Storchaka in :gh:`138525`.)

* Changed the *suggest_on_error* parameter of :class:`argparse.ArgumentParser` to
default to ``True``. This enables suggestions for mistyped arguments by default.
(Contributed by Jakob Schluse in :gh:`140450`.)
Expand Down
20 changes: 16 additions & 4 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,15 +932,26 @@ def __init__(self,
deprecated=False):

_option_strings = []
neg_option_strings = []
for option_string in option_strings:
_option_strings.append(option_string)

if option_string.startswith('--'):
if option_string.startswith('--no-'):
if len(option_string) > 2 and option_string[0] == option_string[1]:
# two-dash long option: '--foo' -> '--no-foo'
if option_string.startswith('no-', 2):
raise ValueError(f'invalid option name {option_string!r} '
f'for BooleanOptionalAction')
option_string = '--no-' + option_string[2:]
option_string = option_string[:2] + 'no-' + option_string[2:]
_option_strings.append(option_string)
neg_option_strings.append(option_string)
elif len(option_string) > 2 and option_string[0] != option_string[1]:
# single-dash long option: '-foo' -> '-nofoo'
if option_string.startswith('no', 1):
raise ValueError(f'invalid option name {option_string!r} '
f'for BooleanOptionalAction')
option_string = option_string[:1] + 'no' + option_string[1:]
_option_strings.append(option_string)
neg_option_strings.append(option_string)

super().__init__(
option_strings=_option_strings,
Expand All @@ -950,11 +961,12 @@ def __init__(self,
required=required,
help=help,
deprecated=deprecated)
self.neg_option_strings = neg_option_strings


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-'))
setattr(namespace, self.dest, option_string not in self.neg_option_strings)

def format_usage(self):
return ' | '.join(self.option_strings)
Expand Down
70 changes: 70 additions & 0 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,76 @@ def test_invalid_name(self):
self.assertEqual(str(cm.exception),
"invalid option name '--no-foo' for BooleanOptionalAction")

class TestBooleanOptionalActionSingleDash(ParserTestCase):
"""Tests BooleanOptionalAction with single dash"""

argument_signatures = [
Sig('-foo', '-x', action=argparse.BooleanOptionalAction),
]
failures = ['--foo', '--no-foo', '-no-foo', '-no-x', '-nox']
successes = [
('', NS(foo=None)),
('-foo', NS(foo=True)),
('-nofoo', NS(foo=False)),
('-x', NS(foo=True)),
]

def test_invalid_name(self):
parser = argparse.ArgumentParser()
with self.assertRaises(ValueError) as cm:
parser.add_argument('-nofoo', action=argparse.BooleanOptionalAction)
self.assertEqual(str(cm.exception),
"invalid option name '-nofoo' for BooleanOptionalAction")

class TestBooleanOptionalActionAlternatePrefixChars(ParserTestCase):
"""Tests BooleanOptionalAction with custom prefixes"""

parser_signature = Sig(prefix_chars='+-', add_help=False)
argument_signatures = [Sig('++foo', action=argparse.BooleanOptionalAction)]
failures = ['--foo', '--no-foo']
successes = [
('', NS(foo=None)),
('++foo', NS(foo=True)),
('++no-foo', NS(foo=False)),
]

def test_invalid_name(self):
parser = argparse.ArgumentParser(prefix_chars='+/')
with self.assertRaisesRegex(ValueError,
'BooleanOptionalAction.*is not valid for positional arguments'):
parser.add_argument('--foo', action=argparse.BooleanOptionalAction)
with self.assertRaises(ValueError) as cm:
parser.add_argument('++no-foo', action=argparse.BooleanOptionalAction)
self.assertEqual(str(cm.exception),
"invalid option name '++no-foo' for BooleanOptionalAction")

class TestBooleanOptionalActionSingleAlternatePrefixChar(ParserTestCase):
"""Tests BooleanOptionalAction with single alternate prefix char"""

parser_signature = Sig(prefix_chars='+/', add_help=False)
argument_signatures = [
Sig('+foo', '+x', action=argparse.BooleanOptionalAction),
]
failures = ['++foo', '++no-foo', '++nofoo',
'-no-foo', '-nofoo', '+no-foo', '-nofoo',
'+no-x', '+nox', '-no-x', '-nox']
successes = [
('', NS(foo=None)),
('+foo', NS(foo=True)),
('+nofoo', NS(foo=False)),
('+x', NS(foo=True)),
]

def test_invalid_name(self):
parser = argparse.ArgumentParser(prefix_chars='+/')
with self.assertRaisesRegex(ValueError,
'BooleanOptionalAction.*is not valid for positional arguments'):
parser.add_argument('-foo', action=argparse.BooleanOptionalAction)
with self.assertRaises(ValueError) as cm:
parser.add_argument('+nofoo', action=argparse.BooleanOptionalAction)
self.assertEqual(str(cm.exception),
"invalid option name '+nofoo' for BooleanOptionalAction")

class TestBooleanOptionalActionRequired(ParserTestCase):
"""Tests BooleanOptionalAction required"""

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add support for single-dash long options and alternate prefix characters in
:class:`argparse.BooleanOptionalAction`.
Loading