Skip to content

Commit ddb0248

Browse files
jiasliVadim Frolov
and
Vadim Frolov
authored
Add error message for invalid argument value (#244)
Co-authored-by: Vadim Frolov <vadim.frolov@dnvgl.com>
1 parent 02b7652 commit ddb0248

File tree

3 files changed

+66
-16
lines changed

3 files changed

+66
-16
lines changed

examples/exapp2

+26
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ type: group
5959
short-summary: An experimental command group
6060
"""
6161

62+
helps['demo'] = """
63+
type: group
64+
short-summary: A command group for demos.
65+
"""
66+
67+
helps['demo arg'] = """
68+
type: group
69+
short-summary: A command showing how to use arguments.
70+
"""
71+
6272

6373
def abc_show_command_handler():
6474
"""
@@ -151,6 +161,13 @@ def hello_command_handler(greetings=None):
151161
return ['Hello World!', greetings]
152162

153163

164+
def demo_arg_handler(move=None):
165+
if move:
166+
print("Your move was: {}".format(move))
167+
return
168+
print("Nothing to do.")
169+
170+
154171
WELCOME_MESSAGE = r"""
155172
_____ _ _____
156173
/ ____| | |_ _|
@@ -179,6 +196,7 @@ class MyCommandsLoader(CLICommandsLoader):
179196
g.command('hello', 'hello_command_handler', confirmation=True)
180197
g.command('sample-json', 'sample_json_handler')
181198
g.command('sample-logger', 'sample_logger_handler')
199+
182200
with CommandGroup(self, 'abc', '__main__#{}') as g:
183201
g.command('list', 'abc_list_command_handler')
184202
g.command('show', 'abc_show_command_handler')
@@ -202,12 +220,20 @@ class MyCommandsLoader(CLICommandsLoader):
202220
# A deprecated command group
203221
with CommandGroup(self, 'dep', '__main__#{}', deprecate_info=g.deprecate(redirect='ga', hide='1.0.0')) as g:
204222
g.command('range', 'range_command_handler')
223+
224+
with CommandGroup(self, 'demo', '__main__#{}') as g:
225+
g.command('arg', 'demo_arg_handler')
226+
205227
return super(MyCommandsLoader, self).load_command_table(args)
206228

207229
def load_arguments(self, command):
208230
with ArgumentsContext(self, 'ga range') as ac:
209231
ac.argument('start', type=int, is_preview=True)
210232
ac.argument('end', type=int, is_experimental=True)
233+
234+
with ArgumentsContext(self, 'demo arg') as ac:
235+
ac.argument('move', choices=['rock', 'paper', 'scissors'])
236+
211237
super(MyCommandsLoader, self).load_arguments(command)
212238

213239

knack/parser.py

+19-14
Original file line numberDiff line numberDiff line change
@@ -266,19 +266,24 @@ def _check_value(self, action, value):
266266
import sys
267267

268268
if action.choices is not None and value not in action.choices:
269-
# parser has no `command_source`, value is part of command itself
270-
error_msg = "{prog}: '{value}' is not in the '{prog}' command group. See '{prog} --help'.".format(
271-
prog=self.prog, value=value)
272-
logger.error(error_msg)
273-
candidates = difflib.get_close_matches(value, action.choices, cutoff=0.7)
274-
if candidates:
275-
print_args = {
276-
's': 's' if len(candidates) > 1 else '',
277-
'verb': 'are' if len(candidates) > 1 else 'is',
278-
'value': value
279-
}
280-
suggestion_msg = "\nThe most similar choice{s} to '{value}' {verb}:\n".format(**print_args)
281-
suggestion_msg += '\n'.join(['\t' + candidate for candidate in candidates])
269+
if action.dest in ["_command", "_subcommand"]:
270+
# Command
271+
error_msg = "{prog}: '{value}' is not in the '{prog}' command group. See '{prog} --help'.".format(
272+
prog=self.prog, value=value)
273+
logger.error(error_msg)
274+
# Show suggestions
275+
candidates = difflib.get_close_matches(value, action.choices, cutoff=0.7)
276+
if candidates:
277+
suggestion_msg = "\nThe most similar choices to '{value}':\n".format(value=value)
278+
suggestion_msg += '\n'.join(['\t' + candidate for candidate in candidates])
279+
print(suggestion_msg, file=sys.stderr)
280+
else:
281+
# Argument
282+
error_msg = "{prog}: '{value}' is not a valid value for '{name}'.".format(
283+
prog=self.prog, value=value,
284+
name=argparse._get_action_name(action)) # pylint: disable=protected-access
285+
logger.error(error_msg)
286+
# Show all allowed values
287+
suggestion_msg = "Allowed values: " + ', '.join(action.choices)
282288
print(suggestion_msg, file=sys.stderr)
283-
284289
self.exit(2)

tests/test_parser.py

+21-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from knack.parser import CLICommandParser
1010
from knack.commands import CLICommand
1111
from knack.arguments import enum_choice_list
12-
from tests.util import MockContext
12+
from tests.util import MockContext, redirect_io
1313

1414

1515
class TestParser(unittest.TestCase):
@@ -83,7 +83,7 @@ def test_handler():
8383
parser.parse_args('test command -req yep'.split())
8484
self.assertTrue(CLICommandParser.error.called)
8585

86-
def test_case_insensitive_enum_choices(self):
86+
def _enum_parser(self):
8787
from enum import Enum
8888

8989
class TestEnum(Enum): # pylint: disable=too-few-public-methods
@@ -102,7 +102,10 @@ def test_handler():
102102

103103
parser = CLICommandParser()
104104
parser.load_command_table(self.mock_ctx.commands_loader)
105+
return parser
105106

107+
def test_case_insensitive_enum_choices(self):
108+
parser = self._enum_parser()
106109
args = parser.parse_args('test command --opt alL_cAps'.split())
107110
self.assertEqual(args.opt, 'ALL_CAPS')
108111

@@ -112,6 +115,22 @@ def test_handler():
112115
args = parser.parse_args('test command --opt sNake_CASE'.split())
113116
self.assertEqual(args.opt, 'snake_case')
114117

118+
@redirect_io
119+
def test_check_value_invalid_command(self):
120+
parser = self._enum_parser()
121+
with self.assertRaises(SystemExit) as cm:
122+
parser.parse_args('test command1'.split()) # 'command1' is invalid
123+
actual = self.io.getvalue()
124+
assert "is not in the" in actual and "command group" in actual
125+
126+
@redirect_io
127+
def test_check_value_invalid_argument_value(self):
128+
parser = self._enum_parser()
129+
with self.assertRaises(SystemExit) as cm:
130+
parser.parse_args('test command --opt foo'.split()) # 'foo' is invalid
131+
actual = self.io.getvalue()
132+
assert "is not a valid value for" in actual
133+
115134
def test_cli_ctx_type_error(self):
116135
with self.assertRaises(TypeError):
117136
CLICommandParser(cli_ctx=object())

0 commit comments

Comments
 (0)