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
24 changes: 9 additions & 15 deletions cmd2/argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,26 @@
cast,
)

from .constants import (
INFINITY,
)
from .constants import INFINITY
from .rich_utils import Cmd2GeneralConsole

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

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

from .argparse_custom import (
ChoicesCallable,
ChoicesProviderFuncWithTokens,
CompletionItem,
generate_range_error,
)
from .command_definition import (
CommandSet,
)
from .exceptions import (
CompletionError,
)
from .command_definition import CommandSet
from .exceptions import CompletionError
from .styles import Cmd2Style

# If no descriptive headers are supplied, then this will be used instead
Expand Down Expand Up @@ -583,8 +578,7 @@ def _format_completions(self, arg_state: _ArgumentState, completions: list[str]
hint_table = Table(
*headers,
box=SIMPLE_HEAD,
show_edge=False,
border_style=Cmd2Style.RULE_LINE,
border_style=Cmd2Style.TABLE_BORDER,
)
for item in completion_items:
hint_table.add_row(item, *item.descriptive_data)
Expand Down
286 changes: 134 additions & 152 deletions cmd2/cmd2.py

Large diffs are not rendered by default.

35 changes: 26 additions & 9 deletions cmd2/rich_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def __repr__(self) -> str:


def _create_default_theme() -> Theme:
"""Create a default theme for cmd2-based applications.
"""Create a default theme for the application.

This theme combines the default styles from cmd2, rich-argparse, and Rich.
"""
Expand Down Expand Up @@ -79,8 +79,7 @@ def set_theme(styles: Mapping[str, StyleType] | None = None) -> None:
RichHelpFormatter.styles[name] = APP_THEME.styles[name]


# The main theme for cmd2-based applications.
# You can change it with set_theme().
# The application-wide theme. You can change it with set_theme().
APP_THEME = _create_default_theme()


Expand All @@ -107,12 +106,22 @@ class RichPrintKwargs(TypedDict, total=False):


class Cmd2BaseConsole(Console):
"""A base class for Rich consoles in cmd2-based applications."""
"""Base class for all cmd2 Rich consoles.

def __init__(self, file: IO[str] | None = None, **kwargs: Any) -> None:
This class handles the core logic for managing Rich behavior based on
cmd2's global settings, such as `ALLOW_STYLE` and `APP_THEME`.
"""

def __init__(
self,
file: IO[str] | None = None,
**kwargs: Any,
) -> None:
"""Cmd2BaseConsole initializer.

:param file: optional file object where the console should write to. Defaults to sys.stdout.
:param file: optional file object where the console should write to.
Defaults to sys.stdout.
:param kwargs: keyword arguments passed to the parent Console class.
"""
# Don't allow force_terminal or force_interactive to be passed in, as their
# behavior is controlled by the ALLOW_STYLE setting.
Expand Down Expand Up @@ -160,12 +169,13 @@ def on_broken_pipe(self) -> None:


class Cmd2GeneralConsole(Cmd2BaseConsole):
"""Rich console for general-purpose printing in cmd2-based applications."""
"""Rich console for general-purpose printing."""

def __init__(self, file: IO[str] | None = None) -> None:
"""Cmd2GeneralConsole initializer.

:param file: optional file object where the console should write to. Defaults to sys.stdout.
:param file: optional file object where the console should write to.
Defaults to sys.stdout.
"""
# This console is configured for general-purpose printing. It enables soft wrap
# and disables Rich's automatic processing for markup, emoji, and highlighting.
Expand All @@ -180,13 +190,20 @@ def __init__(self, file: IO[str] | None = None) -> None:


class Cmd2RichArgparseConsole(Cmd2BaseConsole):
"""Rich console for rich-argparse output in cmd2-based applications.
"""Rich console for rich-argparse output.

This class ensures long lines in help text are not truncated by avoiding soft_wrap,
which conflicts with rich-argparse's explicit no_wrap and overflow settings.
"""


class Cmd2ExceptionConsole(Cmd2BaseConsole):
"""Rich console for printing exceptions.

Ensures that long exception messages word wrap for readability by keeping soft_wrap disabled.
"""


def console_width() -> int:
"""Return the width of the console."""
return Cmd2BaseConsole().width
Expand Down
4 changes: 2 additions & 2 deletions cmd2/rl_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,15 +279,15 @@ def rl_in_search_mode() -> bool: # pragma: no cover
readline_state = ctypes.c_int.in_dll(readline_lib, "rl_readline_state").value
return bool(in_search_mode & readline_state)
if rl_type == RlType.PYREADLINE:
from pyreadline3.modes.emacs import ( # type: ignore[import-not-found]
from pyreadline3.modes.emacs import ( # type: ignore[import]
EmacsMode,
)

# These search modes only apply to Emacs mode, which is the default.
if not isinstance(readline.rl.mode, EmacsMode):
return False

# While in search mode, the current keyevent function is set one of the following.
# While in search mode, the current keyevent function is set to one of the following.
search_funcs = (
readline.rl.mode._process_incremental_search_keyevent,
readline.rl.mode._process_non_incremental_search_keyevent,
Expand Down
7 changes: 3 additions & 4 deletions cmd2/string_utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"""Provides string utility functions.

This module offers a collection of string utility functions built on the Rich library.
These utilities are designed to correctly handle strings with complex formatting, such as
ANSI escape codes and full-width characters (like those used in CJK languages), which the
standard Python library's string methods do not properly support.
These utilities are designed to correctly handle strings with ANSI escape codes and
full-width characters (like those used in CJK languages).
"""

from rich.align import AlignMethod
Expand Down Expand Up @@ -107,7 +106,7 @@ def strip_style(val: str) -> str:
def str_width(val: str) -> int:
"""Return the display width of a string.

This is intended for single line strings.
This is intended for single-line strings.
Replace tabs with spaces before calling this.

:param val: the string being measured
Expand Down
10 changes: 6 additions & 4 deletions cmd2/styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,24 @@ class Cmd2Style(StrEnum):
added here must have a corresponding style definition there.
"""

COMMAND_LINE = "cmd2.example" # Command line examples in help text
ERROR = "cmd2.error" # Error text (used by perror())
EXAMPLE = "cmd2.example" # Command line examples in help text
EXCEPTION_TYPE = "cmd2.exception.type" # Used by pexcept to mark an exception type
HELP_HEADER = "cmd2.help.header" # Help table header text
HELP_LEADER = "cmd2.help.leader" # Text right before the help tables are listed
RULE_LINE = "rule.line" # Rich style for horizontal rules
SUCCESS = "cmd2.success" # Success text (used by psuccess())
TABLE_BORDER = "cmd2.table_border" # Applied to cmd2's table borders
WARNING = "cmd2.warning" # Warning text (used by pwarning())


# Default styles used by cmd2. Tightly coupled with the Cmd2Style enum.
DEFAULT_CMD2_STYLES: dict[str, StyleType] = {
Cmd2Style.COMMAND_LINE: Style(color=Color.CYAN, bold=True),
Cmd2Style.ERROR: Style(color=Color.BRIGHT_RED),
Cmd2Style.EXAMPLE: Style(color=Color.CYAN, bold=True),
Cmd2Style.EXCEPTION_TYPE: Style(color=Color.DARK_ORANGE, bold=True),
Cmd2Style.HELP_HEADER: Style(color=Color.BRIGHT_GREEN, bold=True),
Cmd2Style.HELP_LEADER: Style(color=Color.CYAN, bold=True),
Cmd2Style.RULE_LINE: Style(color=Color.BRIGHT_GREEN),
Cmd2Style.SUCCESS: Style(color=Color.GREEN),
Cmd2Style.TABLE_BORDER: Style(color=Color.BRIGHT_GREEN),
Cmd2Style.WARNING: Style(color=Color.BRIGHT_YELLOW),
}
20 changes: 10 additions & 10 deletions tests/test_argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,8 +720,8 @@ def test_completion_items(ac_app) -> None:
line_found = False
for line in ac_app.formatted_completions.splitlines():
# Since the CompletionItems were created from strings, the left-most column is left-aligned.
# Therefore choice_1 will begin the line (with 1 space for padding).
if line.startswith(' choice_1') and 'A description' in line:
# Therefore choice_1 will begin the line (with 2 spaces for padding).
if line.startswith(' choice_1') and 'A description' in line:
line_found = True
break

Expand All @@ -743,7 +743,7 @@ def test_completion_items(ac_app) -> None:
for line in ac_app.formatted_completions.splitlines():
# Since the CompletionItems were created from numbers, the left-most column is right-aligned.
# Therefore 1.5 will be right-aligned.
if line.startswith(" 1.5") and "One.Five" in line:
if line.startswith(" 1.5") and "One.Five" in line:
line_found = True
break

Expand Down Expand Up @@ -908,7 +908,7 @@ def test_completion_items_arg_header(ac_app) -> None:
begidx = endidx - len(text)

complete_tester(text, line, begidx, endidx, ac_app)
assert "DESC_HEADER" in normalize(ac_app.formatted_completions)[0]
assert "DESC_HEADER" in normalize(ac_app.formatted_completions)[1]

# Test when metavar is a string
text = ''
Expand All @@ -917,7 +917,7 @@ def test_completion_items_arg_header(ac_app) -> None:
begidx = endidx - len(text)

complete_tester(text, line, begidx, endidx, ac_app)
assert ac_app.STR_METAVAR in normalize(ac_app.formatted_completions)[0]
assert ac_app.STR_METAVAR in normalize(ac_app.formatted_completions)[1]

# Test when metavar is a tuple
text = ''
Expand All @@ -927,7 +927,7 @@ def test_completion_items_arg_header(ac_app) -> None:

# We are completing the first argument of this flag. The first element in the tuple should be the column header.
complete_tester(text, line, begidx, endidx, ac_app)
assert ac_app.TUPLE_METAVAR[0].upper() in normalize(ac_app.formatted_completions)[0]
assert ac_app.TUPLE_METAVAR[0].upper() in normalize(ac_app.formatted_completions)[1]

text = ''
line = f'choices --tuple_metavar token_1 {text}'
Expand All @@ -936,7 +936,7 @@ def test_completion_items_arg_header(ac_app) -> None:

# We are completing the second argument of this flag. The second element in the tuple should be the column header.
complete_tester(text, line, begidx, endidx, ac_app)
assert ac_app.TUPLE_METAVAR[1].upper() in normalize(ac_app.formatted_completions)[0]
assert ac_app.TUPLE_METAVAR[1].upper() in normalize(ac_app.formatted_completions)[1]

text = ''
line = f'choices --tuple_metavar token_1 token_2 {text}'
Expand All @@ -946,7 +946,7 @@ def test_completion_items_arg_header(ac_app) -> None:
# We are completing the third argument of this flag. It should still be the second tuple element
# in the column header since the tuple only has two strings in it.
complete_tester(text, line, begidx, endidx, ac_app)
assert ac_app.TUPLE_METAVAR[1].upper() in normalize(ac_app.formatted_completions)[0]
assert ac_app.TUPLE_METAVAR[1].upper() in normalize(ac_app.formatted_completions)[1]


def test_completion_items_descriptive_headers(ac_app) -> None:
Expand All @@ -961,7 +961,7 @@ def test_completion_items_descriptive_headers(ac_app) -> None:
begidx = endidx - len(text)

complete_tester(text, line, begidx, endidx, ac_app)
assert ac_app.CUSTOM_DESC_HEADERS[0] in normalize(ac_app.formatted_completions)[0]
assert ac_app.CUSTOM_DESC_HEADERS[0] in normalize(ac_app.formatted_completions)[1]

# This argument did not provide a descriptive header, so it should be DEFAULT_DESCRIPTIVE_HEADERS
text = ''
Expand All @@ -970,7 +970,7 @@ def test_completion_items_descriptive_headers(ac_app) -> None:
begidx = endidx - len(text)

complete_tester(text, line, begidx, endidx, ac_app)
assert DEFAULT_DESCRIPTIVE_HEADERS[0] in normalize(ac_app.formatted_completions)[0]
assert DEFAULT_DESCRIPTIVE_HEADERS[0] in normalize(ac_app.formatted_completions)[1]


@pytest.mark.parametrize(
Expand Down
Loading
Loading