Skip to content

Commit 970c5fc

Browse files
authored
Merge pull request #892 from python-cmd2/completion_updates
Completion updates
2 parents 2221e08 + 34ce17c commit 970c5fc

File tree

9 files changed

+223
-215
lines changed

9 files changed

+223
-215
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313
* `__name__`: __main__
1414
* `__file__`: script path (as typed, ~ will be expanded)
1515
* Only tab complete after redirection tokens if redirection is allowed
16+
* Made `CompletionError` exception available to non-argparse tab completion
17+
* Added `apply_style` to `CompletionError` initializer. It defaults to True, but can be set to False if
18+
you don't want the error text to have `ansi.style_error()` applied to it when printed.
1619
* Other
1720
* Removed undocumented `py run` command since it was replaced by `run_pyscript` a while ago
21+
* Renamed `AutoCompleter` to `ArgparseCompleter` for clarity
1822

1923
## 0.10.0 (February 7, 2020)
2024
* Enhancements

cmd2/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
pass
1212

1313
from .ansi import style, fg, bg
14-
from .argparse_custom import Cmd2ArgumentParser, CompletionError, CompletionItem, set_default_argument_parser
14+
from .argparse_custom import Cmd2ArgumentParser, CompletionItem, set_default_argument_parser
1515

1616
# Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER
1717
import argparse
@@ -27,4 +27,4 @@
2727
from .decorators import categorize, with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category
2828
from .parsing import Statement
2929
from .py_bridge import CommandResult
30-
from .utils import Settable
30+
from .utils import CompletionError, Settable

cmd2/argparse_completer.py

Lines changed: 112 additions & 145 deletions
Large diffs are not rendered by default.

cmd2/argparse_custom.py

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class called Cmd2ArgumentParser which improves error and help output over normal
2626
parser.add_argument('-f', nargs=(3, 5))
2727
2828
Tab Completion:
29-
cmd2 uses its AutoCompleter class to enable argparse-based tab completion on all commands that use the
29+
cmd2 uses its ArgparseCompleter class to enable argparse-based tab completion on all commands that use the
3030
@with_argparse wrappers. Out of the box you get tab completion of commands, subcommands, and flag names,
3131
as well as instructive hints about the current argument that print when tab is pressed. In addition,
3232
you can add tab completion for each argument's values using parameters passed to add_argument().
@@ -53,7 +53,7 @@ def my_choices_function():
5353
5454
choices_method
5555
This is exactly like choices_function, but the function needs to be an instance method of a cmd2-based class.
56-
When AutoCompleter calls the method, it will pass the app instance as the self argument. This is good in
56+
When ArgparseCompleter calls the method, it will pass the app instance as the self argument. This is good in
5757
cases where the list of choices being generated relies on state data of the cmd2-based app
5858
5959
Example:
@@ -74,7 +74,7 @@ def my_completer_function(text, line, begidx, endidx):
7474
7575
completer_method
7676
This is exactly like completer_function, but the function needs to be an instance method of a cmd2-based class.
77-
When AutoCompleter calls the method, it will pass the app instance as the self argument. cmd2 provides
77+
When ArgparseCompleter calls the method, it will pass the app instance as the self argument. cmd2 provides
7878
a few completer methods for convenience (e.g., path_complete, delimiter_complete)
7979
8080
Example:
@@ -113,21 +113,14 @@ def my_choices_method(self, arg_tokens)
113113
def my_completer_method(self, text, line, begidx, endidx, arg_tokens)
114114
115115
All values of the arg_tokens dictionary are lists, even if a particular argument expects only 1 token. Since
116-
AutoCompleter is for tab completion, it does not convert the tokens to their actual argument types or validate
116+
ArgparseCompleter is for tab completion, it does not convert the tokens to their actual argument types or validate
117117
their values. All tokens are stored in the dictionary as the raw strings provided on the command line. It is up to
118118
the developer to determine if the user entered the correct argument type (e.g. int) and validate their values.
119119
120-
CompletionError Class:
121-
Raised during tab completion operations to report any sort of error you want printed by the AutoCompleter
122-
123-
Example use cases
124-
- Reading a database to retrieve a tab completion data set failed
125-
- A previous command line argument that determines the data set being completed is invalid
126-
127120
CompletionItem Class:
128121
This class was added to help in cases where uninformative data is being tab completed. For instance,
129122
tab completing ID numbers isn't very helpful to a user without context. Returning a list of CompletionItems
130-
instead of a regular string for completion results will signal the AutoCompleter to output the completion
123+
instead of a regular string for completion results will signal the ArgparseCompleter to output the completion
131124
results in a table of completion tokens with descriptions instead of just a table of tokens.
132125
133126
Instead of this:
@@ -229,17 +222,6 @@ def generate_range_error(range_min: int, range_max: Union[int, float]) -> str:
229222
return err_str
230223

