Skip to content

Commit 75f8008

Browse files
committed
Added ability to use CompletionItems as argparse choices
1 parent 506d4f6 commit 75f8008

File tree

4 files changed

+118
-8
lines changed

4 files changed

+118
-8
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.3.1 (TBD, 2021)
2+
* Enhancements
3+
* Added ability to use `CompletionItems` as argparse choices
4+
15
## 2.3.0 (November 11, 2021)
26
* Bug Fixes
37
* Fixed `AttributeError` in `rl_get_prompt()` when prompt is `None`.

cmd2/argparse_custom.py

+58-7
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,12 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens)
156156
2 Another item False
157157
3 Yet another item False
158158
159-
To use CompletionItems, just return them from your choices or completer
160-
functions.
159+
To use CompletionItems, just return them from your choices_provider or
160+
completer functions. They can also be used as argparse choices. When a
161+
CompletionItem is created, it stores the original value (e.g. ID number) and
162+
makes it accessible through a property called orig_value. cmd2 has patched
163+
argparse so that when evaluating choices, input is compared to
164+
CompletionItem.orig_value instead of the CompletionItem instance.
161165
162166
To avoid printing a ton of information to the screen at once when a user
163167
presses tab, there is a maximum threshold for the number of CompletionItems
@@ -173,6 +177,12 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens)
173177
completion and enables nargs range parsing. See _add_argument_wrapper for
174178
more details on these arguments.
175179
180+
``argparse.ArgumentParser._check_value`` - adds support for using
181+
``CompletionItems`` as argparse choices. When evaluating choices, input is
182+
compared to ``CompletionItem.orig_value`` instead of the ``CompletionItem``
183+
instance.
184+
See _ArgumentParser_check_value for more details.
185+
176186
``argparse.ArgumentParser._get_nargs_pattern`` - adds support for nargs ranges.
177187
See _get_nargs_pattern_wrapper for more details.
178188
@@ -308,17 +318,26 @@ def __new__(cls, value: object, *args: Any, **kwargs: Any) -> 'CompletionItem':
308318
return super(CompletionItem, cls).__new__(cls, value)
309319

310320
# noinspection PyUnusedLocal
311-
def __init__(self, value: object, desc: str = '', *args: Any) -> None:
321+
def __init__(self, value: object, description: str = '', *args: Any) -> None:
312322
"""
313323
CompletionItem Initializer
314324
315325
:param value: the value being tab completed
316-
:param desc: description text to display
326+
:param description: description text to display
317327
:param args: args for str __init__
318328
:param kwargs: kwargs for str __init__
319329
"""
320330
super().__init__(*args)
321-
self.description = desc
331+
self.description = description
332+
333+
# Save the original value to support CompletionItems as argparse choices.
334+
# cmd2 has patched argparse so input is compared to this value instead of the CompletionItem instance.
335+
self.__orig_value = value
336+
337+
@property
338+
def orig_value(self) -> Any:
339+
"""Read-only property for __orig_value"""
340+
return self.__orig_value
322341

323342

