From 342590c6c5f2368142933a76b02abc3dc28b01e4 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 22 Aug 2025 14:53:29 -0400 Subject: [PATCH 1/6] Refactor: Isolate column rendering from printing. This change moves the core logic for rendering columns from the columnize() method to a new helper method, render_columns(). --- cmd2/cmd2.py | 39 +++++++++++++++++++++++++++++---------- cmd2/string_utils.py | 7 +++---- tests/test_cmd2.py | 33 +++++++++++++++++++++++---------- 3 files changed, 55 insertions(+), 24 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 7ee983381..d91619827 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -4243,22 +4243,26 @@ def _print_documented_command_topics(self, header: str, cmds: list[str], verbose category_grid.add_row(topics_table) self.poutput(category_grid, "") - def columnize(self, str_list: list[str] | None, display_width: int = 80) -> None: - """Display a list of single-line strings as a compact set of columns. + def render_columns(self, str_list: list[str] | None, display_width: int = 80) -> str: + """Render a list of single-line strings as a compact set of columns. - Override of cmd's columnize() to handle strings with ANSI style sequences and wide characters. + This method correctly handles strings containing ANSI escape codes and + full-width characters (like those used in CJK languages). Each column is + only as wide as necessary and columns are separated by two spaces. - Each column is only as wide as necessary. - Columns are separated by two spaces (one was not legible enough). + :param str_list: list of single-line strings to display + :param display_width: max number of display columns to fit into + :return: a string containing the columnized output """ if not str_list: - self.poutput("") - return + return "" size = len(str_list) if size == 1: - self.poutput(str_list[0]) - return + return str_list[0] + + rows: list[str] = [] + # Try every row count from 1 upwards for nrows in range(1, len(str_list)): ncols = (size + nrows - 1) // nrows @@ -4294,7 +4298,22 @@ def columnize(self, str_list: list[str] | None, display_width: int = 80) -> None del texts[-1] for col in range(len(texts)): texts[col] = su.align_left(texts[col], width=colwidths[col]) - self.poutput(" ".join(texts)) + rows.append(" ".join(texts)) + + return "\n".join(rows) + + def columnize(self, str_list: list[str] | None, display_width: int = 80) -> None: + """Display a list of single-line strings as a compact set of columns. + + Override of cmd's columnize() that uses the render_columns() method. + The method correctly handles strings with ANSI style sequences and + full-width characters (like those used in CJK languages). + + :param str_list: list of single-line strings to display + :param display_width: max number of display columns to fit into + """ + columnized_strs = self.render_columns(str_list, display_width) + self.poutput(columnized_strs) @staticmethod def _build_shortcuts_parser() -> Cmd2ArgumentParser: diff --git a/cmd2/string_utils.py b/cmd2/string_utils.py index 663f8633c..a77eb5f6b 100644 --- a/cmd2/string_utils.py +++ b/cmd2/string_utils.py @@ -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 @@ -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 diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 341f132cd..7ecc973a0 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1355,22 +1355,35 @@ def test_help_verbose_with_fake_command(capsys) -> None: assert cmds[1] not in out -def test_columnize_empty_list(capsys) -> None: - help_app = HelpApp() +def test_render_columns_no_strs(help_app: HelpApp) -> None: no_strs = [] - help_app.columnize(no_strs) - out, err = capsys.readouterr() - assert "" in out + result = help_app.render_columns(no_strs) + assert result == "" -def test_columnize_too_wide(capsys) -> None: - help_app = HelpApp() +def test_render_columns_one_str(help_app: HelpApp) -> None: + one_str = ["one_string"] + result = help_app.render_columns(one_str) + assert result == "one_string" + + +def test_render_columns_too_wide(help_app: HelpApp) -> None: commands = ["kind_of_long_string", "a_slightly_longer_string"] - help_app.columnize(commands, display_width=10) + result = help_app.render_columns(commands, display_width=10) + + expected = "kind_of_long_string \na_slightly_longer_string" + assert result == expected + + +def test_columnize(capsys: pytest.CaptureFixture[str]) -> None: + help_app = HelpApp() + items = ["one", "two"] + help_app.columnize(items) out, err = capsys.readouterr() - expected = "kind_of_long_string \na_slightly_longer_string\n" - assert expected == out + # poutput() adds a newline at the end. + expected = "one two\n" + assert out == expected class HelpCategoriesApp(cmd2.Cmd): From 3f80ffea2f67842be9e766b724a572454699c98d Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 22 Aug 2025 20:16:11 -0400 Subject: [PATCH 2/6] Reformatted cmd2 tables and help output. Added Cmd2Style.TABLE_BORDER style. --- cmd2/argparse_completer.py | 24 ++++++--------- cmd2/cmd2.py | 52 ++++++++++++++++++++------------ cmd2/styles.py | 2 ++ tests/test_argparse_completer.py | 20 ++++++------ tests/test_cmd2.py | 15 +++++---- 5 files changed, 60 insertions(+), 53 deletions(-) diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index e859c94a6..f23a371f6 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -16,18 +16,17 @@ 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, @@ -35,12 +34,8 @@ 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 @@ -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) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index d91619827..b430cc799 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -63,8 +63,9 @@ cast, ) -from rich.box import SIMPLE_HEAD +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 ( @@ -2125,7 +2126,7 @@ def _display_matches_gnu_readline( if self.formatted_completions: if not hint_printed: sys.stdout.write('\n') - sys.stdout.write('\n' + self.formatted_completions + '\n') + sys.stdout.write(self.formatted_completions) # Otherwise use readline's formatter else: @@ -2182,7 +2183,7 @@ def _display_matches_pyreadline(self, matches: list[str]) -> None: # pragma: no if self.formatted_completions: if not hint_printed: sys.stdout.write('\n') - sys.stdout.write('\n' + self.formatted_completions + '\n') + sys.stdout.write(self.formatted_completions) # Redraw the prompt and input lines rl_force_redisplay() @@ -4121,8 +4122,13 @@ def do_help(self, args: argparse.Namespace) -> None: cmds_cats, cmds_doc, cmds_undoc, help_topics = self._build_command_info() if self.doc_leader: + # Indent doc_leader to align with the help tables. self.poutput() - self.poutput(self.doc_leader, style=Cmd2Style.HELP_LEADER, soft_wrap=False) + self.poutput( + Padding.indent(self.doc_leader, 1), + style=Cmd2Style.HELP_LEADER, + soft_wrap=False, + ) self.poutput() if not cmds_cats: @@ -4167,6 +4173,9 @@ def print_topics(self, header: str, cmds: list[str] | None, cmdlen: int, maxcol: Override of cmd's print_topics() to use Rich. + The output for both the header and the commands is indented by one space to align + with the tables printed by the `help -v` command. + :param header: string to print above commands being printed :param cmds: list of topics to print :param cmdlen: unused, even by cmd's version @@ -4177,9 +4186,13 @@ def print_topics(self, header: str, cmds: list[str] | None, cmdlen: int, maxcol: header_grid = Table.grid() header_grid.add_row(header, style=Cmd2Style.HELP_HEADER) - header_grid.add_row(Rule(characters=self.ruler)) - self.poutput(header_grid) - self.columnize(cmds, maxcol - 1) + header_grid.add_row(Rule(characters=self.ruler, style=Cmd2Style.TABLE_BORDER)) + self.poutput(Padding.indent(header_grid, 1)) + + # 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() def _print_documented_command_topics(self, header: str, cmds: list[str], verbose: bool) -> None: @@ -4193,15 +4206,17 @@ def _print_documented_command_topics(self, header: str, cmds: list[str], verbose self.print_topics(header, cmds, 15, 80) return - category_grid = Table.grid() - category_grid.add_row(header, style=Cmd2Style.HELP_HEADER) - category_grid.add_row(Rule(characters=self.ruler)) + # Indent header to align with the help tables. + self.poutput( + Padding.indent(header, 1), + style=Cmd2Style.HELP_HEADER, + soft_wrap=False, + ) topics_table = Table( Column("Name", no_wrap=True), Column("Description", overflow="fold"), - box=SIMPLE_HEAD, - border_style=Cmd2Style.RULE_LINE, - show_edge=False, + box=rich.box.HORIZONTALS, + border_style=Cmd2Style.TABLE_BORDER, ) # Try to get the documentation string for each command @@ -4240,8 +4255,8 @@ def _print_documented_command_topics(self, header: str, cmds: list[str], verbose # Add this command to the table topics_table.add_row(command, cmd_desc) - category_grid.add_row(topics_table) - self.poutput(category_grid, "") + self.poutput(topics_table) + self.poutput() def render_columns(self, str_list: list[str] | None, display_width: int = 80) -> str: """Render a list of single-line strings as a compact set of columns. @@ -4519,9 +4534,8 @@ def do_set(self, args: argparse.Namespace) -> None: Column("Name", no_wrap=True), Column("Value", overflow="fold"), Column("Description", overflow="fold"), - box=SIMPLE_HEAD, - border_style=Cmd2Style.RULE_LINE, - show_edge=False, + box=rich.box.SIMPLE_HEAD, + border_style=Cmd2Style.TABLE_BORDER, ) # Build the table and populate self.last_result @@ -4532,9 +4546,7 @@ def do_set(self, args: argparse.Namespace) -> None: settable_table.add_row(param, str(settable.get_value()), settable.description) self.last_result[param] = settable.get_value() - self.poutput() self.poutput(settable_table) - self.poutput() @classmethod def _build_shell_parser(cls) -> Cmd2ArgumentParser: diff --git a/cmd2/styles.py b/cmd2/styles.py index 57b786069..37171a8c2 100644 --- a/cmd2/styles.py +++ b/cmd2/styles.py @@ -36,6 +36,7 @@ class Cmd2Style(StrEnum): 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()) @@ -47,5 +48,6 @@ class Cmd2Style(StrEnum): 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), } diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py index 27c965987..7d7d735d2 100644 --- a/tests/test_argparse_completer.py +++ b/tests/test_argparse_completer.py @@ -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 @@ -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 @@ -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 = '' @@ -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 = '' @@ -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}' @@ -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}' @@ -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: @@ -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 = '' @@ -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( diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 7ecc973a0..395fa5c96 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -184,15 +184,14 @@ def test_set(base_app) -> None: assert out == expected assert base_app.last_result is True + line_found = False out, err = run_cmd(base_app, 'set quiet') - expected = normalize( - """ - Name Value Description -─────────────────────────────────────────────────── - quiet True Don't print nonessential feedback -""" - ) - assert out == expected + for line in out: + if "quiet" in line and "True" in line and "False" not in line: + line_found = True + break + + assert line_found assert len(base_app.last_result) == 1 assert base_app.last_result['quiet'] is True From 820b4954def9cda8eeb4132c4c33438504344f70 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 23 Aug 2025 02:23:03 -0400 Subject: [PATCH 3/6] Simplified docstrings for the print functions which call print_to(). --- cmd2/cmd2.py | 108 +++++++++++-------------------------------------- cmd2/styles.py | 2 - 2 files changed, 24 insertions(+), 86 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index b430cc799..5c2f67ea4 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1260,19 +1260,7 @@ def poutput( ) -> None: """Print objects to self.stdout. - :param objects: objects to print - :param sep: string to write between print data. Defaults to " ". - :param end: string to write at end of print data. Defaults to a newline. - :param style: optional style to apply to output - :param soft_wrap: Enable soft wrap mode. If True, text lines will not be automatically word-wrapped to fit the - terminal width; instead, any text that doesn't fit will run onto the following line(s), - similar to the built-in print() function. Set to False to enable automatic word-wrapping. - If None (the default for this parameter), the output will default to no word-wrapping, as - configured by the Cmd2GeneralConsole. - :param rich_print_kwargs: optional additional keyword arguments to pass to Rich's Console.print(). - :param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this - method and still call `super()` without encountering unexpected keyword argument errors. - These arguments are not passed to Rich's Console.print(). + For details on the parameters, refer to the `print_to` method documentation. """ self.print_to( self.stdout, @@ -1296,19 +1284,9 @@ def perror( ) -> None: """Print objects to sys.stderr. - :param objects: objects to print - :param sep: string to write between print data. Defaults to " ". - :param end: string to write at end of print data. Defaults to a newline. :param style: optional style to apply to output. Defaults to Cmd2Style.ERROR. - :param soft_wrap: Enable soft wrap mode. If True, text lines will not be automatically word-wrapped to fit the - terminal width; instead, any text that doesn't fit will run onto the following line(s), - similar to the built-in print() function. Set to False to enable automatic word-wrapping. - If None (the default for this parameter), the output will default to no word-wrapping, as - configured by the Cmd2GeneralConsole. - :param rich_print_kwargs: optional additional keyword arguments to pass to Rich's Console.print(). - :param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this - method and still call `super()` without encountering unexpected keyword argument errors. - These arguments are not passed to Rich's Console.print(). + + For details on the other parameters, refer to the `print_to` method documentation. """ self.print_to( sys.stderr, @@ -1331,18 +1309,7 @@ def psuccess( ) -> None: """Wrap poutput, but apply Cmd2Style.SUCCESS. - :param objects: objects to print - :param sep: string to write between print data. Defaults to " ". - :param end: string to write at end of print data. Defaults to a newline. - :param soft_wrap: Enable soft wrap mode. If True, text lines will not be automatically word-wrapped to fit the - terminal width; instead, any text that doesn't fit will run onto the following line(s), - similar to the built-in print() function. Set to False to enable automatic word-wrapping. - If None (the default for this parameter), the output will default to no word-wrapping, as - configured by the Cmd2GeneralConsole. - :param rich_print_kwargs: optional additional keyword arguments to pass to Rich's Console.print(). - :param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this - method and still call `super()` without encountering unexpected keyword argument errors. - These arguments are not passed to Rich's Console.print(). + For details on the parameters, refer to the `print_to` method documentation. """ self.poutput( *objects, @@ -1364,18 +1331,7 @@ def pwarning( ) -> None: """Wrap perror, but apply Cmd2Style.WARNING. - :param objects: objects to print - :param sep: string to write between print data. Defaults to " ". - :param end: string to write at end of print data. Defaults to a newline. - :param soft_wrap: Enable soft wrap mode. If True, text lines will not be automatically word-wrapped to fit the - terminal width; instead, any text that doesn't fit will run onto the following line(s), - similar to the built-in print() function. Set to False to enable automatic word-wrapping. - If None (the default for this parameter), the output will default to no word-wrapping, as - configured by the Cmd2GeneralConsole. - :param rich_print_kwargs: optional additional keyword arguments to pass to Rich's Console.print(). - :param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this - method and still call `super()` without encountering unexpected keyword argument errors. - These arguments are not passed to Rich's Console.print(). + For details on the parameters, refer to the `print_to` method documentation. """ self.perror( *objects, @@ -1393,20 +1349,19 @@ def pexcept( rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: - """Print exception to sys.stderr. If debug is true, print exception traceback if one exists. + """Print an exception to sys.stderr. - :param exception: the exception to print. - :param end: string to write at end of print data. Defaults to a newline. - :param rich_print_kwargs: optional additional keyword arguments to pass to Rich's Console.print(). - :param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this - method and still call `super()` without encountering unexpected keyword argument errors. - These arguments are not passed to Rich's Console.print(). + If `debug` is true, a full exception traceback is also printed, if one exists. + + :param exception: the exception to be printed. + + For details on the other parameters, refer to the `print_to` method documentation. """ final_msg = Text() if self.debug and sys.exc_info() != (None, None, None): console = Cmd2GeneralConsole(sys.stderr) - console.print_exception(word_wrap=True) + console.print_exception(word_wrap=True, max_frames=0) else: final_msg += f"EXCEPTION of type '{type(exception).__name__}' occurred with message: {exception}" @@ -1431,23 +1386,12 @@ def pfeedback( rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: - """For printing nonessential feedback. Can be silenced with `quiet`. + """Print nonessential feedback. - Inclusion in redirected output is controlled by `feedback_to_output`. + The output can be silenced with the `quiet` setting and its inclusion in redirected output + is controlled by the `feedback_to_output` setting. - :param objects: objects to print - :param sep: string to write between print data. Defaults to " ". - :param end: string to write at end of print data. Defaults to a newline. - :param style: optional style to apply to output - :param soft_wrap: Enable soft wrap mode. If True, text lines will not be automatically word-wrapped to fit the - terminal width; instead, any text that doesn't fit will run onto the following line(s), - similar to the built-in print() function. Set to False to enable automatic word-wrapping. - If None (the default for this parameter), the output will default to no word-wrapping, as - configured by the Cmd2GeneralConsole. - :param rich_print_kwargs: optional additional keyword arguments to pass to Rich's Console.print(). - :param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this - method and still call `super()` without encountering unexpected keyword argument errors. - These arguments are not passed to Rich's Console.print(). + For details on the parameters, refer to the `print_to` method documentation. """ if not self.quiet: if self.feedback_to_output: @@ -1480,15 +1424,12 @@ def ppaged( rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: - """Print output using a pager if it would go off screen and stdout isn't currently being redirected. + """Print output using a pager. - Never uses a pager inside a script (Python or text) or when output is being redirected or piped or when - stdout or stdin are not a fully functional terminal. + A pager is used when the terminal is interactive and may exit immediately if the output + fits on the screen. A pager is not used inside a script (Python or text) or when output is + redirected or piped, and in these cases, output is sent to `poutput`. - :param objects: objects to print - :param sep: string to write between print data. Defaults to " ". - :param end: string to write at end of print data. Defaults to a newline. - :param style: optional style to apply to output :param chop: True -> causes lines longer than the screen width to be chopped (truncated) rather than wrapped - truncated text is still accessible by scrolling with the right & left arrow keys - chopping is ideal for displaying wide tabular data as is done in utilities like pgcli @@ -1500,13 +1441,12 @@ def ppaged( similar to the built-in print() function. Set to False to enable automatic word-wrapping. If None (the default for this parameter), the output will default to no word-wrapping, as configured by the Cmd2GeneralConsole. + Note: If chop is True and a pager is used, soft_wrap is automatically set to True. - :param rich_print_kwargs: optional additional keyword arguments to pass to Rich's Console.print(). - :param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this - method and still call `super()` without encountering unexpected keyword argument errors. - These arguments are not passed to Rich's Console.print(). + + For details on the other parameters, refer to the `print_to` method documentation. """ - # Detect if we are running within a fully functional terminal. + # Detect if we are running within an interactive terminal. # Don't try to use the pager when being run by a continuous integration system like Jenkins + pexpect. functional_terminal = ( self.stdin.isatty() diff --git a/cmd2/styles.py b/cmd2/styles.py index 37171a8c2..b2d8f14e2 100644 --- a/cmd2/styles.py +++ b/cmd2/styles.py @@ -34,7 +34,6 @@ class Cmd2Style(StrEnum): EXAMPLE = "cmd2.example" # Command line examples in help text 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()) @@ -46,7 +45,6 @@ class Cmd2Style(StrEnum): Cmd2Style.EXAMPLE: Style(color=Color.CYAN, 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), From 16c466588adf77f7936acf45ee5d7bf41e56996a Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 23 Aug 2025 02:41:09 -0400 Subject: [PATCH 4/6] Removed regex_set.txt test which was difficult to maintain and tested features which are already covered in other tests. --- tests/test_transcript.py | 1 - tests/transcripts/regex_set.txt | 31 ------------------------------- 2 files changed, 32 deletions(-) delete mode 100644 tests/transcripts/regex_set.txt diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 0739c0c7b..8a654ecd5 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -115,7 +115,6 @@ def test_commands_at_invocation() -> None: ('multiline_regex.txt', False), ('no_output.txt', False), ('no_output_last.txt', False), - ('regex_set.txt', False), ('singleslash.txt', False), ('slashes_escaped.txt', False), ('slashslash.txt', False), diff --git a/tests/transcripts/regex_set.txt b/tests/transcripts/regex_set.txt deleted file mode 100644 index adaa68e2f..000000000 --- a/tests/transcripts/regex_set.txt +++ /dev/null @@ -1,31 +0,0 @@ -# Run this transcript with "python example.py -t transcript_regex.txt" -# The regex for allow_style will match any setting for the previous value. -# The regex for editor will match whatever program you use. -# Regexes on prompts just make the trailing space obvious - -(Cmd) set allow_style Terminal -allow_style - was: '/.*/' -now: 'Terminal' -(Cmd) set editor vim -editor - was: '/.*/' -now: 'vim' -(Cmd) set - - Name Value Description/ */ -───────────────────────────────────────────────────────────────────────────────/─*/ - allow_style Terminal Allow ANSI text style sequences in output/ */ - (valid values: Always, Never, Terminal)/ */ - always_show_hint False Display tab completion hint even when/ */ - completion suggestions print/ */ - debug False Show full traceback on exception/ */ - echo False Echo command issued into output/ */ - editor vim Program used by 'edit'/ */ - feedback_to_output False Include nonessentials in '|' and '>'/ */ - results/ */ - max_completion_items 50 Maximum number of CompletionItems to/ */ - display during tab completion/ */ - maxrepeats 3 Max number of `--repeat`s allowed/ */ - quiet False Don't print nonessential feedback/ */ - scripts_add_to_history True Scripts and pyscripts add commands to/ */ - history/ */ - timing False Report execution times/ */ From bcb9012006be85376362d0d21fb981f0b796595e Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 23 Aug 2025 18:23:04 -0400 Subject: [PATCH 5/6] Word wrapping long exceptions in pexcept(). --- cmd2/cmd2.py | 99 +++++++++++++++++++++++++--------------------- cmd2/rich_utils.py | 35 +++++++++++----- cmd2/styles.py | 6 ++- tests/test_cmd2.py | 29 ++++---------- 4 files changed, 93 insertions(+), 76 deletions(-) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 5c2f67ea4..529b13524 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -131,6 +131,7 @@ shlex_split, ) from .rich_utils import ( + Cmd2ExceptionConsole, Cmd2GeneralConsole, RichPrintKwargs, ) @@ -1207,7 +1208,7 @@ def print_to( sep: str = " ", end: str = "\n", style: StyleType | None = None, - soft_wrap: bool | None = None, + soft_wrap: bool = True, rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: @@ -1218,11 +1219,8 @@ def print_to( :param sep: string to write between print data. Defaults to " ". :param end: string to write at end of print data. Defaults to a newline. :param style: optional style to apply to output - :param soft_wrap: Enable soft wrap mode. If True, text lines will not be automatically word-wrapped to fit the - terminal width; instead, any text that doesn't fit will run onto the following line(s), - similar to the built-in print() function. Set to False to enable automatic word-wrapping. - If None (the default for this parameter), the output will default to no word-wrapping, as - configured by the Cmd2GeneralConsole. + :param soft_wrap: Enable soft wrap mode. If True, lines of text will not be word-wrapped or cropped to + fit the terminal width. Defaults to True. :param rich_print_kwargs: optional additional keyword arguments to pass to Rich's Console.print(). :param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this method and still call `super()` without encountering unexpected keyword argument errors. @@ -1254,7 +1252,7 @@ def poutput( sep: str = " ", end: str = "\n", style: StyleType | None = None, - soft_wrap: bool | None = None, + soft_wrap: bool = True, rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: @@ -1278,7 +1276,7 @@ def perror( sep: str = " ", end: str = "\n", style: StyleType | None = Cmd2Style.ERROR, - soft_wrap: bool | None = None, + soft_wrap: bool = True, rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: @@ -1303,7 +1301,7 @@ def psuccess( *objects: Any, sep: str = " ", end: str = "\n", - soft_wrap: bool | None = None, + soft_wrap: bool = True, rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: @@ -1325,7 +1323,7 @@ def pwarning( *objects: Any, sep: str = " ", end: str = "\n", - soft_wrap: bool | None = None, + soft_wrap: bool = True, rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: @@ -1345,36 +1343,51 @@ def pwarning( def pexcept( self, exception: BaseException, - end: str = "\n", - rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: """Print an exception to sys.stderr. - If `debug` is true, a full exception traceback is also printed, if one exists. + If `debug` is true, a full traceback is also printed, if one exists. :param exception: the exception to be printed. - - For details on the other parameters, refer to the `print_to` method documentation. + :param kwargs: Arbitrary keyword arguments. This allows subclasses to extend the signature of this + method and still call `super()` without encountering unexpected keyword argument errors. """ - final_msg = Text() + console = Cmd2ExceptionConsole(sys.stderr) + # Only print a traceback if we're in debug mode and one exists. if self.debug and sys.exc_info() != (None, None, None): - console = Cmd2GeneralConsole(sys.stderr) - console.print_exception(word_wrap=True, max_frames=0) - else: - final_msg += f"EXCEPTION of type '{type(exception).__name__}' occurred with message: {exception}" + console.print_exception( + width=console.width, + show_locals=True, + max_frames=0, # 0 means full traceback. + word_wrap=True, # Wrap long lines of code instead of truncate + ) + console.print() + return - if not self.debug and 'debug' in self.settables: - warning = "\nTo enable full traceback, run the following command: 'set debug true'" - final_msg.append(warning, style=Cmd2Style.WARNING) + # Otherwise highlight and print the exception. + from rich.highlighter import ReprHighlighter - if final_msg: - self.perror( - final_msg, - end=end, - rich_print_kwargs=rich_print_kwargs, + highlighter = ReprHighlighter() + + final_msg = Text.assemble( + ("EXCEPTION of type ", Cmd2Style.ERROR), + (f"{type(exception).__name__}", Cmd2Style.EXCEPTION_TYPE), + (" occurred with message: ", Cmd2Style.ERROR), + highlighter(str(exception)), + ) + + if not self.debug and 'debug' in self.settables: + help_msg = Text.assemble( + "\n\n", + ("To enable full traceback, run the following command: ", Cmd2Style.WARNING), + ("set debug true", Cmd2Style.COMMAND_LINE), ) + final_msg.append(help_msg) + + console.print(final_msg) + console.print() def pfeedback( self, @@ -1382,7 +1395,7 @@ def pfeedback( sep: str = " ", end: str = "\n", style: StyleType | None = None, - soft_wrap: bool | None = None, + soft_wrap: bool = True, rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: @@ -1420,7 +1433,7 @@ def ppaged( end: str = "\n", style: StyleType | None = None, chop: bool = False, - soft_wrap: bool | None = None, + soft_wrap: bool = True, rich_print_kwargs: RichPrintKwargs | None = None, **kwargs: Any, # noqa: ARG002 ) -> None: @@ -1436,13 +1449,11 @@ def ppaged( False -> causes lines longer than the screen width to wrap to the next line - wrapping is ideal when you want to keep users from having to use horizontal scrolling WARNING: On Windows, the text always wraps regardless of what the chop argument is set to - :param soft_wrap: Enable soft wrap mode. If True, text lines will not be automatically word-wrapped to fit the - terminal width; instead, any text that doesn't fit will run onto the following line(s), - similar to the built-in print() function. Set to False to enable automatic word-wrapping. - If None (the default for this parameter), the output will default to no word-wrapping, as - configured by the Cmd2GeneralConsole. + :param soft_wrap: Enable soft wrap mode. If True, lines of text will not be word-wrapped or cropped to + fit the terminal width. Defaults to True. - Note: If chop is True and a pager is used, soft_wrap is automatically set to True. + Note: If chop is True and a pager is used, soft_wrap is automatically set to True to + prevent wrapping and allow for horizontal scrolling. For details on the other parameters, refer to the `print_to` method documentation. """ @@ -3527,7 +3538,7 @@ def _build_alias_create_parser(cls) -> Cmd2ArgumentParser: alias_create_notes = Group( "If you want to use redirection, pipes, or terminators in the value of the alias, then quote them.", "\n", - Text(" alias create save_results print_results \">\" out.txt\n", style=Cmd2Style.EXAMPLE), + Text(" alias create save_results print_results \">\" out.txt\n", style=Cmd2Style.COMMAND_LINE), ( "Since aliases are resolved during parsing, tab completion will function as it would " "for the actual command the alias resolves to." @@ -3740,14 +3751,14 @@ def _build_macro_create_parser(cls) -> Cmd2ArgumentParser: "\n", "The following creates a macro called my_macro that expects two arguments:", "\n", - Text(" macro create my_macro make_dinner --meat {1} --veggie {2}", style=Cmd2Style.EXAMPLE), + Text(" macro create my_macro make_dinner --meat {1} --veggie {2}", style=Cmd2Style.COMMAND_LINE), "\n", "When the macro is called, the provided arguments are resolved and the assembled command is run. For example:", "\n", Text.assemble( - (" my_macro beef broccoli", Cmd2Style.EXAMPLE), + (" my_macro beef broccoli", Cmd2Style.COMMAND_LINE), (" ───> ", Style(bold=True)), - ("make_dinner --meat beef --veggie broccoli", Cmd2Style.EXAMPLE), + ("make_dinner --meat beef --veggie broccoli", Cmd2Style.COMMAND_LINE), ), ) macro_create_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(description=macro_create_description) @@ -3763,15 +3774,15 @@ def _build_macro_create_parser(cls) -> Cmd2ArgumentParser: "first argument will populate both {1} instances." ), "\n", - Text(" macro create ft file_taxes -p {1} -q {2} -r {1}", style=Cmd2Style.EXAMPLE), + Text(" macro create ft file_taxes -p {1} -q {2} -r {1}", style=Cmd2Style.COMMAND_LINE), "\n", "To quote an argument in the resolved command, quote it during creation.", "\n", - Text(" macro create backup !cp \"{1}\" \"{1}.orig\"", style=Cmd2Style.EXAMPLE), + Text(" macro create backup !cp \"{1}\" \"{1}.orig\"", style=Cmd2Style.COMMAND_LINE), "\n", "If you want to use redirection, pipes, or terminators in the value of the macro, then quote them.", "\n", - Text(" macro create show_results print_results -type {1} \"|\" less", style=Cmd2Style.EXAMPLE), + Text(" macro create show_results print_results -type {1} \"|\" less", style=Cmd2Style.COMMAND_LINE), "\n", ( "Since macros don't resolve until after you press Enter, their arguments tab complete as paths. " @@ -5316,7 +5327,7 @@ def _build_edit_parser(cls) -> Cmd2ArgumentParser: "Note", Text.assemble( "To set a new editor, run: ", - ("set editor ", Cmd2Style.EXAMPLE), + ("set editor ", Cmd2Style.COMMAND_LINE), ), ) diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index 07fcef8a0..a47ed8ddd 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -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. """ @@ -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() @@ -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. @@ -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. @@ -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 diff --git a/cmd2/styles.py b/cmd2/styles.py index b2d8f14e2..99cabc2cc 100644 --- a/cmd2/styles.py +++ b/cmd2/styles.py @@ -30,8 +30,9 @@ 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 SUCCESS = "cmd2.success" # Success text (used by psuccess()) @@ -41,8 +42,9 @@ class Cmd2Style(StrEnum): # 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.SUCCESS: Style(color=Color.GREEN), diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 395fa5c96..f03d5224a 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -906,29 +906,14 @@ def test_base_timing(base_app) -> None: assert err[0].startswith('Elapsed: 0:00:00.0') -def _expected_no_editor_error(): - expected_exception = 'OSError' - # If PyPy, expect a different exception than with Python 3 - if hasattr(sys, "pypy_translation_info"): - expected_exception = 'EnvironmentError' - - return normalize( - f""" -EXCEPTION of type '{expected_exception}' occurred with message: Please use 'set editor' to specify your text editing program of choice. -To enable full traceback, run the following command: 'set debug true' -""" - ) - - def test_base_debug(base_app) -> None: # Purposely set the editor to None base_app.editor = None # Make sure we get an exception, but cmd2 handles it out, err = run_cmd(base_app, 'edit') - - expected = _expected_no_editor_error() - assert err == expected + assert "EXCEPTION of type" in err[0] + assert "Please use 'set editor'" in err[0] # Set debug true out, err = run_cmd(base_app, 'set debug True') @@ -2589,7 +2574,9 @@ def test_pexcept_style(base_app, capsys) -> None: base_app.pexcept(msg) out, err = capsys.readouterr() - assert err.startswith("\x1b[91mEXCEPTION of type 'Exception' occurred with message: testing") + expected = su.stylize("EXCEPTION of type ", style=Cmd2Style.ERROR) + expected += su.stylize("Exception", style=Cmd2Style.EXCEPTION_TYPE) + assert err.startswith(expected) @with_ansi_style(ru.AllowStyle.NEVER) @@ -2598,17 +2585,17 @@ def test_pexcept_no_style(base_app, capsys) -> None: base_app.pexcept(msg) out, err = capsys.readouterr() - assert err.startswith("EXCEPTION of type 'Exception' occurred with message: testing...") + assert err.startswith("EXCEPTION of type Exception occurred with message: testing...") -@with_ansi_style(ru.AllowStyle.ALWAYS) +@with_ansi_style(ru.AllowStyle.NEVER) def test_pexcept_not_exception(base_app, capsys) -> None: # Pass in a msg that is not an Exception object msg = False base_app.pexcept(msg) out, err = capsys.readouterr() - assert err.startswith("\x1b[91mEXCEPTION of type 'bool' occurred with message: False") + assert err.startswith("EXCEPTION of type bool occurred with message: False") @pytest.mark.parametrize('chop', [True, False]) From 4a838463d45bd85d8e265cefec3aa2740f1a067a Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Sat, 23 Aug 2025 18:43:45 -0400 Subject: [PATCH 6/6] Changed mypy ignore to work on both Windows and Linux. --- cmd2/rl_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py index 29d8b4e5a..c7f37a0d1 100644 --- a/cmd2/rl_utils.py +++ b/cmd2/rl_utils.py @@ -279,7 +279,7 @@ 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, ) @@ -287,7 +287,7 @@ def rl_in_search_mode() -> bool: # pragma: no cover 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,