Skip to content

Commit 8e0a1a2

Browse files
All tables in cmd2 are now rich-based. (#1475)
* All tables in cmd2 are now rich-based, including those used in tab completion. * Added a StrEnum for all cmd2 text styles. * Force UTF-8 Unicode encoding when running tests both locally and in GitHub Actions * Fixed issue where colors were being stripped by pyreadline3. --------- Co-authored-by: Todd Leonhardt <[email protected]>
1 parent a7129c5 commit 8e0a1a2

File tree

15 files changed

+332
-344
lines changed

15 files changed

+332
-344
lines changed

.github/workflows/tests.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ jobs:
3636
run: uv sync --all-extras --dev
3737

3838
- name: Run tests
39-
run: uv run python -m pytest --cov --cov-config=pyproject.toml --cov-report=xml tests
39+
run: uv run python -Xutf8 -m pytest --cov --cov-config=pyproject.toml --cov-report=xml tests
4040

4141
- name: Run isolated tests
4242
run:
43-
uv run python -m pytest --cov --cov-config=pyproject.toml --cov-report=xml tests_isolated
43+
uv run python -Xutf8 -m pytest --cov --cov-config=pyproject.toml --cov-report=xml
44+
tests_isolated
4445

4546
- name: Upload test results to Codecov
4647
if: ${{ !cancelled() }}

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ typecheck: ## Perform type checking
3333
.PHONY: test
3434
test: ## Test the code with pytest.
3535
@echo "🚀 Testing code: Running pytest"
36-
@uv run python -m pytest --cov --cov-config=pyproject.toml --cov-report=xml tests
37-
@uv run python -m pytest --cov --cov-config=pyproject.toml --cov-report=xml tests_isolated
36+
@uv run python -Xutf8 -m pytest --cov --cov-config=pyproject.toml --cov-report=xml tests
37+
@uv run python -Xutf8 -m pytest --cov --cov-config=pyproject.toml --cov-report=xml tests_isolated
3838

3939
.PHONY: docs-test
4040
docs-test: ## Test if documentation can be built without warnings or errors

cmd2/argparse_completer.py

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,34 @@
66
import argparse
77
import inspect
88
import numbers
9+
import sys
910
from collections import (
1011
deque,
1112
)
13+
from collections.abc import Sequence
1214
from typing import (
1315
IO,
1416
TYPE_CHECKING,
1517
cast,
1618
)
1719

18-
from .ansi import (
19-
style_aware_wcswidth,
20-
widest_line,
21-
)
2220
from .constants import (
2321
INFINITY,
2422
)
23+
from .rich_utils import (
24+
Cmd2Console,
25+
Cmd2Style,
26+
)
2527

2628
if TYPE_CHECKING: # pragma: no cover
2729
from .cmd2 import (
2830
Cmd,
2931
)
3032

33+
34+
from rich.box import SIMPLE_HEAD
35+
from rich.table import Column, Table
36+
3137
from .argparse_custom import (
3238
ChoicesCallable,
3339
ChoicesProviderFuncWithTokens,
@@ -40,14 +46,9 @@
4046
from .exceptions import (
4147
CompletionError,
4248
)
43-
from .table_creator import (
44-
Column,
45-
HorizontalAlignment,
46-
SimpleTable,
47-
)
4849

49-
# If no descriptive header is supplied, then this will be used instead
50-
DEFAULT_DESCRIPTIVE_HEADER = 'Description'
50+
# If no descriptive headers are supplied, then this will be used instead
51+
DEFAULT_DESCRIPTIVE_HEADERS: Sequence[str | Column] = ('Description',)
5152

5253
# Name of the choice/completer function argument that, if present, will be passed a dictionary of
5354
# command line tokens up through the token being completed mapped to their argparse destination name.
@@ -546,8 +547,6 @@ def _format_completions(self, arg_state: _ArgumentState, completions: list[str]
546547

547548
# Check if there are too many CompletionItems to display as a table
548549
if len(completions) <= self._cmd2_app.max_completion_items:
549-
four_spaces = 4 * ' '
550-
551550
# If a metavar was defined, use that instead of the dest field
552551
destination = arg_state.action.metavar if arg_state.action.metavar else arg_state.action.dest
553552

@@ -560,39 +559,45 @@ def _format_completions(self, arg_state: _ArgumentState, completions: list[str]
560559
tuple_index = min(len(destination) - 1, arg_state.count)
561560
destination = destination[tuple_index]
562561

563-
desc_header = arg_state.action.get_descriptive_header() # type: ignore[attr-defined]
564-
if desc_header is None:
565-
desc_header = DEFAULT_DESCRIPTIVE_HEADER
566-
567-
# Replace tabs with 4 spaces so we can calculate width
568-
desc_header = desc_header.replace('\t', four_spaces)
562+
desc_headers = cast(Sequence[str | Column] | None, arg_state.action.get_descriptive_headers()) # type: ignore[attr-defined]
563+
if desc_headers is None:
564+
desc_headers = DEFAULT_DESCRIPTIVE_HEADERS
569565

570-
# Calculate needed widths for the token and description columns of the table
571-
token_width = style_aware_wcswidth(destination)
572-
desc_width = widest_line(desc_header)
573-
574-
for item in completion_items:
575-
token_width = max(style_aware_wcswidth(item), token_width)
576-
577-
# Replace tabs with 4 spaces so we can calculate width
578-
item.description = item.description.replace('\t', four_spaces)
579-
desc_width = max(widest_line(item.description), desc_width)
580-
581-
cols = []
582-
dest_alignment = HorizontalAlignment.RIGHT if all_nums else HorizontalAlignment.LEFT
583-
cols.append(
566+
# Build all headers for the hint table
567+
headers: list[Column] = []
568+
headers.append(
584569
Column(
585570
destination.upper(),
586-
width=token_width,
587-
header_horiz_align=dest_alignment,
588-
data_horiz_align=dest_alignment,
571+
justify="right" if all_nums else "left",
572+
no_wrap=True,
589573
)
590574
)
591-
cols.append(Column(desc_header, width=desc_width))
575+
for desc_header in desc_headers:
576+
header = (
577+
desc_header
578+
if isinstance(desc_header, Column)
579+
else Column(
580+
desc_header,
581+
overflow="fold",
582+
)
583+
)
584+
headers.append(header)
585+
586+
# Build the hint table
587+
hint_table = Table(
588+
*headers,
589+
box=SIMPLE_HEAD,
590+
show_edge=False,
591+
border_style=Cmd2Style.RULE_LINE,
592+
)
593+
for item in completion_items:
594+
hint_table.add_row(item, *item.descriptive_data)
592595

593-
hint_table = SimpleTable(cols, divider_char=self._cmd2_app.ruler)
594-
table_data = [[item, item.description] for item in completion_items]
595-
self._cmd2_app.formatted_completions = hint_table.generate_table(table_data, row_spacing=0)
596+
# Generate the hint table string
597+
console = Cmd2Console(sys.stdout)
598+
with console.capture() as capture:
599+
console.print(hint_table)
600+
self._cmd2_app.formatted_completions = capture.get()
596601

597602
# Return sorted list of completions
598603
return cast(list[str], completions)

0 commit comments

Comments
 (0)