gh-138525: Support single-dash long options and prefix_chars in BooleanOptionalAction (GH-138692)

-nofoo is generated for -foo.
++no-foo is generated for ++foo.
/nofoo is generated for /foo.
This commit is contained in:
Serhiy Storchaka 2025-11-22 22:54:02 +02:00 committed by GitHub
parent cde19e565c
commit 425fd85ca3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 102 additions and 4 deletions

View file

@ -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
-----------------------

View file

@ -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`.)

View file

@ -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,
@ -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)

View file

@ -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"""

View file

@ -0,0 +1,2 @@
Add support for single-dash long options and alternate prefix characters in
:class:`argparse.BooleanOptionalAction`.