231224

232-
class CompletionError(Exception):
233-
"""
234-
Raised during tab completion operations to report any sort of error you want printed by the AutoCompleter
235-
236-
Example use cases
237-
- Reading a database to retrieve a tab completion data set failed
238-
- A previous command line argument that determines the data set being completed is invalid
239-
"""
240-
pass
241-
242-
243225
class CompletionItem(str):
244226
"""
245227
Completion item with descriptive text attached
@@ -353,15 +335,15 @@ def _add_argument_wrapper(self, *args,
353335
:param nargs: extends argparse nargs functionality by allowing tuples which specify a range (min, max)
354336
to specify a max value with no upper bound, use a 1-item tuple (min,)
355337
356-
# Added args used by AutoCompleter
338+
# Added args used by ArgparseCompleter
357339
:param choices_function: function that provides choices for this argument
358340
:param choices_method: cmd2-app method that provides choices for this argument
359341
:param completer_function: tab completion function that provides choices for this argument
360342
:param completer_method: cmd2-app tab completion method that provides choices for this argument
361-
:param suppress_tab_hint: when AutoCompleter has no results to show during tab completion, it displays the current
362-
argument's help text as a hint. Set this to True to suppress the hint. If this argument's
363-
help text is set to argparse.SUPPRESS, then tab hints will not display regardless of the
364-
value passed for suppress_tab_hint. Defaults to False.
343+
:param suppress_tab_hint: when ArgparseCompleter has no results to show during tab completion, it displays the
344+
current argument's help text as a hint. Set this to True to suppress the hint. If this
345+
argument's help text is set to argparse.SUPPRESS, then tab hints will not display
346+
regardless of the value passed for suppress_tab_hint. Defaults to False.
365347
:param descriptive_header: if the provided choices are CompletionItems, then this header will display
366348
during tab completion. Defaults to None.
367349

cmd2/cmd2.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@
4747
from . import constants
4848
from . import plugin
4949
from . import utils
50-
from .argparse_custom import CompletionError, CompletionItem, DEFAULT_ARGUMENT_PARSER
50+
from .argparse_custom import CompletionItem, DEFAULT_ARGUMENT_PARSER
5151
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
5252
from .decorators import with_argparser
5353
from .history import History, HistoryItem
5454
from .parsing import StatementParser, Statement, Macro, MacroArg, shlex_split
5555
from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support, rl_make_safe_prompt, rl_warning
56-
from .utils import Settable
56+
from .utils import CompletionError, Settable
5757

5858
# Set up readline
5959
if rl_type == RlType.NONE: # pragma: no cover
@@ -1416,17 +1416,27 @@ def complete(self, text: str, state: int) -> Optional[str]:
14161416
except IndexError:
14171417
return None
14181418

1419+
except CompletionError as ex:
1420+
# Don't print error and redraw the prompt unless the error has length
1421+
err_str = str(ex)
1422+
if err_str:
1423+
if ex.apply_style:
1424+
err_str = ansi.style_error(err_str)
1425+
ansi.style_aware_write(sys.stdout, '\n' + err_str + '\n')
1426+
rl_force_redisplay()
1427+
return None
14191428
except Exception as e:
14201429
# Insert a newline so the exception doesn't print in the middle of the command line being tab completed
14211430
self.perror()
14221431
self.pexcept(e)
1432+
rl_force_redisplay()
14231433
return None
14241434

14251435
def _autocomplete_default(self, text: str, line: str, begidx: int, endidx: int, *,
14261436
argparser: argparse.ArgumentParser, preserve_quotes: bool) -> List[str]:
14271437
"""Default completion function for argparse commands"""
1428-
from .argparse_completer import AutoCompleter
1429-
completer = AutoCompleter(argparser, self)
1438+
from .argparse_completer import ArgparseCompleter
1439+
completer = ArgparseCompleter(argparser, self)
14301440
tokens, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
14311441

14321442
# To have tab-completion parsing match command line parsing behavior,
@@ -2560,11 +2570,11 @@ def complete_help_subcommands(self, text: str, line: str, begidx: int, endidx: i
25602570
if func is None or argparser is None:
25612571
return []
25622572

2563-
# Combine the command and its subcommand tokens for the AutoCompleter
2573+
# Combine the command and its subcommand tokens for the ArgparseCompleter
25642574
tokens = [command] + arg_tokens['subcommands']
25652575

2566-
from .argparse_completer import AutoCompleter
2567-
completer = AutoCompleter(argparser, self)
2576+
from .argparse_completer import ArgparseCompleter
2577+
completer = ArgparseCompleter(argparser, self)
25682578
return completer.complete_subcommand_help(tokens, text, line, begidx, endidx)
25692579

25702580
help_parser = DEFAULT_ARGUMENT_PARSER(description="List available commands or provide "
@@ -2576,7 +2586,7 @@ def complete_help_subcommands(self, text: str, line: str, begidx: int, endidx: i
25762586
help_parser.add_argument('-v', '--verbose', action='store_true',
25772587
help="print a list of all commands with descriptions of each")
25782588

2579-
# Get rid of cmd's complete_help() functions so AutoCompleter will complete the help command
2589+
# Get rid of cmd's complete_help() functions so ArgparseCompleter will complete the help command
25802590
if getattr(cmd.Cmd, 'complete_help', None) is not None:
25812591
delattr(cmd.Cmd, 'complete_help')
25822592

@@ -2594,8 +2604,8 @@ def do_help(self, args: argparse.Namespace) -> None:
25942604

25952605
# If the command function uses argparse, then use argparse's help
25962606
if func is not None and argparser is not None:
2597-
from .argparse_completer import AutoCompleter
2598-
completer = AutoCompleter(argparser, self)
2607+
from .argparse_completer import ArgparseCompleter
2608+
completer = ArgparseCompleter(argparser, self)
25992609
tokens = [args.command] + args.subcommands
26002610

26012611
# Set end to blank so the help output matches how it looks when "command -h" is used
@@ -2838,8 +2848,8 @@ def complete_set_value(self, text: str, line: str, begidx: int, endidx: int,
28382848
completer_function=settable.completer_function,
28392849
completer_method=settable.completer_method)
28402850

2841-
from .argparse_completer import AutoCompleter
2842-
completer = AutoCompleter(settable_parser, self)
2851+
from .argparse_completer import ArgparseCompleter
2852+
completer = ArgparseCompleter(settable_parser, self)
28432853

28442854
# Use raw_tokens since quotes have been preserved
28452855
_, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
@@ -2860,7 +2870,7 @@ def complete_set_value(self, text: str, line: str, begidx: int, endidx: int,
28602870
set_parser = DEFAULT_ARGUMENT_PARSER(parents=[set_parser_parent])
28612871

28622872
# Suppress tab-completion hints for this field. The completer method is going to create an
2863-
# AutoCompleter based on the actual parameter being completed and we only want that hint printing.
2873+
# ArgparseCompleter based on the actual parameter being completed and we only want that hint printing.
28642874
set_parser.add_argument('value', nargs=argparse.OPTIONAL, help='new value for settable',
28652875
completer_method=complete_set_value, suppress_tab_hint=True)
28662876

cmd2/utils.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,31 @@ def str_to_bool(val: str) -> bool:
7272
raise ValueError("must be True or False (case-insensitive)")
7373

7474

75+
class CompletionError(Exception):
76+
"""
77+
Raised during tab completion operations to report any sort of error you want printed by the ArgparseCompleter
78+
This can also be used just to display a message, even if it's not an error. ArgparseCompleter raises
79+
CompletionErrors to display tab completion hints and sets apply_style to False so hints aren't colored
80+
like error text.
81+
82+
Example use cases
83+
- Reading a database to retrieve a tab completion data set failed
84+
- A previous command line argument that determines the data set being completed is invalid
85+
- Tab completion hints
86+
"""
87+
def __init__(self, *args, apply_style: bool = True, **kwargs):
88+
"""
89+
Initializer for CompletionError
90+
:param apply_style: If True, then ansi.style_error will be applied to the message text when printed.
91+
Set to False in cases where the message text already has the desired style.
92+
Defaults to True.
93+
"""
94+
self.apply_style = apply_style
95+
96+
# noinspection PyArgumentList
97+
super().__init__(*args, **kwargs)
98+
99+
75100
class Settable:
76101
"""Used to configure a cmd2 instance member to be settable via the set command in the CLI"""
77102
def __init__(self, name: str, val_type: Callable, description: str, *,
@@ -109,8 +134,8 @@ def __init__(self, name: str, val_type: Callable, description: str, *,
109134
for this argument (See note below)
110135
111136
Note:
112-
For choices_method and completer_method, do not set them to a bound method. This is because AutoCompleter
113-
passes the self argument explicitly to these functions.
137+
For choices_method and completer_method, do not set them to a bound method. This is because
138+
ArgparseCompleter passes the self argument explicitly to these functions.
114139
115140
Therefore instead of passing something like self.path_complete, pass cmd2.Cmd.path_complete.
116141
"""

