Skip to content

Commit 0f9fcfc

Browse files
authored
Merge pull request #750 from python-cmd2/support_custom_actions
Added functions to manually add choice providing functions to an argparse action
2 parents 4aa8370 + 0ffe33d commit 0f9fcfc

File tree

3 files changed

+88
-28
lines changed

3 files changed

+88
-28
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
* Enhancements
66
* Raise `TypeError` if trying to set choices/completions on argparse action that accepts no arguments
77
* Create directory for the persistent history file if it does not already exist
8+
* Added `set_choices_function()`, `set_choices_method()`, `set_completer_function()`, and `set_completer_method()`
9+
to support cases where this functionality needs to be added to an argparse action outside of the normal
10+
`parser.add_argument()` call.
811

912
## 0.9.15 (July 24, 2019)
1013
* Bug Fixes

cmd2/argparse_custom.py

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ def my_completer_function(text, line, begidx, endidx):
9494
as dynamic. Therefore it is up to the developer to validate if the user has typed an acceptable value for these
9595
arguments.
9696
97+
The following functions exist in cases where you may want to manually add choice providing function/methods to
98+
an existing argparse action. For instance, in __init__() of a custom action class.
99+
100+
set_choices_function(action, func)
101+
set_choices_method(action, method)
102+
set_completer_function(action, func)
103+
set_completer_method(action, method)
104+
97105
CompletionItem Class:
98106
This class was added to help in cases where uninformative data is being tab completed. For instance,
99107
tab completing ID numbers isn't very helpful to a user without context. Returning a list of CompletionItems
@@ -223,6 +231,9 @@ def __init__(self, value: object, desc: str = '', *args, **kwargs) -> None:
223231
self.description = desc
224232

225233

234+
############################################################################################################
235+
# Class and functions related to ChoicesCallable
236+
############################################################################################################
226237
class ChoicesCallable:
227238
"""
228239
Enables using a callable as the choices provider for an argparse argument.
@@ -241,6 +252,48 @@ def __init__(self, is_method: bool, is_completer: bool, to_call: Callable):
241252
self.to_call = to_call
242253

243254

255+
def _set_choices_callable(action: argparse.Action, choices_callable: ChoicesCallable) -> None:
256+
"""
257+
Set the choices_callable attribute of an argparse Action
258+
:param action: action being edited
259+
:param choices_callable: the ChoicesCallable instance to use
260+
:raises: TypeError if used on incompatible action type
261+
"""
262+
# Verify consistent use of parameters
263+
if action.choices is not None:
264+
err_msg = ("None of the following parameters can be used alongside a choices parameter:\n"
265+
"choices_function, choices_method, completer_function, completer_method")
266+
raise (TypeError(err_msg))
267+
elif action.nargs == 0:
268+
err_msg = ("None of the following parameters can be used on an action that takes no arguments:\n"
269+
"choices_function, choices_method, completer_function, completer_method")
270+
raise (TypeError(err_msg))
271+
272+
setattr(action, ATTR_CHOICES_CALLABLE, choices_callable)
273+
274+
275+
def set_choices_function(action: argparse.Action, choices_function: Callable[[], Iterable[Any]]) -> None:
276+
"""Set choices_function on an argparse action"""
277+
_set_choices_callable(action, ChoicesCallable(is_method=False, is_completer=False, to_call=choices_function))
278+
279+
280+
def set_choices_method(action: argparse.Action, choices_method: Callable[[Any], Iterable[Any]]) -> None:
281+
"""Set choices_method on an argparse action"""
282+
_set_choices_callable(action, ChoicesCallable(is_method=True, is_completer=False, to_call=choices_method))
283+
284+
285+
def set_completer_function(action: argparse.Action,
286+
completer_function: Callable[[str, str, int, int], List[str]]) -> None:
287+
"""Set completer_function on an argparse action"""
288+
_set_choices_callable(action, ChoicesCallable(is_method=False, is_completer=True, to_call=completer_function))
289+
290+
291+
def set_completer_method(action: argparse.Action,
292+
completer_method: Callable[[Any, str, str, int, int], List[str]]) -> None:
293+
"""Set completer_method on an argparse action"""
294+
_set_choices_callable(action, ChoicesCallable(is_method=True, is_completer=True, to_call=completer_method))
295+
296+
244297
############################################################################################################
245298
# Patch _ActionsContainer.add_argument with our wrapper to support more arguments
246299
############################################################################################################
@@ -291,7 +344,17 @@ def _add_argument_wrapper(self, *args,
291344
See the header of this file for more information
292345
293346
:return: the created argument action
347+
:raises ValueError on incorrect parameter usage
294348
"""
349+
# Verify consistent use of arguments
350+
choices_callables = [choices_function, choices_method, completer_function, completer_method]
351+
num_params_set = len(choices_callables) - choices_callables.count(None)
352+
353+
if num_params_set > 1:
354+
err_msg = ("Only one of the following parameters may be used at a time:\n"
355+
"choices_function, choices_method, completer_function, completer_method")
356+
raise (ValueError(err_msg))
357+
295358
# Pre-process special ranged nargs
296359
nargs_range = None
297360

