Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ jobs:
run: uv sync --all-extras --dev

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

- name: Run isolated tests
run:
uv run python -m pytest --cov --cov-config=pyproject.toml --cov-report=xml tests_isolated
uv run python -Xutf8 -m pytest --cov --cov-config=pyproject.toml --cov-report=xml
tests_isolated

- name: Upload test results to Codecov
if: ${{ !cancelled() }}
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ typecheck: ## Perform type checking
.PHONY: test
test: ## Test the code with pytest.
@echo "🚀 Testing code: Running pytest"
@uv run python -m pytest --cov --cov-config=pyproject.toml --cov-report=xml tests
@uv run python -m pytest --cov --cov-config=pyproject.toml --cov-report=xml tests_isolated
@uv run python -Xutf8 -m pytest --cov --cov-config=pyproject.toml --cov-report=xml tests
@uv run python -Xutf8 -m pytest --cov --cov-config=pyproject.toml --cov-report=xml tests_isolated

.PHONY: docs-test
docs-test: ## Test if documentation can be built without warnings or errors
Expand Down
85 changes: 45 additions & 40 deletions cmd2/argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,34 @@
import argparse
import inspect
import numbers
import sys
from collections import (
deque,
)
from collections.abc import Sequence
from typing import (
IO,
TYPE_CHECKING,
cast,
)

from .ansi import (
style_aware_wcswidth,
widest_line,
)
from .constants import (
INFINITY,
)
from .rich_utils import (
Cmd2Console,
Cmd2Style,
)

if TYPE_CHECKING: # pragma: no cover
from .cmd2 import (
Cmd,
)


from rich.box import SIMPLE_HEAD
from rich.table import Column, Table

from .argparse_custom import (
ChoicesCallable,
ChoicesProviderFuncWithTokens,
Expand All @@ -40,14 +46,9 @@
from .exceptions import (
CompletionError,
)
from .table_creator import (
Column,
HorizontalAlignment,
SimpleTable,
)

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

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

# Check if there are too many CompletionItems to display as a table
if len(completions) <= self._cmd2_app.max_completion_items:
four_spaces = 4 * ' '

# If a metavar was defined, use that instead of the dest field
destination = arg_state.action.metavar if arg_state.action.metavar else arg_state.action.dest

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

desc_header = arg_state.action.get_descriptive_header() # type: ignore[attr-defined]
if desc_header is None:
desc_header = DEFAULT_DESCRIPTIVE_HEADER

# Replace tabs with 4 spaces so we can calculate width
desc_header = desc_header.replace('\t', four_spaces)
desc_headers = cast(Sequence[str | Column] | None, arg_state.action.get_descriptive_headers()) # type: ignore[attr-defined]
if desc_headers is None:
desc_headers = DEFAULT_DESCRIPTIVE_HEADERS

# Calculate needed widths for the token and description columns of the table
token_width = style_aware_wcswidth(destination)
desc_width = widest_line(desc_header)

for item in completion_items:
token_width = max(style_aware_wcswidth(item), token_width)

# Replace tabs with 4 spaces so we can calculate width
item.description = item.description.replace('\t', four_spaces)
desc_width = max(widest_line(item.description), desc_width)

cols = []
dest_alignment = HorizontalAlignment.RIGHT if all_nums else HorizontalAlignment.LEFT
cols.append(
# Build all headers for the hint table
headers: list[Column] = []
headers.append(
Column(
destination.upper(),
width=token_width,
header_horiz_align=dest_alignment,
data_horiz_align=dest_alignment,
justify="right" if all_nums else "left",
no_wrap=True,
)
)
cols.append(Column(desc_header, width=desc_width))
for desc_header in desc_headers:
header = (
desc_header
if isinstance(desc_header, Column)
else Column(
desc_header,
overflow="fold",
)
)
headers.append(header)

# Build the hint table
hint_table = Table(
*headers,
box=SIMPLE_HEAD,
show_edge=False,
border_style=Cmd2Style.RULE_LINE,
)
for item in completion_items:
hint_table.add_row(item, *item.descriptive_data)

hint_table = SimpleTable(cols, divider_char=self._cmd2_app.ruler)
table_data = [[item, item.description] for item in completion_items]
self._cmd2_app.formatted_completions = hint_table.generate_table(table_data, row_spacing=0)
# Generate the hint table string
console = Cmd2Console(sys.stdout)
with console.capture() as capture:
console.print(hint_table)
self._cmd2_app.formatted_completions = capture.get()

# Return sorted list of completions
return cast(list[str], completions)
Expand Down
Loading
Loading