mirror of
https://github.com/python/cpython.git
synced 2025-08-30 21:48:47 +00:00
gh-62090: Simplify argparse usage formatting (GH-105039)
Rationale ========= argparse performs a complex formatting of the usage for argument grouping and for line wrapping to fit the terminal width. This formatting has been a constant source of bugs for at least 10 years (see linked issues below) where defensive assertion errors are triggered or brackets and paranthesis are not properly handeled. Problem ======= The current implementation of argparse usage formatting relies on regular expressions to group arguments usage only to separate them again later with another set of regular expressions. This is a complex and error prone approach that caused all the issues linked below. Special casing certain argument formats has not solved the problem. The following are some of the most common issues: - empty `metavar` - mutually exclusive groups with `SUPPRESS`ed arguments - metavars with whitespace - metavars with brackets or paranthesis Solution ======== The following two comments summarize the solution: - https://github.com/python/cpython/issues/82091#issuecomment-1093832187 - https://github.com/python/cpython/issues/77048#issuecomment-1093776995 Mainly, the solution is to rewrite the usage formatting to avoid the group-then-separate approach. Instead, the usage parts are kept separate and only joined together at the end. This allows for a much simpler implementation that is easier to understand and maintain. It avoids the regular expressions approach and fixes the corresponding issues. This closes the following GitHub issues: - #62090 - #62549 - #77048 - #82091 - #89743 - #96310 - #98666 These PRs become obsolete: - #15372 - #96311
This commit is contained in:
parent
49258efada
commit
de1428f8c2
3 changed files with 164 additions and 72 deletions
|
@ -4255,6 +4255,140 @@ class TestHelpUsagePositionalsOnlyWrap(HelpTestCase):
|
|||
version = ''
|
||||
|
||||
|
||||
class TestHelpUsageMetavarsSpacesParentheses(HelpTestCase):
|
||||
# https://github.com/python/cpython/issues/62549
|
||||
# https://github.com/python/cpython/issues/89743
|
||||
parser_signature = Sig(prog='PROG')
|
||||
argument_signatures = [
|
||||
Sig('-n1', metavar='()', help='n1'),
|
||||
Sig('-o1', metavar='(1, 2)', help='o1'),
|
||||
Sig('-u1', metavar=' (uu) ', help='u1'),
|
||||
Sig('-v1', metavar='( vv )', help='v1'),
|
||||
Sig('-w1', metavar='(w)w', help='w1'),
|
||||
Sig('-x1', metavar='x(x)', help='x1'),
|
||||
Sig('-y1', metavar='yy)', help='y1'),
|
||||
Sig('-z1', metavar='(zz', help='z1'),
|
||||
Sig('-n2', metavar='[]', help='n2'),
|
||||
Sig('-o2', metavar='[1, 2]', help='o2'),
|
||||
Sig('-u2', metavar=' [uu] ', help='u2'),
|
||||
Sig('-v2', metavar='[ vv ]', help='v2'),
|
||||
Sig('-w2', metavar='[w]w', help='w2'),
|
||||
Sig('-x2', metavar='x[x]', help='x2'),
|
||||
Sig('-y2', metavar='yy]', help='y2'),
|
||||
Sig('-z2', metavar='[zz', help='z2'),
|
||||
]
|
||||
|
||||
usage = '''\
|
||||
usage: PROG [-h] [-n1 ()] [-o1 (1, 2)] [-u1 (uu) ] [-v1 ( vv )] [-w1 (w)w]
|
||||
[-x1 x(x)] [-y1 yy)] [-z1 (zz] [-n2 []] [-o2 [1, 2]] [-u2 [uu] ]
|
||||
[-v2 [ vv ]] [-w2 [w]w] [-x2 x[x]] [-y2 yy]] [-z2 [zz]
|
||||
'''
|
||||
help = usage + '''\
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-n1 () n1
|
||||
-o1 (1, 2) o1
|
||||
-u1 (uu) u1
|
||||
-v1 ( vv ) v1
|
||||
-w1 (w)w w1
|
||||
-x1 x(x) x1
|
||||
-y1 yy) y1
|
||||
-z1 (zz z1
|
||||
-n2 [] n2
|
||||
-o2 [1, 2] o2
|
||||
-u2 [uu] u2
|
||||
-v2 [ vv ] v2
|
||||
-w2 [w]w w2
|
||||
-x2 x[x] x2
|
||||
-y2 yy] y2
|
||||
-z2 [zz z2
|
||||
'''
|
||||
version = ''
|
||||
|
||||
|
||||
class TestHelpUsageNoWhitespaceCrash(TestCase):
|
||||
|
||||
def test_all_suppressed_mutex_followed_by_long_arg(self):
|
||||
# https://github.com/python/cpython/issues/62090
|
||||
# https://github.com/python/cpython/issues/96310
|
||||
parser = argparse.ArgumentParser(prog='PROG')
|
||||
mutex = parser.add_mutually_exclusive_group()
|
||||
mutex.add_argument('--spam', help=argparse.SUPPRESS)
|
||||
parser.add_argument('--eggs-eggs-eggs-eggs-eggs-eggs')
|
||||
usage = textwrap.dedent('''\
|
||||
usage: PROG [-h]
|
||||
[--eggs-eggs-eggs-eggs-eggs-eggs EGGS_EGGS_EGGS_EGGS_EGGS_EGGS]
|
||||
''')
|
||||
self.assertEqual(parser.format_usage(), usage)
|
||||
|
||||
def test_newline_in_metavar(self):
|
||||
# https://github.com/python/cpython/issues/77048
|
||||
mapping = ['123456', '12345', '12345', '123']
|
||||
parser = argparse.ArgumentParser('11111111111111')
|
||||
parser.add_argument('-v', '--verbose',
|
||||
help='verbose mode', action='store_true')
|
||||
parser.add_argument('targets',
|
||||
help='installation targets',
|
||||
nargs='+',
|
||||
metavar='\n'.join(mapping))
|
||||
usage = textwrap.dedent('''\
|
||||
usage: 11111111111111 [-h] [-v]
|
||||
123456
|
||||
12345
|
||||
12345
|
||||
123 [123456
|
||||
12345
|
||||
12345
|
||||
123 ...]
|
||||
''')
|
||||
self.assertEqual(parser.format_usage(), usage)
|
||||
|
||||
def test_empty_metavar_required_arg(self):
|
||||
# https://github.com/python/cpython/issues/82091
|
||||
parser = argparse.ArgumentParser(prog='PROG')
|
||||
parser.add_argument('--nil', metavar='', required=True)
|
||||
parser.add_argument('--a', metavar='A' * 70)
|
||||
usage = (
|
||||
'usage: PROG [-h] --nil \n'
|
||||
' [--a AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
|
||||
'AAAAAAAAAAAAAAAAAAAAAAA]\n'
|
||||
)
|
||||
self.assertEqual(parser.format_usage(), usage)
|
||||
|
||||
def test_all_suppressed_mutex_with_optional_nargs(self):
|
||||
# https://github.com/python/cpython/issues/98666
|
||||
parser = argparse.ArgumentParser(prog='PROG')
|
||||
mutex = parser.add_mutually_exclusive_group()
|
||||
mutex.add_argument(
|
||||
'--param1',
|
||||
nargs='?', const='default', metavar='NAME', help=argparse.SUPPRESS)
|
||||
mutex.add_argument(
|
||||
'--param2',
|
||||
nargs='?', const='default', metavar='NAME', help=argparse.SUPPRESS)
|
||||
usage = 'usage: PROG [-h]\n'
|
||||
self.assertEqual(parser.format_usage(), usage)
|
||||
|
||||
def test_nested_mutex_groups(self):
|
||||
parser = argparse.ArgumentParser(prog='PROG')
|
||||
g = parser.add_mutually_exclusive_group()
|
||||
g.add_argument("--spam")
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore', DeprecationWarning)
|
||||
gg = g.add_mutually_exclusive_group()
|
||||
gg.add_argument("--hax")
|
||||
gg.add_argument("--hox", help=argparse.SUPPRESS)
|
||||
gg.add_argument("--hex")
|
||||
g.add_argument("--eggs")
|
||||
parser.add_argument("--num")
|
||||
|
||||
usage = textwrap.dedent('''\
|
||||
usage: PROG [-h] [--spam SPAM | [--hax HAX | --hex HEX] | --eggs EGGS]
|
||||
[--num NUM]
|
||||
''')
|
||||
self.assertEqual(parser.format_usage(), usage)
|
||||
|
||||
|
||||
class TestHelpVariableExpansion(HelpTestCase):
|
||||
"""Test that variables are expanded properly in help messages"""
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue