Merge pull request #4592 from carlosmiei/argparse

feat(argparse): update to 3.11
This commit is contained in:
Jeong YunWon authored and GitHub committed 2023-03-01 03:52:03 +09:00
commit f2515405b5
2 files changed
+123 -45

No files matched your search

+53 -15
View File
@@ -89,6 +89,8 @@ import os as _os
import re as _re
import sys as _sys
import warnings
from gettext import gettext as _, ngettext
SUPPRESS = '==SUPPRESS=='
@@ -151,6 +153,7 @@ def _copy_items(items):
# Formatting Help
# ===============
class HelpFormatter(object):
"""Formatter for generating usage messages and argument help strings.
@@ -693,8 +696,19 @@ class ArgumentDefaultsHelpFormatter(HelpFormatter):
"""
def _get_help_string(self, action):
"""
Add the default value to the option help message.
ArgumentDefaultsHelpFormatter and BooleanOptionalAction when it isn't
already present. This code will do that, detecting cornercases to
prevent duplicates or cases where it wouldn't make sense to the end
user.
"""
help = action.help
if '%(default)' not in action.help:
if help is None:
help = ''
if '%(default)' not in help:
if action.default is not SUPPRESS:
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
if action.option_strings or action.nargs in defaulting_nargs:
@@ -702,6 +716,7 @@ class ArgumentDefaultsHelpFormatter(HelpFormatter):
return help
class MetavarTypeHelpFormatter(HelpFormatter):
"""Help message formatter which uses the argument 'type' as the default
metavar value (instead of the argument 'dest')
@@ -717,7 +732,6 @@ class MetavarTypeHelpFormatter(HelpFormatter):
return action.type.__name__
# =====================
# Options and Arguments
# =====================
@@ -752,7 +766,7 @@ class ArgumentError(Exception):
if self.argument_name is None:
format = '%(message)s'
else:
format = 'argument %(argument_name)s: %(message)s'
format = _('argument %(argument_name)s: %(message)s')
return format % dict(message=self.message,
argument_name=self.argument_name)
@@ -860,6 +874,7 @@ class Action(_AttributeHolder):
def __call__(self, parser, namespace, values, option_string=None):
raise NotImplementedError(_('.__call__() not defined'))
class BooleanOptionalAction(Action):
def __init__(self,
option_strings,
@@ -879,9 +894,6 @@ class BooleanOptionalAction(Action):
option_string = '--no-' + option_string[2:]
_option_strings.append(option_string)
if help is not None and default is not None and default is not SUPPRESS:
help += " (default: %(default)s)"
super().__init__(
option_strings=_option_strings,
dest=dest,
@@ -893,6 +905,7 @@ class BooleanOptionalAction(Action):
help=help,
metavar=metavar)
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-'))
@@ -941,7 +954,7 @@ class _StoreConstAction(Action):
def __init__(self,
option_strings,
dest,
const,
const=None,
default=None,
required=False,
help=None,
@@ -1036,7 +1049,7 @@ class _AppendConstAction(Action):
def __init__(self,
option_strings,
dest,
const,
const=None,
default=None,
required=False,
help=None,
@@ -1168,6 +1181,13 @@ class _SubParsersAction(Action):
aliases = kwargs.pop('aliases', ())
if name in self._name_parser_map:
raise ArgumentError(self, _('conflicting subparser: %s') % name)
for alias in aliases:
if alias in self._name_parser_map:
raise ArgumentError(
self, _('conflicting subparser alias: %s') % alias)
# create a pseudo-action to hold the choice help
if 'help' in kwargs:
help = kwargs.pop('help')
@@ -1648,6 +1668,14 @@ class _ArgumentGroup(_ActionsContainer):
super(_ArgumentGroup, self)._remove_action(action)
self._group_actions.remove(action)
def add_argument_group(self, *args, **kwargs):
warnings.warn(
"Nesting argument groups is deprecated.",
category=DeprecationWarning,
stacklevel=2
)
return super().add_argument_group(*args, **kwargs)
class _MutuallyExclusiveGroup(_ArgumentGroup):
@@ -1668,6 +1696,14 @@ class _MutuallyExclusiveGroup(_ArgumentGroup):
self._container._remove_action(action)
self._group_actions.remove(action)
def add_mutually_exclusive_group(self, *args, **kwargs):
warnings.warn(
"Nesting mutually exclusive groups is deprecated.",
category=DeprecationWarning,
stacklevel=2
)
return super().add_mutually_exclusive_group(*args, **kwargs)
class ArgumentParser(_AttributeHolder, _ActionsContainer):
"""Object for parsing command line strings into Python objects.
@@ -1857,8 +1893,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
if self.exit_on_error:
try:
namespace, args = self._parse_known_args(args, namespace)
except ArgumentError:
err = _sys.exc_info()[1]
except ArgumentError as err:
self.error(str(err))
else:
namespace, args = self._parse_known_args(args, namespace)
@@ -1962,7 +1997,11 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# arguments, try to parse more single-dash options out
# of the tail of the option string
chars = self.prefix_chars
if arg_count == 0 and option_string[1] not in chars:
if (
arg_count == 0
and option_string[1] not in chars
and explicit_arg != ''
):
action_tuples.append((action, [], option_string))
char = option_string[0]
option_string = char + explicit_arg[0]
@@ -2133,8 +2172,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
arg_strings.append(arg)
arg_strings = self._read_args_from_files(arg_strings)
new_arg_strings.extend(arg_strings)
except OSError:
err = _sys.exc_info()[1]
except OSError as err:
self.error(str(err))
# return the modified argument list
@@ -2484,9 +2522,9 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
result = type_func(arg_string)
# ArgumentTypeErrors indicate errors
except ArgumentTypeError:
except ArgumentTypeError as err:
name = getattr(action.type, '__name__', repr(action.type))
msg = str(_sys.exc_info()[1])
msg = str(err)
raise ArgumentError(action, msg)
# TypeErrors or ValueErrors also indicate errors
+70 -30
View File
@@ -11,6 +11,7 @@ import textwrap
import tempfile
import unittest
import argparse
import warnings
from test.support import os_helper
from unittest import mock
@@ -40,11 +41,11 @@ class TestCase(unittest.TestCase):
# The tests assume that line wrapping occurs at 80 columns, but this
# behaviour can be overridden by setting the COLUMNS environment
# variable. To ensure that this width is used, set COLUMNS to 80.
env = os_helper.EnvironmentVarGuard()
env = self.enterContext(os_helper.EnvironmentVarGuard())
env['COLUMNS'] = '80'
self.addCleanup(env.__exit__)
@os_helper.skip_unless_working_chmod
class TempDirMixin(object):
def setUp(self):
@@ -295,7 +296,7 @@ class TestOptionalsSingleDashCombined(ParserTestCase):
Sig('-z'),
]
failures = ['a', '--foo', '-xa', '-x --foo', '-x -z', '-z -x',
'-yx', '-yz a', '-yyyx', '-yyyza', '-xyza']
'-yx', '-yz a', '-yyyx', '-yyyza', '-xyza', '-x=']
successes = [
('', NS(x=False, yyy=None, z=None)),
('-x', NS(x=True, yyy=None, z=None)),
@@ -769,6 +770,25 @@ class TestOptionalsActionAppendWithDefault(ParserTestCase):
]
class TestConstActionsMissingConstKwarg(ParserTestCase):
"""Tests that const gets default value of None when not provided"""
argument_signatures = [
Sig('-f', action='append_const'),
Sig('--foo', action='append_const'),
Sig('-b', action='store_const'),
Sig('--bar', action='store_const')
]
failures = ['-f v', '--foo=bar', '--foo bar']
successes = [
('', NS(f=None, foo=None, b=None, bar=None)),
('-f', NS(f=[None], foo=None, b=None, bar=None)),
('--foo', NS(f=None, foo=[None], b=None, bar=None)),
('-b', NS(f=None, foo=None, b=None, bar=None)),
('--bar', NS(f=None, foo=None, b=None, bar=None)),
]
class TestOptionalsActionAppendConst(ParserTestCase):
"""Tests the append_const action for an Optional"""
@@ -1703,8 +1723,7 @@ class WFile(object):
return self.name == other.name
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
"non-root user required")
@os_helper.skip_if_dac_override
class TestFileTypeW(TempDirMixin, ParserTestCase):
"""Test the FileType option/argument type for writing files"""
@@ -1726,8 +1745,8 @@ class TestFileTypeW(TempDirMixin, ParserTestCase):
('-x - -', NS(x=eq_stdout, spam=eq_stdout)),
]
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
"non-root user required")
@os_helper.skip_if_dac_override
class TestFileTypeX(TempDirMixin, ParserTestCase):
"""Test the FileType option/argument type for writing new files only"""
@@ -1747,8 +1766,7 @@ class TestFileTypeX(TempDirMixin, ParserTestCase):
]
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
"non-root user required")
@os_helper.skip_if_dac_override
class TestFileTypeWB(TempDirMixin, ParserTestCase):
"""Test the FileType option/argument type for writing binary files"""
@@ -1765,8 +1783,7 @@ class TestFileTypeWB(TempDirMixin, ParserTestCase):
]
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
"non-root user required")
@os_helper.skip_if_dac_override
class TestFileTypeXB(TestFileTypeX):
"Test the FileType option/argument type for writing new binary files only"
@@ -2245,8 +2262,7 @@ class TestAddSubparsers(TestCase):
main description
positional arguments:
foo
foo \n
options:
-h, --help show this help message and exit
'''))
@@ -2262,8 +2278,7 @@ class TestAddSubparsers(TestCase):
main description
positional arguments:
{}
{} \n
options:
-h, --help show this help message and exit
'''))
@@ -3041,15 +3056,24 @@ class TestMutuallyExclusiveOptionalsAndPositionalsMixed(MEMixin, TestCase):
class TestMutuallyExclusiveNested(MEMixin, TestCase):
# Nesting mutually exclusive groups is an undocumented feature
# that came about by accident through inheritance and has been
# the source of many bugs. It is deprecated and this test should
# eventually be removed along with it.
def get_parser(self, required):
parser = ErrorRaisingArgumentParser(prog='PROG')
group = parser.add_mutually_exclusive_group(required=required)
group.add_argument('-a')
group.add_argument('-b')
group2 = group.add_mutually_exclusive_group(required=required)
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
group2 = group.add_mutually_exclusive_group(required=required)
group2.add_argument('-c')
group2.add_argument('-d')
group3 = group2.add_mutually_exclusive_group(required=required)
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
group3 = group2.add_mutually_exclusive_group(required=required)
group3.add_argument('-e')
group3.add_argument('-f')
return parser
@@ -3321,6 +3345,7 @@ class TestHelpFormattingMetaclass(type):
def _test(self, tester, parser_text):
expected_text = getattr(tester, self.func_suffix)
expected_text = textwrap.dedent(expected_text)
tester.maxDiff = None
tester.assertEqual(expected_text, parser_text)
def test_format(self, tester):
@@ -3400,9 +3425,8 @@ class TestShortColumns(HelpTestCase):
but we don't want any exceptions thrown in such cases. Only ugly representation.
'''
def setUp(self):
env = os_helper.EnvironmentVarGuard()
env = self.enterContext(os_helper.EnvironmentVarGuard())
env.set("COLUMNS", '15')
self.addCleanup(env.__exit__)
parser_signature = TestHelpBiggerOptionals.parser_signature
argument_signatures = TestHelpBiggerOptionals.argument_signatures
@@ -3716,7 +3740,7 @@ class TestHelpUsage(HelpTestCase):
-w W [W ...] w
-x [X ...] x
--foo, --no-foo Whether to foo
--bar, --no-bar Whether to bar (default: True)
--bar, --no-bar Whether to bar
-f, --foobar, --no-foobar, --barfoo, --no-barfoo
--bazz, --no-bazz Bazz!
@@ -4396,6 +4420,8 @@ class TestHelpArgumentDefaults(HelpTestCase):
Sig('--bar', action='store_true', help='bar help'),
Sig('--taz', action=argparse.BooleanOptionalAction,
help='Whether to taz it', default=True),
Sig('--corge', action=argparse.BooleanOptionalAction,
help='Whether to corge it', default=argparse.SUPPRESS),
Sig('--quux', help="Set the quux", default=42),
Sig('spam', help='spam help'),
Sig('badger', nargs='?', default='wooden', help='badger help'),
@@ -4405,8 +4431,8 @@ class TestHelpArgumentDefaults(HelpTestCase):
[Sig('--baz', type=int, default=42, help='baz help')]),
]
usage = '''\
usage: PROG [-h] [--foo FOO] [--bar] [--taz | --no-taz] [--quux QUUX]
[--baz BAZ]
usage: PROG [-h] [--foo FOO] [--bar] [--taz | --no-taz] [--corge | --no-corge]
[--quux QUUX] [--baz BAZ]
spam [badger]
'''
help = usage + '''\
@@ -4414,20 +4440,21 @@ class TestHelpArgumentDefaults(HelpTestCase):
description
positional arguments:
spam spam help
badger badger help (default: wooden)
spam spam help
badger badger help (default: wooden)
options:
-h, --help show this help message and exit
--foo FOO foo help - oh and by the way, None
--bar bar help (default: False)
--taz, --no-taz Whether to taz it (default: True)
--quux QUUX Set the quux (default: 42)
-h, --help show this help message and exit
--foo FOO foo help - oh and by the way, None
--bar bar help (default: False)
--taz, --no-taz Whether to taz it (default: True)
--corge, --no-corge Whether to corge it
--quux QUUX Set the quux (default: 42)
title:
description
--baz BAZ baz help (default: 42)
--baz BAZ baz help (default: 42)
'''
version = ''
@@ -4777,6 +4804,19 @@ class TestConflictHandling(TestCase):
--spam NEW_SPAM
'''))
def test_subparser_conflict(self):
parser = argparse.ArgumentParser()
sp = parser.add_subparsers()
sp.add_parser('fullname', aliases=['alias'])
self.assertRaises(argparse.ArgumentError,
sp.add_parser, 'fullname')
self.assertRaises(argparse.ArgumentError,
sp.add_parser, 'alias')
self.assertRaises(argparse.ArgumentError,
sp.add_parser, 'other', aliases=['fullname'])
self.assertRaises(argparse.ArgumentError,
sp.add_parser, 'other', aliases=['alias'])
# =============================
# Help and Version option tests