Skip to content

bpo-29298: Fix crash with required subparsers without dest #3680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,8 @@ def _get_action_name(argument):
return argument.metavar
elif argument.dest not in (None, SUPPRESS):
return argument.dest
elif argument.choices:
return '{' + ','.join(argument.choices) + '}'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test doesn’t really help me understand what this change does (i.e. what a human would see in an error case).

I’m also not sure if some of the changes in the various patches for bpo-9253 (especially in tests) could be useful here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check those out. I had a difficult time demonstrating (by assertion) the actual output (didn't find any examples) -- I'll bang against that and hopefully I'll make something more expressive :)

Copy link
Contributor Author

@asottile asottile Sep 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@merwok looks like they had the same idea I had (but a different implementation) -- I took inspiration from one of their tests for additional coverage and updated the tests to hopefully be more expressive (and have a better failure mode)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code for this is already in _metavar_formatter:

        elif action.choices is not None:
            choice_strs = [str(choice) for choice in action.choices]
            result = '{%s}' % ','.join(choice_strs)

The metavar formatter can be called, if the parser is known (as self), using:

            default = self._get_default_metavar_for_positional(action)
            metavar, = self._metavar_formatter(action, default)(1)

Perhaps _get_action_name should be made into a method, or take the parser as an optional argument?

else:
return None

Expand Down
24 changes: 24 additions & 0 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2060,6 +2060,30 @@ def test_required_subparsers_default(self):
ret = parser.parse_args(())
self.assertIsNone(ret.command)

def test_required_subparsers_no_destination_error(self):
parser = ErrorRaisingArgumentParser()
subparsers = parser.add_subparsers(required=True)
subparsers.add_parser('foo')
subparsers.add_parser('bar')
with self.assertRaises(ArgumentParserError) as excinfo:
parser.parse_args(())
self.assertRegex(
excinfo.exception.stderr,
'error: the following arguments are required: {foo,bar}\n$'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a space after comma. Why braces are used?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a notation for alternatives: it means one of foo or bar is required.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is consistent with the rendering of choices:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--foo', choices=('baz', 'womp'))
parser.parse_args()
$ python test.py --help
usage: test.py [-h] [--foo {baz,womp}]

optional arguments:
  -h, --help        show this help message and exit
  --foo {baz,womp}

)

def test_wrong_argument_subparsers_no_destination_error(self):
parser = ErrorRaisingArgumentParser()
subparsers = parser.add_subparsers(required=True)
subparsers.add_parser('foo')
subparsers.add_parser('bar')
with self.assertRaises(ArgumentParserError) as excinfo:
parser.parse_args(('baz',))
self.assertRegex(
excinfo.exception.stderr,
r"error: argument {foo,bar}: invalid choice: 'baz' \(choose from 'foo', 'bar'\)\n$"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"foo, bar" is duplicated. And this error message looks confusing to me. What it means?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I didn't really know what to do for this, it's essentially an anonymous positional choices argument. Personally I'd love a default dest='command' but I imagine that could cause some incompatibility?

Then this would read something like error: argument command: invalid choice ...

)

def test_optional_subparsers(self):
parser = ErrorRaisingArgumentParser()
subparsers = parser.add_subparsers(dest='command', required=False)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix ``TypeError`` when required subparsers without ``dest`` do not receive
arguments. Patch by Anthony Sottile.