324343
############################################################################################################
@@ -870,7 +889,7 @@ def _add_argument_wrapper(
870889
setattr(argparse._ActionsContainer, 'add_argument', _add_argument_wrapper)
871890

872891
############################################################################################################
873-
# Patch ArgumentParser._get_nargs_pattern with our wrapper to nargs ranges
892+
# Patch ArgumentParser._get_nargs_pattern with our wrapper to support nargs ranges
874893
############################################################################################################
875894

876895
# Save original ArgumentParser._get_nargs_pattern so we can call it in our wrapper
@@ -905,7 +924,7 @@ def _get_nargs_pattern_wrapper(self: argparse.ArgumentParser, action: argparse.A
905924

906925

907926
############################################################################################################
908-
# Patch ArgumentParser._match_argument with our wrapper to nargs ranges
927+
# Patch ArgumentParser._match_argument with our wrapper to support nargs ranges
909928
############################################################################################################
910929
# noinspection PyProtectedMember
911930
orig_argument_parser_match_argument = argparse.ArgumentParser._match_argument
@@ -977,6 +996,38 @@ def _ArgumentParser_set_ap_completer_type(self: argparse.ArgumentParser, ap_comp
977996
setattr(argparse.ArgumentParser, 'set_ap_completer_type', _ArgumentParser_set_ap_completer_type)
978997

979998

999+
############################################################################################################
1000+
# Patch ArgumentParser._check_value to support CompletionItems as choices
1001+
############################################################################################################
1002+
# noinspection PyPep8Naming
1003+
def _ArgumentParser_check_value(self: argparse.ArgumentParser, action: argparse.Action, value: Any) -> None:
1004+
"""
1005+
Custom override of ArgumentParser._check_value that supports CompletionItems as choices.
1006+
When evaluating choices, input is compared to CompletionItem.orig_value instead of the
1007+
CompletionItem instance.
1008+
1009+
:param self: ArgumentParser instance
1010+
:param action: the action being populated
1011+
:param value: value from command line already run through conversion function by argparse
1012+
"""
1013+
# Import gettext like argparse does
1014+
from gettext import (
1015+
gettext as _,
1016+
)
1017+
1018+
# converted value must be one of the choices (if specified)
1019+
if action.choices is not None:
1020+
# If any choice is a CompletionItem, then use its orig_value property.
1021+
choices = [c.orig_value if isinstance(c, CompletionItem) else c for c in action.choices]
1022+
if value not in choices:
1023+
args = {'value': value, 'choices': ', '.join(map(repr, choices))}
1024+
msg = _('invalid choice: %(value)r (choose from %(choices)s)')
1025+
raise ArgumentError(action, msg % args)
1026+
1027+
1028+
setattr(argparse.ArgumentParser, '_check_value', _ArgumentParser_check_value)
1029+
1030+
9801031
############################################################################################################
9811032
# Patch argparse._SubParsersAction to add remove_parser function
9821033
############################################################################################################

tests/test_argparse_completer.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def completion_item_method(self) -> List[CompletionItem]:
130130
items = []
131131
for i in range(0, 10):
132132
main_str = 'main_str{}'.format(i)
133-
items.append(CompletionItem(main_str, desc='blah blah'))
133+
items.append(CompletionItem(main_str, description='blah blah'))
134134
return items
135135

136136
choices_parser = Cmd2ArgumentParser()

tests/test_argparse_custom.py

+55
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,58 @@ def test_cmd2_attribute_wrapper():
284284
new_val = 22
285285
wrapper.set(new_val)
286286
assert wrapper.get() == new_val
287+
288+
289+
def test_completion_items_as_choices(capsys):
290+
"""
291+
Test cmd2's patch to Argparse._check_value() which supports CompletionItems as choices.
292+
Choices are compared to CompletionItems.orig_value instead of the CompletionItem instance.
293+
"""
294+
from cmd2.argparse_custom import (
295+
CompletionItem,
296+
)
297+
298+
##############################################################
299+
# Test CompletionItems with str values
300+
##############################################################
301+
choices = [CompletionItem("1", "Description One"), CompletionItem("2", "Two")]
302+
parser = Cmd2ArgumentParser()
303+
parser.add_argument("choices_arg", type=str, choices=choices)
304+
305+
# First test valid choices. Confirm the parsed data matches the correct type of str.
306+
args = parser.parse_args(['1'])
307+
assert args.choices_arg == '1'
308+
309+
args = parser.parse_args(['2'])
310+
assert args.choices_arg == '2'
311+
312+
# Next test invalid choice
313+
with pytest.raises(SystemExit):
314+
args = parser.parse_args(['3'])
315+
316+
# Confirm error text contains correct value type of str
317+
out, err = capsys.readouterr()
318+
assert "invalid choice: '3' (choose from '1', '2')" in err
319+
320+
##############################################################
321+
# Test CompletionItems with int values
322+
##############################################################
323+
choices = [CompletionItem(1, "Description One"), CompletionItem(2, "Two")]
324+
parser = Cmd2ArgumentParser()
325+
# noinspection PyTypeChecker
326+
parser.add_argument("choices_arg", type=int, choices=choices)
327+
328+
# First test valid choices. Confirm the parsed data matches the correct type of int.
329+
args = parser.parse_args(['1'])
330+
assert args.choices_arg == 1
331+
332+
args = parser.parse_args(['2'])
333+
assert args.choices_arg == 2
334+
335+
# Next test invalid choice
336+
with pytest.raises(SystemExit):
337+
args = parser.parse_args(['3'])
338+
339+
# Confirm error text contains correct value type of int
340+
out, err = capsys.readouterr()
341+
assert 'invalid choice: 3 (choose from 1, 2)' in err

0 commit comments

Comments
 (0)