Skip to content

Commit 425fd85

Browse files
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.
1 parent cde19e5 commit 425fd85

File tree

5 files changed

+102
-4
lines changed

5 files changed

+102
-4
lines changed

Doc/library/argparse.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,8 +1445,18 @@ this API may be passed as the ``action`` parameter to
14451445
>>> parser.parse_args(['--no-foo'])
14461446
Namespace(foo=False)
14471447

1448+
Single-dash long options are also supported.
1449+
For example, negative option ``-nofoo`` is automatically added for
1450+
positive option ``-foo``.
1451+
But no additional options are added for short options such as ``-f``.
1452+
14481453
.. versionadded:: 3.9
14491454

1455+
.. versionchanged:: next
1456+
Added support for single-dash options.
1457+
1458+
Added support for alternate prefix_chars_.
1459+
14501460

14511461
The parse_args() method
14521462
-----------------------

Doc/whatsnew/3.15.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,10 @@ Improved modules
416416
argparse
417417
--------
418418

419+
* The :class:`~argparse.BooleanOptionalAction` action supports now single-dash
420+
long options and alternate prefix characters.
421+
(Contributed by Serhiy Storchaka in :gh:`138525`.)
422+
419423
* Changed the *suggest_on_error* parameter of :class:`argparse.ArgumentParser` to
420424
default to ``True``. This enables suggestions for mistyped arguments by default.
421425
(Contributed by Jakob Schluse in :gh:`140450`.)

Lib/argparse.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -932,15 +932,26 @@ def __init__(self,
932932
deprecated=False):
933933

934934
_option_strings = []
935+
neg_option_strings = []
935936
for option_string in option_strings:
936937
_option_strings.append(option_string)
937938

938-
if option_string.startswith('--'):
939-
if option_string.startswith('--no-'):
939+
if len(option_string) > 2 and option_string[0] == option_string[1]:
940+
# two-dash long option: '--foo' -> '--no-foo'
941+
if option_string.startswith('no-', 2):
940942
raise ValueError(f'invalid option name {option_string!r} '
941943
f'for BooleanOptionalAction')
942-
option_string = '--no-' + option_string[2:]
944+
option_string = option_string[:2] + 'no-' + option_string[2:]
943945
_option_strings.append(option_string)
946+
neg_option_strings.append(option_string)
947+
elif len(option_string) > 2 and option_string[0] != option_string[1]:
948+
# single-dash long option: '-foo' -> '-nofoo'
949+
if option_string.startswith('no', 1):
950+
raise ValueError(f'invalid option name {option_string!r} '
951+
f'for BooleanOptionalAction')
952+
option_string = option_string[:1] + 'no' + option_string[1:]
953+
_option_strings.append(option_string)
954+
neg_option_strings.append(option_string)
944955

945956
super().__init__(
946957
option_strings=_option_strings,
@@ -950,11 +961,12 @@ def __init__(self,
950961
required=required,
951962
help=help,
952963
deprecated=deprecated)
964+
self.neg_option_strings = neg_option_strings
953965

954966

955967
def __call__(self, parser, namespace, values, option_string=None):
956968
if option_string in self.option_strings:
957-
setattr(namespace, self.dest, not option_string.startswith('--no-'))
969+
setattr(namespace, self.dest, option_string not in self.neg_option_strings)
958970

959971
def format_usage(self):
960972
return ' | '.join(self.option_strings)

Lib/test/test_argparse.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,76 @@ def test_invalid_name(self):
805805
self.assertEqual(str(cm.exception),
806806
"invalid option name '--no-foo' for BooleanOptionalAction")
807807

808+
class TestBooleanOptionalActionSingleDash(ParserTestCase):
809+
"""Tests BooleanOptionalAction with single dash"""
810+
811+
argument_signatures = [
812+
Sig('-foo', '-x', action=argparse.BooleanOptionalAction),
813+
]
814+
failures = ['--foo', '--no-foo', '-no-foo', '-no-x', '-nox']
815+
successes = [
816+
('', NS(foo=None)),
817+
('-foo', NS(foo=True)),
818+
('-nofoo', NS(foo=False)),
819+
('-x', NS(foo=True)),
820+
]
821+
822+
def test_invalid_name(self):
823+
parser = argparse.ArgumentParser()
824+
with self.assertRaises(ValueError) as cm:
825+
parser.add_argument('-nofoo', action=argparse.BooleanOptionalAction)
826+
self.assertEqual(str(cm.exception),
827+
"invalid option name '-nofoo' for BooleanOptionalAction")
828+
829+
class TestBooleanOptionalActionAlternatePrefixChars(ParserTestCase):
830+
"""Tests BooleanOptionalAction with custom prefixes"""
831+
832+
parser_signature = Sig(prefix_chars='+-', add_help=False)
833+
argument_signatures = [Sig('++foo', action=argparse.BooleanOptionalAction)]
834+
failures = ['--foo', '--no-foo']
835+
successes = [
836+
('', NS(foo=None)),
837+
('++foo', NS(foo=True)),
838+
('++no-foo', NS(foo=False)),
839+
]
840+
841+
def test_invalid_name(self):
842+
parser = argparse.ArgumentParser(prefix_chars='+/')
843+
with self.assertRaisesRegex(ValueError,
844+
'BooleanOptionalAction.*is not valid for positional arguments'):
845+
parser.add_argument('--foo', action=argparse.BooleanOptionalAction)
846+
with self.assertRaises(ValueError) as cm:
847+
parser.add_argument('++no-foo', action=argparse.BooleanOptionalAction)
848+
self.assertEqual(str(cm.exception),
849+
"invalid option name '++no-foo' for BooleanOptionalAction")
850+
851+
class TestBooleanOptionalActionSingleAlternatePrefixChar(ParserTestCase):
852+
"""Tests BooleanOptionalAction with single alternate prefix char"""
853+
854+
parser_signature = Sig(prefix_chars='+/', add_help=False)
855+
argument_signatures = [
856+
Sig('+foo', '+x', action=argparse.BooleanOptionalAction),
857+
]
858+
failures = ['++foo', '++no-foo', '++nofoo',
859+
'-no-foo', '-nofoo', '+no-foo', '-nofoo',
860+
'+no-x', '+nox', '-no-x', '-nox']
861+
successes = [
862+
('', NS(foo=None)),
863+
('+foo', NS(foo=True)),
864+
('+nofoo', NS(foo=False)),
865+
('+x', NS(foo=True)),
866+
]
867+
868+
def test_invalid_name(self):
869+
parser = argparse.ArgumentParser(prefix_chars='+/')
870+
with self.assertRaisesRegex(ValueError,
871+
'BooleanOptionalAction.*is not valid for positional arguments'):
872+
parser.add_argument('-foo', action=argparse.BooleanOptionalAction)
873+
with self.assertRaises(ValueError) as cm:
874+
parser.add_argument('+nofoo', action=argparse.BooleanOptionalAction)
875+
self.assertEqual(str(cm.exception),
876+
"invalid option name '+nofoo' for BooleanOptionalAction")
877+
808878
class TestBooleanOptionalActionRequired(ParserTestCase):
809879
"""Tests BooleanOptionalAction required"""
810880

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add support for single-dash long options and alternate prefix characters in
2+
:class:`argparse.BooleanOptionalAction`.

0 commit comments

Comments
 (0)