@@ -345,34 +408,17 @@ def _add_argument_wrapper(self, *args,
345408
# Create the argument using the original add_argument function
346409
new_arg = orig_actions_container_add_argument(self, *args, **kwargs)
347410

348-
# Verify consistent use of arguments
349-
choices_params = [new_arg.choices, choices_function, choices_method, completer_function, completer_method]
350-
num_params_set = len(choices_params) - choices_params.count(None)
351-
352-
if num_params_set > 1:
353-
err_msg = ("Only one of the following parameters may be used at a time:\n"
354-
"choices, choices_function, choices_method, completer_function, completer_method")
355-
raise (ValueError(err_msg))
356-
elif num_params_set > 0 and new_arg.nargs == 0:
357-
err_msg = ("None of the following parameters can be used for this type of action:\n"
358-
"choices, choices_function, choices_method, completer_function, completer_method")
359-
raise (TypeError(err_msg))
360-
361411
# Set the custom attributes
362412
setattr(new_arg, ATTR_NARGS_RANGE, nargs_range)
363413

364414
if choices_function:
365-
setattr(new_arg, ATTR_CHOICES_CALLABLE,
366-
ChoicesCallable(is_method=False, is_completer=False, to_call=choices_function))
415+
set_choices_function(new_arg, choices_function)
367416
elif choices_method:
368-
setattr(new_arg, ATTR_CHOICES_CALLABLE,
369-
ChoicesCallable(is_method=True, is_completer=False, to_call=choices_method))
417+
set_choices_method(new_arg, choices_method)
370418
elif completer_function:
371-
setattr(new_arg, ATTR_CHOICES_CALLABLE,
372-
ChoicesCallable(is_method=False, is_completer=True, to_call=completer_function))
419+
set_completer_function(new_arg, completer_function)
373420
elif completer_method:
374-
setattr(new_arg, ATTR_CHOICES_CALLABLE,
375-
ChoicesCallable(is_method=True, is_completer=True, to_call=completer_method))
421+
set_completer_method(new_arg, completer_method)
376422

377423
setattr(new_arg, ATTR_SUPPRESS_TAB_HINT, suppress_tab_hint)
378424
setattr(new_arg, ATTR_DESCRIPTIVE_COMPLETION_HEADER, descriptive_header)

tests/test_argparse_custom.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,15 @@ def fake_func():
4040

4141

4242
@pytest.mark.parametrize('kwargs, is_valid', [
43-
({'choices': []}, True),
4443
({'choices_function': fake_func}, True),
4544
({'choices_method': fake_func}, True),
4645
({'completer_function': fake_func}, True),
4746
({'completer_method': fake_func}, True),
48-
({'choices': [], 'choices_function': fake_func}, False),
49-
({'choices': [], 'choices_method': fake_func}, False),
47+
({'choices_function': fake_func, 'choices_method': fake_func}, False),
5048
({'choices_method': fake_func, 'completer_function': fake_func}, False),
51-
({'choices_method': fake_func, 'completer_method': fake_func}, False),
49+
({'completer_function': fake_func, 'completer_method': fake_func}, False),
5250
])
53-
def test_apcustom_choices_params_count(kwargs, is_valid):
51+
def test_apcustom_choices_callable_count(kwargs, is_valid):
5452
parser = Cmd2ArgumentParser(prog='test')
5553
try:
5654
parser.add_argument('name', **kwargs)
@@ -66,11 +64,24 @@ def test_apcustom_choices_params_count(kwargs, is_valid):
6664
({'completer_function': fake_func}),
6765
({'completer_method': fake_func})
6866
])
69-
def test_apcustom_no_choices_when_nargs_is_0(kwargs):
67+
def test_apcustom_no_choices_callables_alongside_choices(kwargs):
68+
with pytest.raises(TypeError) as excinfo:
69+
parser = Cmd2ArgumentParser(prog='test')
70+
parser.add_argument('name', choices=['my', 'choices', 'list'], **kwargs)
71+
assert 'None of the following parameters can be used alongside a choices parameter' in str(excinfo.value)
72+
73+
74+
@pytest.mark.parametrize('kwargs', [
75+
({'choices_function': fake_func}),
76+
({'choices_method': fake_func}),
77+
({'completer_function': fake_func}),
78+
({'completer_method': fake_func})
79+
])
80+
def test_apcustom_no_choices_callables_when_nargs_is_0(kwargs):
7081
with pytest.raises(TypeError) as excinfo:
7182
parser = Cmd2ArgumentParser(prog='test')
7283
parser.add_argument('name', action='store_true', **kwargs)
73-
assert 'None of the following parameters can be used' in str(excinfo.value)
84+
assert 'None of the following parameters can be used on an action that takes no arguments' in str(excinfo.value)
7485

7586

7687
def test_apcustom_usage():

0 commit comments

Comments
 (0)