Skip to content

Commit 1a85bfe

Browse files
authored
Merge pull request #1133 from python-cmd2/topic_width
Updated some commands to use SimpleTable in their output
2 parents 9d81810 + 16e145a commit 1a85bfe

File tree

14 files changed

+304
-172
lines changed

14 files changed

+304
-172
lines changed

CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@
55
* New function `register_argparse_argument_parameter()` allows developers to specify custom
66
parameters to be passed to the argparse parser's `add_argument()` method. These parameters will
77
become accessible in the resulting argparse Action object when modifying `ArgparseCompleter` behavior.
8+
* Using `SimpleTable` in the output for the following commands to improve appearance.
9+
* help
10+
* set (command and tab completion of Settables)
11+
* alias tab completion
12+
* macro tab completion
13+
* Tab completion of `CompletionItems` now includes divider row comprised of `Cmd.ruler` character.
14+
* Removed `--verbose` flag from set command since descriptions always show now.
815
* Deletions (potentially breaking changes)
9-
* Deleted ``set_choices_provider()`` and ``set_completer()`` which were deprecated in 2.1.2
10-
16+
* Deleted ``set_choices_provider()`` and ``set_completer()`` which were deprecated in 2.1.2
17+
1118
## 2.1.2 (July 5, 2021)
1219
* Enhancements
1320
* Added the following accessor methods for cmd2-specific attributes to the `argparse.Action` class

cmd2/argparse_completer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,7 +588,7 @@ def _format_completions(self, arg_state: _ArgumentState, completions: Union[List
588588
cols.append(Column(destination.upper(), width=token_width))
589589
cols.append(Column(desc_header, width=desc_width))
590590

591-
hint_table = SimpleTable(cols, divider_char=None)
591+
hint_table = SimpleTable(cols, divider_char=self._cmd2_app.ruler)
592592
table_data = [[item, item.description] for item in completion_items]
593593
self._cmd2_app.formatted_completions = hint_table.generate_table(table_data, row_spacing=0)
594594

cmd2/argparse_custom.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens)
130130
131131
The user sees this:
132132
ITEM_ID Item Name
133+
============================
133134
1 My item
134135
2 Another item
135136
3 Yet another item
@@ -150,6 +151,7 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens)
150151
can format them in such a way to have multiple columns::
151152
152153
ITEM_ID Item Name Checked Out Due Date
154+
==========================================================
153155
1 My item True 02/02/2022
154156
2 Another item False
155157
3 Yet another item False

cmd2/cmd2.py

Lines changed: 162 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@
131131
rl_warning,
132132
vt100_support,
133133
)
134+
from .table_creator import (
135+
Column,
136+
SimpleTable,
137+
)
134138
from .utils import (
135139
Settable,
136140
get_defining_class,
@@ -2024,7 +2028,7 @@ def _perform_completion(
20242028
def complete( # type: ignore[override]
20252029
self, text: str, state: int, custom_settings: Optional[utils.CustomCompletionSettings] = None
20262030
) -> Optional[str]:
2027-
"""Override of cmd2's complete method which returns the next possible completion for 'text'
2031+
"""Override of cmd's complete method which returns the next possible completion for 'text'
20282032
20292033
This completer function is called by readline as complete(text, state), for state in 0, 1, 2, …,
20302034
until it returns a non-string value. It should return the next possible completion starting with text.
@@ -2159,17 +2163,44 @@ def get_visible_commands(self) -> List[str]:
21592163
if command not in self.hidden_commands and command not in self.disabled_commands
21602164
]
21612165

2166+
# Table displayed when tab completing aliases
2167+
_alias_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None)
2168+
21622169
def _get_alias_completion_items(self) -> List[CompletionItem]:
2163-
"""Return list of current alias names and values as CompletionItems"""
2164-
return [CompletionItem(cur_key, self.aliases[cur_key]) for cur_key in self.aliases]
2170+
"""Return list of alias names and values as CompletionItems"""
2171+
results: List[CompletionItem] = []
2172+
2173+
for cur_key in self.aliases:
2174+
row_data = [self.aliases[cur_key]]
2175+
results.append(CompletionItem(cur_key, self._alias_completion_table.generate_data_row(row_data)))
2176+
2177+
return results
2178+
2179+
# Table displayed when tab completing macros
2180+
_macro_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None)
21652181

