Skip to content

Commit 50161c4

Browse files
committed
Merge branch 'main' into prek
2 parents c4bcdc1 + 9f8dbea commit 50161c4

13 files changed

Lines changed: 408 additions & 236 deletions

CHANGELOG.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,15 @@ prompt is displayed.
3737
longer needed
3838
- `completer` functions must now return a `cmd2.Completions` object instead of `list[str]`.
3939
- `choices_provider` functions must now return a `cmd2.Choices` object instead of `list[str]`.
40-
- An argparse argument's `descriptive_headers` field is now called `table_header`.
41-
- `CompletionItem.descriptive_data` is now called `CompletionItem.table_row`.
40+
- An argparse argument's `descriptive_headers` field is now called `table_columns`.
41+
- `CompletionItem.descriptive_data` is now called `CompletionItem.table_data`.
42+
- Removed `DEFAULT_DESCRIPTIVE_HEADERS`. This means you must define `table_columns` when using
43+
`CompletionItem.table_data` data.
4244
- `Cmd.default_sort_key` moved to `utils.DEFAULT_STR_SORT_KEY`.
4345
- Moved completion state data, which previously resided in `Cmd`, into other classes.
4446
- `Cmd.matches_sorted` -> `Completions.is_sorted` and `Choices.is_sorted`
45-
- `Cmd.completion_hint` -> `Completions.completion_hint`
46-
- `Cmd.formatted_completions` -> `Completions.completion_table`
47+
- `Cmd.completion_hint` -> `Completions.hint`
48+
- `Cmd.formatted_completions` -> `Completions.table` (Now a Rich Table)
4749
- `Cmd.allow_appended_space/allow_closing_quote` -> `Completions.allow_finalization`
4850
- Removed `Cmd.matches_delimited` since it's no longer used.
4951
- Removed `flag_based_complete` and `index_based_complete` functions since their functionality

cmd2/argparse_completer.py

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from rich.text import Text
2626

2727
from .constants import INFINITY
28-
from .rich_utils import Cmd2GeneralConsole
2928

3029
if TYPE_CHECKING: # pragma: no cover
3130
from .cmd2 import Cmd
@@ -49,9 +48,6 @@
4948
from .exceptions import CompletionError
5049
from .styles import Cmd2Style
5150

