diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 516388cb5..b0461659d 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -284,7 +284,7 @@ def get_items(self) -> list[CompletionItems]: RenderableType, ) from rich.protocol import is_renderable -from rich.table import Column, Table +from rich.table import Column from rich.text import Text from rich_argparse import ( ArgumentDefaultsRichHelpFormatter, @@ -295,6 +295,7 @@ def get_items(self) -> list[CompletionItems]: ) from . import constants +from . import rich_utils as ru from .rich_utils import Cmd2RichArgparseConsole from .styles import Cmd2Style @@ -1377,17 +1378,10 @@ def __rich__(self) -> Group: style=formatter.styles["argparse.groups"], ) - # Left pad the text like an argparse argument group does - left_padding = formatter._indent_increment - text_table = Table( - Column(overflow="fold"), - box=None, - show_header=False, - padding=(0, 0, 0, left_padding), - ) - text_table.add_row(self.text) + # Indent text like an argparse argument group does + indented_text = ru.indent(self.text, formatter._indent_increment) - return Group(styled_title, text_table) + return Group(styled_title, indented_text) class Cmd2ArgumentParser(argparse.ArgumentParser): diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 3882d4d69..fe8740a45 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -65,7 +65,6 @@ import rich.box from rich.console import Group -from rich.padding import Padding from rich.rule import Rule from rich.style import Style, StyleType from rich.table import ( @@ -4076,9 +4075,8 @@ def do_help(self, args: argparse.Namespace) -> None: # Indent doc_leader to align with the help tables. self.poutput() self.poutput( - Padding.indent(self.doc_leader, 1), + ru.indent(self.doc_leader, 1), style=Cmd2Style.HELP_LEADER, - soft_wrap=False, ) self.poutput() @@ -4135,17 +4133,20 @@ def print_topics(self, header: str, cmds: list[str] | None, cmdlen: int, maxcol: if not cmds: return - # Add a row that looks like a table header. - header_grid = Table.grid() - header_grid.add_row(header, style=Cmd2Style.HELP_HEADER) - header_grid.add_row(Rule(characters=self.ruler, style=Cmd2Style.TABLE_BORDER)) - self.poutput(Padding.indent(header_grid, 1)) + # Print a row that looks like a table header. + if header: + header_grid = Table.grid() + header_grid.add_row(header, style=Cmd2Style.HELP_HEADER) + header_grid.add_row(Rule(characters=self.ruler, style=Cmd2Style.TABLE_BORDER)) + self.poutput(ru.indent(header_grid, 1)) + + # Subtract 2 from the max column width to account for the + # one-space indentation and a one-space right margin. + maxcol = min(maxcol, ru.console_width()) - 2 # Print the topics in columns. - # Subtract 1 from maxcol to account for indentation. - maxcol = min(maxcol, ru.console_width()) - 1 columnized_cmds = self.render_columns(cmds, maxcol) - self.poutput(Padding.indent(columnized_cmds, 1), soft_wrap=False) + self.poutput(ru.indent(columnized_cmds, 1)) self.poutput() def _print_documented_command_topics(self, header: str, cmds: list[str], verbose: bool) -> None: @@ -4160,11 +4161,7 @@ def _print_documented_command_topics(self, header: str, cmds: list[str], verbose return # Indent header to align with the help tables. - self.poutput( - Padding.indent(header, 1), - style=Cmd2Style.HELP_HEADER, - soft_wrap=False, - ) + self.poutput(ru.indent(header, 1), style=Cmd2Style.HELP_HEADER) topics_table = Table( Column("Name", no_wrap=True), Column("Description", overflow="fold"), @@ -5529,7 +5526,7 @@ class TestMyAppCase(Cmd2TestCase): num_transcripts = len(transcripts_expanded) plural = '' if len(transcripts_expanded) == 1 else 's' self.poutput( - Rule("cmd2 transcript test", style=Style.null()), + Rule("cmd2 transcript test", characters=self.ruler, style=Style.null()), style=Style(bold=True), ) self.poutput(f'platform {sys.platform} -- Python {verinfo}, cmd2-{cmd2.__version__}, readline-{rl_type}') @@ -5548,7 +5545,7 @@ class TestMyAppCase(Cmd2TestCase): if test_results.wasSuccessful(): self.perror(stream.read(), end="", style=None) finish_msg = f'{num_transcripts} transcript{plural} passed in {execution_time:.3f} seconds' - self.psuccess(Rule(finish_msg, style=Style.null())) + self.psuccess(Rule(finish_msg, characters=self.ruler, style=Style.null())) else: # Strip off the initial traceback which isn't particularly useful for end users error_str = stream.read() diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index 2c08c6da1..c2da6915c 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -17,7 +17,12 @@ RenderableType, RichCast, ) +from rich.padding import Padding from rich.style import StyleType +from rich.table import ( + Column, + Table, +) from rich.text import Text from rich.theme import Theme from rich_argparse import RichHelpFormatter @@ -288,6 +293,30 @@ def string_to_rich_text(text: str) -> Text: return result +def indent(renderable: RenderableType, level: int) -> Padding: + """Indent a Rich renderable. + + When soft-wrapping is enabled, a Rich console is unable to properly print a + Padding object of indented text, as it truncates long strings instead of wrapping + them. This function provides a workaround for this issue, ensuring that indented + text is printed correctly regardless of the soft-wrap setting. + + For non-text objects, this function merely serves as a convenience + wrapper around Padding.indent(). + + :param renderable: a Rich renderable to indent. + :param level: number of characters to indent. + :return: a Padding object containing the indented content. + """ + if isinstance(renderable, (str, Text)): + # Wrap text in a grid to handle the wrapping. + text_grid = Table.grid(Column(overflow="fold")) + text_grid.add_row(renderable) + renderable = text_grid + + return Padding.indent(renderable, level) + + def prepare_objects_for_rich_print(*objects: Any) -> tuple[RenderableType, ...]: """Prepare a tuple of objects for printing by Rich's Console.print(). diff --git a/tests/test_rich_utils.py b/tests/test_rich_utils.py index 61da54238..f471d7d58 100644 --- a/tests/test_rich_utils.py +++ b/tests/test_rich_utils.py @@ -1,7 +1,9 @@ """Unit testing for cmd2/rich_utils.py module""" import pytest +import rich.box from rich.style import Style +from rich.table import Table from rich.text import Text from cmd2 import ( @@ -59,6 +61,45 @@ def test_string_to_rich_text() -> None: assert ru.string_to_rich_text(input_string).plain == input_string +def test_indented_text() -> None: + console = ru.Cmd2GeneralConsole() + + # With an indention of 10, text will be evenly split across two lines. + console.width = 20 + text = "A" * 20 + level = 10 + indented_text = ru.indent(text, level) + + with console.capture() as capture: + console.print(indented_text) + result = capture.get().splitlines() + + padding = " " * level + expected_line = padding + ("A" * 10) + assert result[0] == expected_line + assert result[1] == expected_line + + +def test_indented_table() -> None: + console = ru.Cmd2GeneralConsole() + + level = 2 + table = Table("Column", box=rich.box.ASCII) + table.add_row("Some Data") + indented_table = ru.indent(table, level) + + with console.capture() as capture: + console.print(indented_table) + result = capture.get().splitlines() + + padding = " " * level + assert result[0].startswith(padding + "+-----------+") + assert result[1].startswith(padding + "| Column |") + assert result[2].startswith(padding + "|-----------|") + assert result[3].startswith(padding + "| Some Data |") + assert result[4].startswith(padding + "+-----------+") + + @pytest.mark.parametrize( ('rich_text', 'string'), [