21662182
def _get_macro_completion_items(self) -> List[CompletionItem]:
2167-
"""Return list of current macro names and values as CompletionItems"""
2168-
return [CompletionItem(cur_key, self.macros[cur_key].value) for cur_key in self.macros]
2183+
"""Return list of macro names and values as CompletionItems"""
2184+
results: List[CompletionItem] = []
2185+
2186+
for cur_key in self.macros:
2187+
row_data = [self.macros[cur_key].value]
2188+
results.append(CompletionItem(cur_key, self._macro_completion_table.generate_data_row(row_data)))
2189+
2190+
return results
2191+
2192+
# Table displayed when tab completing Settables
2193+
_settable_completion_table = SimpleTable([Column('Value', width=30), Column('Description', width=60)], divider_char=None)
21692194

21702195
def _get_settable_completion_items(self) -> List[CompletionItem]:
2171-
"""Return list of current settable names and descriptions as CompletionItems"""
2172-
return [CompletionItem(cur_key, self.settables[cur_key].description) for cur_key in self.settables]
2196+
"""Return list of Settable names, values, and descriptions as CompletionItems"""
2197+
results: List[CompletionItem] = []
2198+
2199+
for cur_key in self.settables:
2200+
row_data = [self.settables[cur_key].get_value(), self.settables[cur_key].description]
2201+
results.append(CompletionItem(cur_key, self._settable_completion_table.generate_data_row(row_data)))
2202+
2203+
return results
21732204

21742205
def _get_commands_aliases_and_macros_for_completion(self) -> List[str]:
21752206
"""Return a list of visible commands, aliases, and macros for tab completion"""
@@ -3172,7 +3203,7 @@ def _alias_create(self, args: argparse.Namespace) -> None:
31723203
nargs=argparse.ZERO_OR_MORE,
31733204
help='alias(es) to delete',
31743205
choices_provider=_get_alias_completion_items,
3175-
descriptive_header='Value',
3206+
descriptive_header=_alias_completion_table.generate_header(),
31763207
)
31773208

31783209
@as_subcommand_to('alias', 'delete', alias_delete_parser, help=alias_delete_help)
@@ -3206,7 +3237,7 @@ def _alias_delete(self, args: argparse.Namespace) -> None:
32063237
nargs=argparse.ZERO_OR_MORE,
32073238
help='alias(es) to list',
32083239
choices_provider=_get_alias_completion_items,
3209-
descriptive_header='Value',
3240+
descriptive_header=_alias_completion_table.generate_header(),
32103241
)
32113242

32123243
@as_subcommand_to('alias', 'list', alias_list_parser, help=alias_list_help)
@@ -3400,7 +3431,7 @@ def _macro_create(self, args: argparse.Namespace) -> None:
34003431
nargs=argparse.ZERO_OR_MORE,
34013432
help='macro(s) to delete',
34023433
choices_provider=_get_macro_completion_items,
3403-
descriptive_header='Value',
3434+
descriptive_header=_macro_completion_table.generate_header(),
34043435
)
34053436

34063437
@as_subcommand_to('macro', 'delete', macro_delete_parser, help=macro_delete_help)
@@ -3434,7 +3465,7 @@ def _macro_delete(self, args: argparse.Namespace) -> None:
34343465
nargs=argparse.ZERO_OR_MORE,
34353466
help='macro(s) to list',
34363467
choices_provider=_get_macro_completion_items,
3437-
descriptive_header='Value',
3468+
descriptive_header=_macro_completion_table.generate_header(),
34383469
)
34393470

34403471
@as_subcommand_to('macro', 'list', macro_list_parser, help=macro_list_help)
@@ -3556,6 +3587,80 @@ def do_help(self, args: argparse.Namespace) -> None:
35563587
# Set apply_style to False so help_error's style is not overridden
35573588
self.perror(err_msg, apply_style=False)
35583589