52-
# If no table header is supplied, then this will be used instead
53-
DEFAULT_TABLE_HEADER: Sequence[str | Column] = ['Description']
54-
5551
# Name of the choice/completer function argument that, if present, will be passed a dictionary of
5652
# command line tokens up through the token being completed mapped to their argparse destination name.
5753
ARG_TOKENS = 'arg_tokens'
@@ -500,11 +496,11 @@ def _handle_last_token(
500496

501497
# If we have results, then return them
502498
if completions:
503-
if not completions.completion_hint:
499+
if not completions.hint:
504500
# Add a hint even though there are results in case Cmd.always_show_hint is True.
505501
completions = dataclasses.replace(
506502
completions,
507-
completion_hint=_build_hint(self._parser, flag_arg_state.action),
503+
hint=_build_hint(self._parser, flag_arg_state.action),
508504
)
509505

510506
return completions
@@ -528,11 +524,11 @@ def _handle_last_token(
528524

529525
# If we have results, then return them
530526
if completions:
531-
if not completions.completion_hint:
527+
if not completions.hint:
532528
# Add a hint even though there are results in case Cmd.always_show_hint is True.
533529
completions = dataclasses.replace(
534530
completions,
535-
completion_hint=_build_hint(self._parser, pos_arg_state.action),
531+
hint=_build_hint(self._parser, pos_arg_state.action),
536532
)
537533
return completions
538534

@@ -592,15 +588,52 @@ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, used_f
592588

593589
return Completions(items)
594590

595-
def _format_completions(self, arg_state: _ArgumentState, completions: Completions) -> Completions:
596-
"""Format CompletionItems into completion table."""
597-
# Skip table generation for single results or if the list exceeds the
598-
# user-defined threshold for table display.
599-
if len(completions) < 2 or len(completions) > self._cmd2_app.max_completion_table_items:
600-
return completions
591+
@staticmethod
592+
def _validate_table_data(arg_state: _ArgumentState, completions: Completions) -> None:
593+
"""Verify the integrity of completion table data.
601594
602-
# Ensure every item provides table metadata to avoid an incomplete table.
603-
if not all(item.table_row for item in completions):
595+
:raises ValueError: if there is an error with the data.
596+
"""
597+
table_columns = arg_state.action.get_table_columns() # type: ignore[attr-defined]
598+
has_table_data = any(item.table_data for item in completions)
599+
600+
if table_columns is None:
601+
if has_table_data:
602+
raise ValueError(
603+
f"Argument '{arg_state.action.dest}' has CompletionItems with table_data, "
604+
f"but no table_columns were defined in add_argument()."
605+
)
606+
return
607+
608+
# If columns are defined, then every item must have data, and lengths must match
609+
for item in completions:
610+
if not item.table_data:
611+
raise ValueError(
612+
f"Argument '{arg_state.action.dest}' has table_columns defined, "
613+
f"but the CompletionItem for '{item.text}' is missing table_data."
614+
)
615+
if len(item.table_data) != len(table_columns):
616+
raise ValueError(
617+
f"Argument '{arg_state.action.dest}': table_data length ({len(item.table_data)}) "
618+
f"does not match table_columns length ({len(table_columns)}) for item '{item.text}'."
619+
)
620+
621+
def _build_completion_table(self, arg_state: _ArgumentState, completions: Completions) -> Completions:
622+
"""Build a rich.Table for completion results if applicable."""
623+
# Verify integrity of completion data
624+
self._validate_table_data(arg_state, completions)
625+
626+
table_columns = cast(
627+
Sequence[str | Column] | None,
628+
arg_state.action.get_table_columns(), # type: ignore[attr-defined]
629+
)
630+
631+
# Skip table generation if results are outside thresholds or no columns are defined
632+
if (
633+
len(completions) < 2
634+
or len(completions) > self._cmd2_app.max_completion_table_items
635+
or table_columns is None
636+
): # fmt: skip
604637
return completions
605638

606639
# If a metavar was defined, use that instead of the dest field
@@ -620,26 +653,18 @@ def _format_completions(self, arg_state: _ArgumentState, completions: Completion
620653
# Build header row
621654
rich_columns: list[Column] = []
622655
rich_columns.append(Column(destination.upper(), justify="right" if all_nums else "left", no_wrap=True))
623-
table_header = cast(Sequence[str | Column] | None, arg_state.action.get_table_header()) # type: ignore[attr-defined]
624-
if table_header is None:
625-
table_header = DEFAULT_TABLE_HEADER
626656
rich_columns.extend(
627-
column if isinstance(column, Column) else Column(column, overflow="fold") for column in table_header
657+
column if isinstance(column, Column) else Column(column, overflow="fold") for column in table_columns
628658
)
629659

630-
# Add the data rows
631-
hint_table = Table(*rich_columns, box=SIMPLE_HEAD, show_edge=False, border_style=Cmd2Style.TABLE_BORDER)
660+
# Build the table
661+
table = Table(*rich_columns, box=SIMPLE_HEAD, show_edge=False, border_style=Cmd2Style.TABLE_BORDER)
632662
for item in completions:
633-
hint_table.add_row(Text.from_ansi(item.display), *item.table_row)
634-
635-
# Generate the table string
636-
console = Cmd2GeneralConsole(file=self._cmd2_app.stdout)
637-
with console.capture() as capture:
638-
console.print(hint_table, end="", soft_wrap=False)
663+
table.add_row(Text.from_ansi(item.display), *item.table_data)
639664

640665
return dataclasses.replace(
641666
completions,
642-
completion_table=capture.get(),
667+
table=table,
643668
)
644669

645670
def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: Sequence[str]) -> Completions:
@@ -780,7 +805,7 @@ def _complete_arg(
780805
filtered = [choice for choice in all_choices if choice.text not in used_values]
781806
completions = self._cmd2_app.basic_complete(text, line, begidx, endidx, filtered)
782807

783-
return self._format_completions(arg_state, completions)
808+
return self._build_completion_table(arg_state, completions)
784809

785810

786811
# The default ArgparseCompleter class for a cmd2 app

cmd2/argparse_custom.py

Lines changed: 46 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) -> Completions
127127
128128
1. display - string for displaying the completion differently in the completion menu
129129
2. display_meta - meta information about completion which displays in the completion menu
130-
3. table_row - row data for completion tables
130+
3. table_data - supplemental data for completion tables
131131
132132
They can also be used as argparse choices. When a ``CompletionItem`` is created, it
133133
stores the original value (e.g. ID number) and makes it accessible through a property
@@ -139,8 +139,8 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) -> Completions
139139
These were added to help in cases where uninformative data is being completed.
140140
For instance, completing ID numbers isn't very helpful to a user without context.
141141
142-
Providing ``table_row`` data in your ``CompletionItem`` signals ArgparseCompleter
143-
to output the completion results in a table with descriptive data instead of just a table
142+
Providing ``table_data`` in your ``CompletionItem`` signals ArgparseCompleter
143+
to output the completion results in a table with supplemental data instead of just a table
144144
of tokens::
145145
146146
Instead of this:
@@ -155,35 +155,34 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens) -> Completions
155155
156156
157157
The left-most column is the actual value being completed and its header is
158-
that value's name. The right column header is defined using the
159-
``table_header`` parameter of add_argument(), which is a list of header
160-
names that defaults to ["Description"]. The right column values come from the
161-
``table_row`` argument to ``CompletionItem``. It's a ``Sequence`` with the
162-
same number of items as ``table_header``.
158+
that value's name. Any additional column headers are defined using the
159+
``table_columns`` parameter of add_argument(), which is a list of header
160+
names. The supplemental column values come from the
161+
``table_data`` argument to ``CompletionItem``. It's a ``Sequence`` with the
162+
same number of items as ``table_columns``.
163163
164164
Example::
165165
166-
Add an argument and define its table_header.
166+
Add an argument and define its table_columns.
167167
168168
parser.add_argument(
169-
add_argument(
170169
"item_id",
171170
type=int,
172171
choices_provider=get_choices,
173-
table_header=["Item Name", "Checked Out", "Due Date"],
172+
table_columns=["Item Name", "Checked Out", "Due Date"],
174173
)
175174
176175
Implement the choices_provider to return Choices.
177176
178177
def get_choices(self) -> Choices:
179178
\"\"\"choices_provider which returns CompletionItems\"\"\"
180179
181-
# Populate CompletionItem's table_row argument.
182-
# Its item count should match that of table_header.
180+
# Populate CompletionItem's table_data argument.
181+
# Its item count should match that of table_columns.
183182
items = [
184-
CompletionItem(1, table_row=["My item", True, "02/02/2022"]),
185-
CompletionItem(2, table_row=["Another item", False, ""]),
186-
CompletionItem(3, table_row=["Yet another item", False, ""]),
183+
CompletionItem(1, table_data=["My item", True, "02/02/2022"]),
184+
CompletionItem(2, table_data=["Another item", False, ""]),
185+
CompletionItem(3, table_data=["Yet another item", False, ""]),
187186
]
188187
return Choices(items)
189188
@@ -195,7 +194,7 @@ def get_choices(self) -> Choices:
195194
2 Another item False
196195
3 Yet another item False
197196
198-
``table_header`` can be strings or ``Rich.table.Columns`` for more
197+
``table_columns`` can be strings or ``Rich.table.Columns`` for more
199198
control over things like alignment.
200199
201200
- If a header is a string, it will render as a left-aligned column with its
@@ -207,9 +206,9 @@ def get_choices(self) -> Choices:
207206
truncated with an ellipsis at the end. You can override this and other settings
208207
when you create the ``Column``.
209208
210-
``table_row`` items can include Rich objects, including styled Text and Tables.
209+
``table_data`` items can include Rich objects, including styled Text and Tables.
211210
212-
To avoid printing a excessive information to the screen at once when a user
211+
To avoid printing excessive information to the screen at once when a user
213212
presses tab, there is a maximum threshold for the number of ``CompletionItems``
214213
that will be shown. Its value is defined in ``cmd2.Cmd.max_completion_table_items``.
215214
It defaults to 50, but can be changed. If the number of completion suggestions
@@ -240,8 +239,8 @@ def get_choices(self) -> Choices:
240239
- ``argparse.Action.get_choices_callable()`` - See `action_get_choices_callable` for more details.
241240
- ``argparse.Action.set_choices_provider()`` - See `_action_set_choices_provider` for more details.
242241
- ``argparse.Action.set_completer()`` - See `_action_set_completer` for more details.
243-
- ``argparse.Action.get_table_header()`` - See `_action_get_table_header` for more details.
244-
- ``argparse.Action.set_table_header()`` - See `_action_set_table_header` for more details.
242+
- ``argparse.Action.get_table_columns()`` - See `_action_get_table_columns` for more details.
243+
- ``argparse.Action.set_table_columns()`` - See `_action_set_table_columns` for more details.
245244
- ``argparse.Action.get_nargs_range()`` - See `_action_get_nargs_range` for more details.
246245
- ``argparse.Action.set_nargs_range()`` - See `_action_set_nargs_range` for more details.
247246
- ``argparse.Action.get_suppress_tab_hint()`` - See `_action_get_suppress_tab_hint` for more details.
@@ -309,21 +308,21 @@ def get_choices(self) -> Choices:
309308

310309
def generate_range_error(range_min: int, range_max: float) -> str:
311310
"""Generate an error message when the the number of arguments provided is not within the expected range."""
312-
err_str = "expected "
311+
err_msg = "expected "
313312

314313
if range_max == constants.INFINITY:
315314
plural = '' if range_min == 1 else 's'
316-
err_str += f"at least {range_min}"
315+
err_msg += f"at least {range_min}"
317316
else:
318317
plural = '' if range_max == 1 else 's'
319318
if range_min == range_max:
320-
err_str += f"{range_min}"
319+
err_msg += f"{range_min}"
321320
else:
322-
err_str += f"{range_min} to {range_max}"
321+
err_msg += f"{range_min} to {range_max}"
323322

324-
err_str += f" argument{plural}"
323+
err_msg += f" argument{plural}"
325324

326-
return err_str
325+
return err_msg
327326

328327

329328
def set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
@@ -418,8 +417,8 @@ def completer(self) -> CompleterUnbound[CmdOrSet]:
418417
# ChoicesCallable object that specifies the function to be called which provides choices to the argument
419418
ATTR_CHOICES_CALLABLE = 'choices_callable'
420419

421-
# A completion table header
422-
ATTR_TABLE_HEADER = 'table_header'
420+
# Completion table columns
421+
ATTR_TABLE_COLUMNS = 'table_columns'
423422

424423
# A tuple specifying nargs as a range (min, max)
425424
ATTR_NARGS_RANGE = 'nargs_range'
@@ -516,38 +515,38 @@ def _action_set_completer(
516515

517516

518517
############################################################################################################
519-
# Patch argparse.Action with accessors for table_header attribute
518+
# Patch argparse.Action with accessors for table_columns attribute
520519
############################################################################################################
521-
def _action_get_table_header(self: argparse.Action) -> Sequence[str | Column] | None:
522-
"""Get the table_header attribute of an argparse Action.
520+
def _action_get_table_columns(self: argparse.Action) -> Sequence[str | Column] | None:
521+
"""Get the table_columns attribute of an argparse Action.
523522
524-
This function is added by cmd2 as a method called ``get_table_header()`` to ``argparse.Action`` class.
523+
This function is added by cmd2 as a method called ``get_table_columns()`` to ``argparse.Action`` class.
525524
526-
To call: ``action.get_table_header()``
525+
To call: ``action.get_table_columns()``
527526
528527
:param self: argparse Action being queried
529-
:return: The value of table_header or None if attribute does not exist
528+
:return: The value of table_columns or None if attribute does not exist
530529
"""
531-
return cast(Sequence[str | Column] | None, getattr(self, ATTR_TABLE_HEADER, None))
530+
return cast(Sequence[str | Column] | None, getattr(self, ATTR_TABLE_COLUMNS, None))
532531

533532

534-
setattr(argparse.Action, 'get_table_header', _action_get_table_header)
533+
setattr(argparse.Action, 'get_table_columns', _action_get_table_columns)
535534

536535

537-
def _action_set_table_header(self: argparse.Action, table_header: Sequence[str | Column] | None) -> None:
538-
"""Set the table_header attribute of an argparse Action.
536+
def _action_set_table_columns(self: argparse.Action, table_columns: Sequence[str | Column] | None) -> None:
537+
"""Set the table_columns attribute of an argparse Action.
539538
540-
This function is added by cmd2 as a method called ``set_table_header()`` to ``argparse.Action`` class.
539+
This function is added by cmd2 as a method called ``set_table_columns()`` to ``argparse.Action`` class.
541540
542-
To call: ``action.set_table_header(table_header)``
541+
To call: ``action.set_table_columns(table_columns)``
543542
544543
:param self: argparse Action being updated
545-
:param table_header: value being assigned
544+
:param table_columns: value being assigned
546545
"""
547-
setattr(self, ATTR_TABLE_HEADER, table_header)
546+
setattr(self, ATTR_TABLE_COLUMNS, table_columns)
548547

549548

550-
setattr(argparse.Action, 'set_table_header', _action_set_table_header)
549+
setattr(argparse.Action, 'set_table_columns', _action_set_table_columns)
551550

552551

553552
############################################################################################################
@@ -698,7 +697,7 @@ def _add_argument_wrapper(
698697
choices_provider: ChoicesProviderUnbound[CmdOrSet] | None = None,
699698
completer: CompleterUnbound[CmdOrSet] | None = None,
700699
suppress_tab_hint: bool = False,
701-
table_header: Sequence[str | Column] | None = None,
700+
table_columns: Sequence[str | Column] | None = None,
702701
**kwargs: Any,
703702
) -> argparse.Action:
704703
"""Wrap ActionsContainer.add_argument() which supports more settings used by cmd2.
@@ -718,7 +717,7 @@ def _add_argument_wrapper(
718717
current argument's help text as a hint. Set this to True to suppress the hint. If this
719718
argument's help text is set to argparse.SUPPRESS, then tab hints will not display
720719
regardless of the value passed for suppress_tab_hint. Defaults to False.
721-
:param table_header: optional header for when displaying a completion table. Defaults to None.
720+
:param table_columns: optional headers for when displaying a completion table. Defaults to None.
722721
723722
# Args from original function
724723
:param kwargs: keyword-arguments recognized by argparse._ActionsContainer.add_argument
@@ -809,7 +808,7 @@ def _add_argument_wrapper(
809808
new_arg.set_completer(completer) # type: ignore[attr-defined]
810809

811810
new_arg.set_suppress_tab_hint(suppress_tab_hint) # type: ignore[attr-defined]
812-
new_arg.set_table_header(table_header) # type: ignore[attr-defined]
811+
new_arg.set_table_columns(table_columns) # type: ignore[attr-defined]
813812

814813
for keyword, value in custom_attribs.items():
815814
attr_setter = getattr(new_arg, f'set_{keyword}', None)

0 commit comments

Comments
 (0)