diff --git a/CHANGELOG.md b/CHANGELOG.md index 3423fe70..3eee66d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `cmd2` 2.6 supports Python 3.9+ (removed support for Python 3.8) - Enhancements - Add support for Python 3.14 + - Added new `Cmd.ppretty()` method for pretty printing arbitrary Python data structures ## 2.5.11 (January 25, 2025) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index a1845d68..ea6a2a65 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -36,6 +36,7 @@ import glob import inspect import os +import pprint import pydoc import re import sys @@ -1338,6 +1339,17 @@ def ppaged(self, msg: Any, *, end: str = '\n', chop: bool = False) -> None: else: self.poutput(msg, end=end) + def ppretty(self, data: Any, *, indent: int = 2, width: int = 80, depth: Optional[int] = None, end: str = '\n') -> None: + """Pretty print arbitrary Python data structures to self.stdout and appends a newline by default. + + :param data: object to print + :param indent: the amount of indentation added for each nesting level + :param width: the desired maximum number of characters per line in the output, a best effort will be made for long data + :param depth: the number of nesting levels which may be printed, if data is too deep, the next level replaced by ... + :param end: string appended after the end of the message, default a newline + """ + self.print_to(self.stdout, pprint.pformat(data, indent, width, depth), end=end) + # ----- Methods related to tab completion ----- def _reset_completion_defaults(self) -> None: diff --git a/docs/features/generating_output.md b/docs/features/generating_output.md index ce23b5f3..d832dc45 100644 --- a/docs/features/generating_output.md +++ b/docs/features/generating_output.md @@ -80,3 +80,14 @@ These functions differ from Python's string justifying functions in that they su When generating output in multiple columns, you often need to calculate the width of each item so you can pad it appropriately with spaces. However, there are categories of Unicode characters that occupy 2 cells, and other that occupy 0. To further complicate matters, you might have included ANSI escape sequences in the output to generate colors on the terminal. The `cmd2.ansi.style_aware_wcswidth` function solves both of these problems. Pass it a string, and regardless of which Unicode characters and ANSI text style escape sequences it contains, it will tell you how many characters on the screen that string will consume when printed. + +## Pretty Printing Data Structures + +The `cmd2.Cmd.ppretty` method is similar to the Python [pprint](https://docs.python.org/3/library/pprint.html) function from the standard `pprint` module. `cmd2.Cmd.pprint` adds the same conveniences as `cmd2.Cmd.poutput`. + +This method provides a capability to “pretty-print” arbitrary Python data structures in a form which can be used as input to the interpreter and is easy for humans +to read. + +The formatted representation keeps objects on a single line if it can, and breaks them onto multiple lines if they don’t fit within the allowed width, adjustable by the width parameter defaulting to 80 characters. + +Dictionaries are sorted by key before the display is computed. diff --git a/docs/features/settings.md b/docs/features/settings.md index 29f59619..78bc96f1 100644 --- a/docs/features/settings.md +++ b/docs/features/settings.md @@ -16,6 +16,7 @@ Output generated by `cmd2` programs may contain ANSI escape sequences which inst - **`cmd2.Cmd.pexcept`** - **`cmd2.Cmd.pfeedback`** - **`cmd2.Cmd.ppaged`** +- **`cmd2.Cmd.ppretty`** This setting can be one of three values: diff --git a/examples/README.md b/examples/README.md index 660ee68e..3db86021 100644 --- a/examples/README.md +++ b/examples/README.md @@ -68,6 +68,8 @@ Here is the list of examples in alphabetical order by filename along with a brie - Shows how to enable persistent history in your `cmd2` application - [pirate.py](https://github.com/python-cmd2/cmd2/blob/master/examples/pirate.py) - Demonstrates many features including colorized output, multiline commands, shorcuts, defaulting to shell, etc. +- [pretty_print.py](https://github.com/python-cmd2/cmd2/blob/master/examples/pretty_print.py) + - Demonstrates use of cmd2.Cmd.ppretty() for pretty-printing arbitrary Python data structures like dictionaries. - [python_scripting.py](https://github.com/python-cmd2/cmd2/blob/master/examples/python_scripting.py) - Shows how cmd2's built-in `run_pyscript` command can provide advanced Python scripting of cmd2 applications - [read_input.py](https://github.com/python-cmd2/cmd2/blob/master/examples/read_input.py) diff --git a/examples/pretty_print.py b/examples/pretty_print.py new file mode 100755 index 00000000..9cdc5715 --- /dev/null +++ b/examples/pretty_print.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""A simple example demonstrating use of cmd2.Cmd.ppretty().""" + +import cmd2 + +data = { + "name": "John Doe", + "age": 30, + "address": {"street": "123 Main St", "city": "Anytown", "state": "CA"}, + "hobbies": ["reading", "hiking", "coding"], +} + + +class Cmd2App(cmd2.Cmd): + def __init__(self) -> None: + super().__init__() + + def do_normal(self, _) -> None: + """Display the data using the normal poutput method.""" + self.poutput(data) + + def do_pretty(self, _) -> None: + """Display the data using the ppretty method.""" + self.ppretty(data) + + +if __name__ == '__main__': + app = Cmd2App() + app.cmdloop() diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 730f9030..8e23b7ab 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1962,6 +1962,24 @@ def test_poutput_none(outsim_app) -> None: assert out == expected +def test_ppretty_dict(outsim_app) -> None: + data = { + "name": "John Doe", + "age": 30, + "address": {"street": "123 Main St", "city": "Anytown", "state": "CA"}, + "hobbies": ["reading", "hiking", "coding"], + } + outsim_app.ppretty(data) + out = outsim_app.stdout.getvalue() + expected = """ +{ 'address': {'city': 'Anytown', 'state': 'CA', 'street': '123 Main St'}, + 'age': 30, + 'hobbies': ['reading', 'hiking', 'coding'], + 'name': 'John Doe'} +""" + assert out == expected.lstrip() + + @with_ansi_style(ansi.AllowStyle.ALWAYS) def test_poutput_ansi_always(outsim_app) -> None: msg = 'Hello World'