diff --git a/CHANGELOG.md b/CHANGELOG.md index 94dc16788..50ab4d2cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,7 @@ prompt is displayed. - **pre_prompt**: hook method that is called before the prompt is displayed, but after `prompt-toolkit` event loop has started - **read_secret**: read secrets like passwords without displaying them to the terminal + - **ppretty**: a cmd2-compatible replacement for `rich.pretty.pprint()` - New settables: - **max_column_completion_results**: (int) the maximum number of completion results to display in a single column diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 1282d3cb1..43cc0f3ed 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -88,6 +88,7 @@ RenderableType, ) from rich.highlighter import ReprHighlighter +from rich.pretty import Pretty from rich.rule import Rule from rich.style import ( Style, @@ -1822,6 +1823,56 @@ def ppaged( rich_print_kwargs=rich_print_kwargs, ) + def ppretty( + self, + obj: Any, + *, + file: IO[str] | None = None, + indent_size: int = 4, + indent_guides: bool = True, + max_length: int | None = None, + max_string: int | None = None, + max_depth: int | None = None, + expand_all: bool = False, + end: str = "\n", + ) -> None: + """Pretty print an object. + + This is a cmd2-compatible replacement for rich.pretty.pprint(). + + :param obj: object to pretty print + :param file: file stream being written to or None for self.stdout. + Defaults to None. + :param indent_size: number of spaces in indent. Defaults to 4. + :param indent_guides: enable indentation guides. Defaults to True. + :param max_length: maximum length of containers before abbreviating, or None for no abbreviation. + Defaults to None. + :param max_string: maximum length of strings before truncating, or None to disable. Defaults to None. + :param max_depth: maximum depth for nested data structures, or None for unlimited depth. Defaults to None. + :param expand_all: Expand all containers. Defaults to False. + :param end: string to write at end of printed text. Defaults to a newline. + """ + # The overflow and soft_wrap values match those in rich.pretty.pprint(). + # This ensures long strings are neither truncated with ellipses nor broken + # up by injected newlines. + pretty_obj = Pretty( + obj, + indent_size=indent_size, + indent_guides=indent_guides, + max_length=max_length, + max_string=max_string, + max_depth=max_depth, + expand_all=expand_all, + overflow="ignore", + ) + + self.print_to( + file or self.stdout, + pretty_obj, + soft_wrap=True, + end=end, + ) + def get_bottom_toolbar(self) -> list[str | tuple[str, str]] | None: """Get the bottom toolbar content. diff --git a/examples/pretty_print.py b/examples/pretty_print.py index bf3ce9c9c..110f9aa86 100755 --- a/examples/pretty_print.py +++ b/examples/pretty_print.py @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -"""A simple example demonstrating how to pretty print JSON data in a cmd2 app using rich.""" - -from rich.json import JSON +"""A simple example demonstrating how to pretty print data.""" import cmd2 @@ -9,34 +7,20 @@ "name": "John Doe", "age": 30, "address": {"street": "123 Main St", "city": "Anytown", "state": "CA"}, - "hobbies": ["reading", "hiking", "coding"], + "hobbies": ["reading", "hiking", "coding", "cooking", "running", "painting", "music", "photography", "cycling"], + "member": True, + "vip": False, + "phone": None, } class Cmd2App(cmd2.Cmd): def __init__(self) -> None: super().__init__() - self.data = EXAMPLE_DATA - - def do_normal(self, _) -> None: - """Display the data using the normal poutput method.""" - self.poutput(self.data) - - def do_pretty(self, _) -> None: - """Display the JSON data in a pretty way using rich.""" - json_renderable = JSON.from_data( - self.data, - indent=2, - highlight=True, - skip_keys=False, - ensure_ascii=False, - check_circular=True, - allow_nan=True, - default=None, - sort_keys=False, - ) - self.poutput(json_renderable) + def do_pretty(self, _: cmd2.Statement) -> None: + """Print an object using ppretty().""" + self.ppretty(EXAMPLE_DATA) if __name__ == '__main__': diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index e971ae736..0f1e79566 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -3490,6 +3490,46 @@ def test_ppaged_terminal_restoration_oserror(outsim_app, monkeypatch) -> None: assert not termios_mock.tcsetattr.called +def test_ppretty(base_app: cmd2.Cmd) -> None: + # Mock the Pretty class and the print_to() method + with mock.patch('cmd2.cmd2.Pretty') as mock_pretty, mock.patch.object(cmd2.Cmd, 'print_to') as mock_print_to: + # Set up the mock return value for Pretty + mock_pretty_obj = mock.Mock() + mock_pretty.return_value = mock_pretty_obj + + test_obj = {"key": "value"} + + # Call ppretty() with some custom arguments + base_app.ppretty( + test_obj, + indent_size=2, + max_depth=5, + expand_all=True, + end="\n\n", + ) + + # Verify Pretty was instantiated with the correct arguments + mock_pretty.assert_called_once_with( + test_obj, + indent_size=2, + indent_guides=True, + max_length=None, + max_string=None, + max_depth=5, + expand_all=True, + overflow="ignore", + ) + + # Verify print_to() was called with the mock pretty object and soft_wrap=True + # It should default to self.stdout when no file is provided + mock_print_to.assert_called_once_with( + base_app.stdout, + mock_pretty_obj, + soft_wrap=True, + end="\n\n", + ) + + # we override cmd.parseline() so we always get consistent # command parsing by parent methods we don't override # don't need to test all the parsing logic here, because