examples/argparse_completion.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import argparse
77
from typing import Dict, List
88

9-
from cmd2 import Cmd, Cmd2ArgumentParser, with_argparser, CompletionError, CompletionItem
10-
from cmd2.utils import basic_complete
9+
from cmd2 import Cmd, Cmd2ArgumentParser, with_argparser, CompletionItem
10+
from cmd2.utils import basic_complete, CompletionError
1111

1212
# Data source for argparse.choices
1313
food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato']

examples/basic_completion.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22
# coding=utf-8
33
"""
44
A simple example demonstrating how to enable tab completion by assigning a completer function to do_* commands.
5-
This also demonstrates capabilities of the following completer methods included with cmd2:
6-
- delimiter_completer
7-
- flag_based_complete (see note below)
8-
- index_based_complete (see note below)
5+
This also demonstrates capabilities of the following completer features included with cmd2:
6+
- CompletionError exceptions
7+
- delimiter_completer()
8+
- flag_based_complete() (see note below)
9+
- index_based_complete() (see note below)
910
1011
flag_based_complete() and index_based_complete() are basic methods and should only be used if you are not
1112
familiar with argparse. The recommended approach for tab completing positional tokens and flags is to use
1213
argparse-based completion. For an example integrating tab completion with argparse, see argparse_completion.py
1314
"""
1415
import functools
16+
from typing import List
1517

