Skip to content

Commit fe05304

Browse files
gh-138525: Support single-dash long options and prefix_chars in BooleanOptionalAction
-no-foo is generated for -foo. ++no-foo is generated for ++foo. /no-foo is generated for /foo.
1 parent 5edfe55 commit fe05304

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
@@ -1428,8 +1428,18 @@ this API may be passed as the ``action`` parameter to
14281428
>>> parser.parse_args(['--no-foo'])
14291429
Namespace(foo=False)
14301430

1431+
Single-dash long options are also supported.
1432+
For example, negative option ``-no-foo`` is automatically added for
1433+
positive option``-foo``.
1434+
But no additional options are added for short options such as ``-f``.
1435+
14311436
.. versionadded:: 3.9
14321437

1438+
.. versionchanged:: next
1439+
Added support for single-dash options.
1440+
1441+
Added support for alternate prefix_chars_.
1442+
14331443

14341444
The parse_args() method
14351445
-----------------------

Doc/whatsnew/3.15.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,14 @@ New modules
283283
Improved modules
284284
================
285285

286+
argparse
287+
--------
288+
289+
* The :class:`~argparse.BooleanOptionalAction` action supports now single-dash
290+
long options and alternate prefix_chars_.
291+
(Contributed by Serhiy Storchaka in :gh:`138525`.)
292+
293+
286294
dbm
287295
---
288296

Lib/argparse.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -933,15 +933,24 @@ def __init__(self,
933933
deprecated=False):
934934

935935
_option_strings = []
936+
neg_option_strings = []
936937
for option_string in option_strings:
937938
_option_strings.append(option_string)
938939

939-
if option_string.startswith('--'):
940-
if option_string.startswith('--no-'):
940+
if len(option_string) > 2 and option_string[0] == option_string[1]:
941+
if option_string.startswith('no-', 2):
941942
raise ValueError(f'invalid option name {option_string!r} '
942943
f'for BooleanOptionalAction')
943-
option_string = '--no-' + option_string[2:]
944+
option_string = option_string[:2] + 'no-' + option_string[2:]
944945
_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+
if option_string.startswith('no-', 1):
949+
raise ValueError(f'invalid option name {option_string!r} '
950+
f'for BooleanOptionalAction')
951+
option_string = option_string[:1] + 'no-' + option_string[1:]
952+
_option_strings.append(option_string)
953+
neg_option_strings.append(option_string)
945954

946955
super().__init__(
947956
option_strings=_option_strings,
@@ -951,11 +960,12 @@ def __init__(self,
951960
required=required,
952961
help=help,
953962
deprecated=deprecated)
963+
self.neg_option_strings = neg_option_strings
954964

955965

956966
def __call__(self, parser, namespace, values, option_string=None):
957967
if option_string in self.option_strings:
958-
setattr(namespace, self.dest, not option_string.startswith('--no-'))
968+
setattr(namespace, self.dest, option_string not in self.neg_option_strings)
959969

960970
def format_usage(self):
961971
return ' | '.join(self.option_strings)

Lib/test/test_argparse.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,74 @@ def test_invalid_name(self):
795795
self.assertEqual(str(cm.exception),
796796
"invalid option name '--no-foo' for BooleanOptionalAction")
797797

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

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_chars_ in
2+
:class:`argparse.BooleanOptionalAction`.

0 commit comments

Comments
 (0)