]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-138525: Support single-dash long options and prefix_chars in BooleanOptionalAction...
authorSerhiy Storchaka <storchaka@gmail.com>
Sat, 22 Nov 2025 20:54:02 +0000 (22:54 +0200)
committerGitHub <noreply@github.com>
Sat, 22 Nov 2025 20:54:02 +0000 (20:54 +0000)
-nofoo is generated for -foo.
++no-foo is generated for ++foo.
/nofoo is generated for /foo.

Doc/library/argparse.rst
Doc/whatsnew/3.15.rst
Lib/argparse.py
Lib/test/test_argparse.py
Misc/NEWS.d/next/Library/2025-09-09-10-13-24.gh-issue-138525.hDTaAM.rst [new file with mode: 0644]

index 30ddd849f3a2efefac81528c5adb742a5c931066..2a39f248651936417d900ace705cdb7862468d13 100644 (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
 -----------------------
index 8991584a9f22dde9e3ee53735c92c6844f837b35..4882ddb4310fc21b4d0cf37d3f4f1e110bc96b06 100644 (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`.)
index 5003927cb30485230a2f60cebd3e8533ff5a26cf..02a17d93bdfcf5fc9e1e41ff6314c132a278dca7 100644 (file)
@@ -932,15 +932,26 @@ class BooleanOptionalAction(Action):
                  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 @@ class BooleanOptionalAction(Action):
             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)
index b93502a74596df5a9ee3f501d415e809cd04301f..8af51b1fc6eb2677e74b942196b69102078d93e5 100644 (file)
@@ -805,6 +805,76 @@ class TestBooleanOptionalAction(ParserTestCase):
         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"""
 
diff --git a/Misc/NEWS.d/next/Library/2025-09-09-10-13-24.gh-issue-138525.hDTaAM.rst b/Misc/NEWS.d/next/Library/2025-09-09-10-13-24.gh-issue-138525.hDTaAM.rst
new file mode 100644 (file)
index 0000000..c4cea4b
--- /dev/null
@@ -0,0 +1,2 @@
+Add support for single-dash long options and alternate prefix characters in
+:class:`argparse.BooleanOptionalAction`.