1618
import cmd2
1719

@@ -42,7 +44,7 @@ def do_flag_based(self, statement: cmd2.Statement):
4244
"""
4345
self.poutput("Args: {}".format(statement.args))
4446

45-
def complete_flag_based(self, text, line, begidx, endidx):
47+
def complete_flag_based(self, text, line, begidx, endidx) -> List[str]:
4648
"""Completion function for do_flag_based"""
4749
flag_dict = \
4850
{
@@ -65,7 +67,7 @@ def do_index_based(self, statement: cmd2.Statement):
6567
"""Tab completes first 3 arguments using index_based_complete"""
6668
self.poutput("Args: {}".format(statement.args))
6769

68-
def complete_index_based(self, text, line, begidx, endidx):
70+
def complete_index_based(self, text, line, begidx, endidx) -> List[str]:
6971
"""Completion function for do_index_based"""
7072
index_dict = \
7173
{
@@ -84,6 +86,20 @@ def do_delimiter_complete(self, statement: cmd2.Statement):
8486
complete_delimiter_complete = functools.partialmethod(cmd2.Cmd.delimiter_complete,
8587
match_against=file_strs, delimiter='/')
8688

89+
def do_raise_error(self, statement: cmd2.Statement):
90+
"""Demonstrates effect of raising CompletionError"""
91+
self.poutput("Args: {}".format(statement.args))
92+
93+
def complete_raise_error(self, text, line, begidx, endidx) -> List[str]:
94+
"""
95+
CompletionErrors can be raised if an error occurs while tab completing.
96+
97+
Example use cases
98+
- Reading a database to retrieve a tab completion data set failed
99+
- A previous command line argument that determines the data set being completed is invalid
100+
"""
101+
raise cmd2.CompletionError("This is how a CompletionError behaves")
102+
87103

88104
if __name__ == '__main__':
89105
import sys

0 commit comments

Comments
 (0)