3590+
def print_topics(self, header: str, cmds: Optional[List[str]], cmdlen: int, maxcol: int) -> None:
3591+
"""
3592+
Print groups of commands and topics in columns and an optional header
3593+
Override of cmd's print_topics() to handle headers with newlines, ANSI style sequences, and wide characters
3594+
3595+
:param header: string to print above commands being printed
3596+
:param cmds: list of topics to print
3597+
:param cmdlen: unused, even by cmd's version
3598+
:param maxcol: max number of display columns to fit into
3599+
"""
3600+
if cmds:
3601+
self.poutput(header)
3602+
if self.ruler:
3603+
divider = utils.align_left('', fill_char=self.ruler, width=ansi.widest_line(header))
3604+
self.poutput(divider)
3605+
self.columnize(cmds, maxcol - 1)
3606+
self.poutput()
3607+
3608+
def columnize(self, str_list: Optional[List[str]], display_width: int = 80) -> None:
3609+
"""Display a list of single-line strings as a compact set of columns.
3610+
Override of cmd's print_topics() to handle strings with ANSI style sequences and wide characters
3611+
3612+
Each column is only as wide as necessary.
3613+
Columns are separated by two spaces (one was not legible enough).
3614+
"""
3615+
if not str_list:
3616+
self.poutput("<empty>")
3617+
return
3618+
3619+
nonstrings = [i for i in range(len(str_list)) if not isinstance(str_list[i], str)]
3620+
if nonstrings:
3621+
raise TypeError(f"str_list[i] not a string for i in {nonstrings}")
3622+
size = len(str_list)
3623+
if size == 1:
3624+
self.poutput(str_list[0])
3625+
return
3626+
# Try every row count from 1 upwards
3627+
for nrows in range(1, len(str_list)):
3628+
ncols = (size + nrows - 1) // nrows
3629+
colwidths = []
3630+
totwidth = -2
3631+
for col in range(ncols):
3632+
colwidth = 0
3633+
for row in range(nrows):
3634+
i = row + nrows * col
3635+
if i >= size:
3636+
break
3637+
x = str_list[i]
3638+
colwidth = max(colwidth, ansi.style_aware_wcswidth(x))
3639+
colwidths.append(colwidth)
3640+
totwidth += colwidth + 2
3641+
if totwidth > display_width:
3642+
break
3643+
if totwidth <= display_width:
3644+
break
3645+
else:
3646+
nrows = len(str_list)
3647+
ncols = 1
3648+
colwidths = [0]
3649+
for row in range(nrows):
3650+
texts = []
3651+
for col in range(ncols):
3652+
i = row + nrows * col
3653+
if i >= size:
3654+
x = ""
3655+
else:
3656+
x = str_list[i]
3657+
texts.append(x)
3658+
while texts and not texts[-1]:
3659+
del texts[-1]
3660+
for col in range(len(texts)):
3661+
texts[col] = utils.align_left(texts[col], width=colwidths[col])
3662+
self.poutput(" ".join(texts))
3663+
35593664
def _help_menu(self, verbose: bool = False) -> None:
35603665
"""Show a list of commands which help can be displayed for"""
35613666
cmds_cats, cmds_doc, cmds_undoc, help_topics = self._build_command_info()
@@ -3613,25 +3718,26 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
36133718
if not verbose:
36143719
self.print_topics(header, cmds, 15, 80)
36153720
else:
3616-
self.poutput(f'{header}')
3617-
widest = 0
3618-
# measure the commands
3619-
for command in cmds:
3620-
width = ansi.style_aware_wcswidth(command)
3621-
if width > widest:
3622-
widest = width
3623-
# add a 4-space pad
3624-
widest += 4
3625-
if widest < 20:
3626-
widest = 20
3627-
3628-
if self.ruler:
3629-
ruler_line = utils.align_left('', width=80, fill_char=self.ruler)
3630-
self.poutput(f'{ruler_line}')
3721+
# Find the widest command
3722+
widest = max([ansi.style_aware_wcswidth(command) for command in cmds])
3723+
3724+
# Define the table structure
3725+
name_column = Column('', width=max(widest, 20))
3726+
desc_column = Column('', width=80)
3727+
3728+
topic_table = SimpleTable([name_column, desc_column], divider_char=self.ruler)
3729+
3730+
# Build the topic table
3731+
table_str_buf = io.StringIO()
3732+
if header:
3733+
table_str_buf.write(header + "\n")
3734+
3735+
divider = topic_table.generate_divider()
3736+
if divider:
3737+
table_str_buf.write(divider + "\n")
36313738

36323739
# Try to get the documentation string for each command
36333740
topics = self.get_help_topics()
3634-
36353741
for command in cmds:
36363742
cmd_func = self.cmd_func(command)
36373743
doc: Optional[str]
@@ -3658,10 +3764,8 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
36583764
doc = cmd_func.__doc__
36593765

