diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 5fc484608..e859c94a6 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -19,7 +19,7 @@ from .constants import ( INFINITY, ) -from .rich_utils import Cmd2Console +from .rich_utils import Cmd2GeneralConsole if TYPE_CHECKING: # pragma: no cover from .cmd2 import ( @@ -590,7 +590,7 @@ def _format_completions(self, arg_state: _ArgumentState, completions: list[str] hint_table.add_row(item, *item.descriptive_data) # Generate the hint table string - console = Cmd2Console() + console = Cmd2GeneralConsole() with console.capture() as capture: console.print(hint_table, end="") self._cmd2_app.formatted_completions = capture.get() diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 73a1aa466..516388cb5 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -295,7 +295,7 @@ def get_items(self) -> list[CompletionItems]: ) from . import constants -from .rich_utils import Cmd2Console +from .rich_utils import Cmd2RichArgparseConsole from .styles import Cmd2Style if TYPE_CHECKING: # pragma: no cover @@ -1113,12 +1113,12 @@ def __init__( max_help_position: int = 24, width: int | None = None, *, - console: Cmd2Console | None = None, + console: Cmd2RichArgparseConsole | None = None, **kwargs: Any, ) -> None: """Initialize Cmd2HelpFormatter.""" if console is None: - console = Cmd2Console() + console = Cmd2RichArgparseConsole() super().__init__(prog, indent_increment, max_help_position, width, console=console, **kwargs) @@ -1481,7 +1481,7 @@ def error(self, message: str) -> NoReturn: # Add error style to message console = self._get_formatter().console with console.capture() as capture: - console.print(formatted_message, style=Cmd2Style.ERROR) + console.print(formatted_message, style=Cmd2Style.ERROR, crop=False) formatted_message = f"{capture.get()}" self.exit(2, f'{formatted_message}\n') diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 87c9ce1d2..7ee983381 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -130,7 +130,7 @@ shlex_split, ) from .rich_utils import ( - Cmd2Console, + Cmd2GeneralConsole, RichPrintKwargs, ) from .styles import Cmd2Style @@ -161,7 +161,7 @@ # Set up readline if rl_type == RlType.NONE: # pragma: no cover - Cmd2Console(sys.stderr).print(rl_warning, style=Cmd2Style.WARNING) + Cmd2GeneralConsole(sys.stderr).print(rl_warning, style=Cmd2Style.WARNING) else: from .rl_utils import ( readline, @@ -1221,7 +1221,7 @@ def print_to( 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 Cmd2Console. + 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. @@ -1230,7 +1230,7 @@ def print_to( prepared_objects = ru.prepare_objects_for_rich_print(*objects) try: - Cmd2Console(file).print( + Cmd2GeneralConsole(file).print( *prepared_objects, sep=sep, end=end, @@ -1245,7 +1245,7 @@ def print_to( # warning message, then set the broken_pipe_warning attribute # to the message you want printed. if self.broken_pipe_warning and file != sys.stderr: - Cmd2Console(sys.stderr).print(self.broken_pipe_warning) + Cmd2GeneralConsole(sys.stderr).print(self.broken_pipe_warning) def poutput( self, @@ -1267,7 +1267,7 @@ def poutput( 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 Cmd2Console. + 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. @@ -1303,7 +1303,7 @@ def perror( 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 Cmd2Console. + 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. @@ -1337,7 +1337,7 @@ def psuccess( 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 Cmd2Console. + 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. @@ -1370,7 +1370,7 @@ def pwarning( 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 Cmd2Console. + 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. @@ -1404,7 +1404,7 @@ def pexcept( final_msg = Text() if self.debug and sys.exc_info() != (None, None, None): - console = Cmd2Console(sys.stderr) + console = Cmd2GeneralConsole(sys.stderr) console.print_exception(word_wrap=True) else: final_msg += f"EXCEPTION of type '{type(exception).__name__}' occurred with message: {exception}" @@ -1442,7 +1442,7 @@ def pfeedback( 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 Cmd2Console. + 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. @@ -1498,7 +1498,7 @@ def ppaged( 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 Cmd2Console. + 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 @@ -1525,7 +1525,7 @@ def ppaged( soft_wrap = True # Generate the bytes to send to the pager - console = Cmd2Console(self.stdout) + console = Cmd2GeneralConsole(self.stdout) with console.capture() as capture: console.print( *prepared_objects, diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index 05ef523b2..07fcef8a0 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -16,9 +16,7 @@ RenderableType, RichCast, ) -from rich.style import ( - StyleType, -) +from rich.style import StyleType from rich.text import Text from rich.theme import Theme from rich_argparse import RichHelpFormatter @@ -108,14 +106,33 @@ class RichPrintKwargs(TypedDict, total=False): new_line_start: bool -class Cmd2Console(Console): - """Rich console with characteristics appropriate for cmd2-based applications.""" +class Cmd2BaseConsole(Console): + """A base class for Rich consoles in cmd2-based applications.""" - def __init__(self, file: IO[str] | None = None) -> None: - """Cmd2Console initializer. + 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. """ + # Don't allow force_terminal or force_interactive to be passed in, as their + # behavior is controlled by the ALLOW_STYLE setting. + if "force_terminal" in kwargs: + raise TypeError( + "Passing 'force_terminal' is not allowed. Its behavior is controlled by the 'ALLOW_STYLE' setting." + ) + if "force_interactive" in kwargs: + raise TypeError( + "Passing 'force_interactive' is not allowed. Its behavior is controlled by the 'ALLOW_STYLE' setting." + ) + + # Don't allow a theme to be passed in, as it is controlled by the global APP_THEME. + # Use cmd2.rich_utils.set_theme() to set the global theme or use a temporary + # theme with console.use_theme(). + if "theme" in kwargs: + raise TypeError( + "Passing 'theme' is not allowed. Its behavior is controlled by the global APP_THEME and set_theme()." + ) + force_terminal: bool | None = None force_interactive: bool | None = None @@ -128,20 +145,12 @@ def __init__(self, file: IO[str] | None = None) -> None: elif ALLOW_STYLE == AllowStyle.NEVER: force_terminal = False - # The console's defaults are configured to handle pre-formatted text. It enables soft wrap, - # which prevents automatic word-wrapping, and disables Rich's automatic processing for - # markup, emoji, and highlighting. While these features are off by default, the console - # can still fully render explicit Rich objects like Panels and Tables. These defaults can - # be overridden in calls to Cmd2Console.print() and cmd2's print methods. super().__init__( file=file, force_terminal=force_terminal, force_interactive=force_interactive, - soft_wrap=True, - markup=False, - emoji=False, - highlight=False, theme=APP_THEME, + **kwargs, ) def on_broken_pipe(self) -> None: @@ -150,9 +159,37 @@ def on_broken_pipe(self) -> None: raise BrokenPipeError +class Cmd2GeneralConsole(Cmd2BaseConsole): + """Rich console for general-purpose printing in cmd2-based applications.""" + + 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. + """ + # This console is configured for general-purpose printing. It enables soft wrap + # and disables Rich's automatic processing for markup, emoji, and highlighting. + # These defaults can be overridden in calls to the console's or cmd2's print methods. + super().__init__( + file=file, + soft_wrap=True, + markup=False, + emoji=False, + highlight=False, + ) + + +class Cmd2RichArgparseConsole(Cmd2BaseConsole): + """Rich console for rich-argparse output in cmd2-based applications. + + 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. + """ + + def console_width() -> int: """Return the width of the console.""" - return Cmd2Console().width + return Cmd2BaseConsole().width def rich_text_to_string(text: Text) -> str: diff --git a/tests/test_rich_utils.py b/tests/test_rich_utils.py index af6f4b914..61da54238 100644 --- a/tests/test_rich_utils.py +++ b/tests/test_rich_utils.py @@ -12,6 +12,21 @@ from cmd2 import string_utils as su +def test_cmd2_base_console() -> None: + # Test the keyword arguments which are not allowed. + with pytest.raises(TypeError) as excinfo: + ru.Cmd2BaseConsole(force_terminal=True) + assert 'force_terminal' in str(excinfo.value) + + with pytest.raises(TypeError) as excinfo: + ru.Cmd2BaseConsole(force_interactive=True) + assert 'force_interactive' in str(excinfo.value) + + with pytest.raises(TypeError) as excinfo: + ru.Cmd2BaseConsole(theme=None) + assert 'theme' in str(excinfo.value) + + def test_string_to_rich_text() -> None: # Line breaks recognized by str.splitlines(). # Source: https://docs.python.org/3/library/stdtypes.html#str.splitlines @@ -56,7 +71,7 @@ def test_rich_text_to_string(rich_text: Text, string: str) -> None: assert ru.rich_text_to_string(rich_text) == string -def test_set_style() -> None: +def test_set_theme() -> None: # Save a cmd2, rich-argparse, and rich-specific style. cmd2_style_key = Cmd2Style.ERROR argparse_style_key = "argparse.args"