36603766
# Attempt to locate the first documentation block
3661-
if not doc:
3662-
doc_block = ['']
3663-
else:
3664-
doc_block = []
3767+
cmd_desc = ''
3768+
if doc:
36653769
found_first = False
36663770
for doc_line in doc.splitlines():
36673771
stripped_line = doc_line.strip()
@@ -3671,15 +3775,18 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
36713775
if found_first:
36723776
break
36733777
elif stripped_line:
3674-
doc_block.append(stripped_line)
3778+
if found_first:
3779+
cmd_desc += "\n"
3780+
cmd_desc += stripped_line
36753781
found_first = True
36763782
elif found_first:
36773783
break
36783784

3679-
for doc_line in doc_block:
3680-
self.poutput(f'{command: <{widest}}{doc_line}')
3681-
command = ''
3682-
self.poutput()
3785+
# Add this command to the table
3786+
table_row = topic_table.generate_data_row([command, cmd_desc])
3787+
table_str_buf.write(table_row + '\n')
3788+
3789+
self.poutput(table_str_buf.getvalue())
36833790

36843791
shortcuts_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="List available shortcuts")
36853792

@@ -3806,15 +3913,12 @@ def complete_set_value(
38063913
"Call with just param to view that parameter's value."
38073914
)
38083915
set_parser_parent = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=set_description, add_help=False)
3809-
set_parser_parent.add_argument(
3810-
'-v', '--verbose', action='store_true', help='include description of parameters when viewing'
3811-
)
38123916
set_parser_parent.add_argument(
38133917
'param',
38143918
nargs=argparse.OPTIONAL,
38153919
help='parameter to set or view',
38163920
choices_provider=_get_settable_completion_items,
3817-
descriptive_header='Description',
3921+
descriptive_header=_settable_completion_table.generate_header(),
38183922
)
38193923

38203924
# Create the parser for the set command
@@ -3856,21 +3960,25 @@ def do_set(self, args: argparse.Namespace) -> None:
38563960
# Show all settables
38573961
to_show = list(self.settables.keys())
38583962

3859-
# Build the result strings
3860-
max_len = 0
3861-
results = dict()
3862-
for param in to_show:
3963+
# Define the table structure
3964+
name_label = 'Name'
3965+
max_name_width = max([ansi.style_aware_wcswidth(param) for param in to_show])
3966+
max_name_width = max(max_name_width, ansi.style_aware_wcswidth(name_label))
3967+
3968+
cols: List[Column] = [
3969+
Column(name_label, width=max_name_width),
3970+
Column('Value', width=30),
3971+
Column('Description', width=60),
3972+
]
3973+
3974+
table = SimpleTable(cols, divider_char=self.ruler)
3975+
self.poutput(table.generate_header())
3976+
3977+
# Build the table
3978+
for param in sorted(to_show, key=self.default_sort_key):
38633979
settable = self.settables[param]
3864-
results[param] = f"{param}: {settable.get_value()!r}"
3865-
max_len = max(max_len, ansi.style_aware_wcswidth(results[param]))
3866-
3867-
# Display the results
3868-
for param in sorted(results, key=self.default_sort_key):
3869-
result_str = results[param]
3870-
if args.verbose:
3871-
self.poutput(f'{utils.align_left(result_str, width=max_len)} # {self.settables[param].description}')
3872-
else:
3873-
self.poutput(result_str)
3980+
row_data = [param, settable.get_value(), settable.description]
3981+
self.poutput(table.generate_data_row(row_data))
38743982

38753983
shell_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description="Execute a command as if at the OS prompt")
38763984
shell_parser.add_argument('command', help='the command to run', completer=shell_cmd_complete)
@@ -4070,7 +4178,6 @@ def _run_python(self, *, pyscript: Optional[str] = None) -> Optional[bool]:
40704178
If pyscript is None, then this function runs an interactive Python shell.
40714179
Otherwise, it runs the pyscript file.
40724180
4073-
:param args: Namespace of args on the command line
40744181
:param pyscript: optional path to a pyscript file to run. This is intended only to be used by do_run_pyscript()
40754182
after it sets up sys.argv for the script. (Defaults to None)
40764183
:return: True if running of commands should stop

0 commit comments

Comments
 (0)