diff options
-rw-r--r-- | cmd2/ansi.py | 12 | ||||
-rw-r--r-- | cmd2/cmd2.py | 16 | ||||
-rw-r--r-- | docs/api/ansi.rst | 5 | ||||
-rw-r--r-- | docs/api/cmd.rst | 4 | ||||
-rw-r--r-- | docs/api/index.rst | 1 | ||||
-rw-r--r-- | docs/doc_conventions.rst | 119 | ||||
-rw-r--r-- | docs/features/generating_output.rst | 173 | ||||
-rw-r--r-- | docs/features/settings.rst | 62 | ||||
-rw-r--r-- | docs/features/startup_commands.rst | 2 | ||||
-rwxr-xr-x | examples/colors.py | 26 | ||||
-rwxr-xr-x | examples/plumbum_colors.py | 2 |
11 files changed, 332 insertions, 90 deletions
diff --git a/cmd2/ansi.py b/cmd2/ansi.py index f1b2def8..db828af4 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -91,6 +91,7 @@ def ansi_safe_wcswidth(text: str) -> int: Wrap wcswidth to make it compatible with strings that contains ANSI escape sequences :param text: the string being measured + :return: the width of the string when printed to the terminal """ # Strip ANSI escape sequences since they cause wcswidth to return -1 return wcswidth(strip_ansi(text)) @@ -114,7 +115,7 @@ def fg_lookup(fg_name: str) -> str: :param fg_name: foreground color name to look up ANSI escape code(s) for :return: ANSI escape code(s) associated with this color - :raises ValueError if the color cannot be found + :raises ValueError: if the color cannot be found """ try: ansi_escape = FG_COLORS[fg_name.lower()] @@ -128,7 +129,7 @@ def bg_lookup(bg_name: str) -> str: :param bg_name: background color name to look up ANSI escape code(s) for :return: ANSI escape code(s) associated with this color - :raises ValueError if the color cannot be found + :raises ValueError: if the color cannot be found """ try: ansi_escape = BG_COLORS[bg_name.lower()] @@ -184,8 +185,13 @@ def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False, underlin # These can be altered to suit an application's needs and only need to be a # function with the following structure: func(str) -> str style_success = functools.partial(style, fg='green', bold=True) +"""Partial function supplying arguments to style() to generate bold green text""" + style_warning = functools.partial(style, fg='bright_yellow') +"""Partial function supplying arguments to ansi.style() to generate yellow text""" + style_error = functools.partial(style, fg='bright_red') +"""Partial function supplying arguments to ansi.style() to generate bright red text""" def async_alert_str(*, terminal_columns: int, prompt: str, line: str, cursor_offset: int, alert_msg: str) -> str: @@ -246,6 +252,6 @@ def set_title_str(title: str) -> str: """Get the required string, including ANSI escape codes, for setting window title for the terminal. :param title: new title for the window - :return string to write to sys.stderr in order to set the window title to the desired test + :return: string to write to sys.stderr in order to set the window title to the desired test """ return colorama.ansi.set_title(title) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index d2ab6bd3..2ed87fdc 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -289,8 +289,8 @@ class Cmd(cmd.Cmd): # The error that prints when a non-existent command is run self.default_error = "{} is not a recognized command, alias, or macro" + """The error message displayed when a non-existent command is run.""" - # If this string is non-empty, then this warning message will print if a broken pipe error occurs while printing self.broken_pipe_warning = '' # Commands that will run at the beginning of the command loop @@ -413,6 +413,20 @@ class Cmd(cmd.Cmd): self.perror('Invalid value: {} (valid values: {}, {}, {})'.format(new_val, ansi.ANSI_TERMINAL, ansi.ANSI_ALWAYS, ansi.ANSI_NEVER)) + @property + def broken_pipe_warning(self) -> str: + """Message to display if a BrokenPipeError is raised while writing output. + + :meth:`~cmd2.cmd2.Cmd.poutput()` catches BrokenPipeError exceptions and + outputs the contents of `broken_pipe_warning`. The default value is an + empty string meaning the BrokenPipeError is silently swallowed. + """ + return self.broken_pipe_error + + @broken_pipe_warning.setter + def broken_pipe_warning(self, new_val: str) -> None: + self.broken_pipe_error = new_val + def _completion_supported(self) -> bool: """Return whether tab completion is supported""" return self.use_rawinput and self.completekey and rl_type != RlType.NONE diff --git a/docs/api/ansi.rst b/docs/api/ansi.rst new file mode 100644 index 00000000..e361bd4f --- /dev/null +++ b/docs/api/ansi.rst @@ -0,0 +1,5 @@ +cmd2.ansi +========= + +.. automodule:: cmd2.ansi + :members: diff --git a/docs/api/cmd.rst b/docs/api/cmd.rst index 4f88101e..ebec17c3 100644 --- a/docs/api/cmd.rst +++ b/docs/api/cmd.rst @@ -3,3 +3,7 @@ cmd2.Cmd .. autoclass:: cmd2.cmd2.Cmd :members: + + .. attribute:: default_error + + diff --git a/docs/api/index.rst b/docs/api/index.rst index 94d0fdd4..f0324eab 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -7,5 +7,6 @@ API Reference cmd decorators exceptions + ansi utility_classes utility_functions diff --git a/docs/doc_conventions.rst b/docs/doc_conventions.rst index 39302fc6..226c5edc 100644 --- a/docs/doc_conventions.rst +++ b/docs/doc_conventions.rst @@ -22,25 +22,60 @@ In addition: Naming Files ------------ -- all lower case file names +All source files in the documentation must: + +- have all lower case file names - if the name has multiple words, separate them with an underscore -- all documentation file names end in '.rst' +- end in '.rst' + + +Titles and Headings +------------------- + +reStructuredText allows flexibility in how headings are defined. You only have +to worry about the heirarchy of headings within a single file. Sphinx magically +handles the intra-file heirarchy on it's own. This magic means that no matter +how you style titles and headings in the various files that comprise the +documentation, Sphinx will render properly structured output. To ensure we have +a similar consistency when viewing the source files, we use the following +conventions for titles and headings: + +1. When creating a heading for a section, do not use the overline and underline +syntax. Use the underline syntax only:: + + Document Title + ============== + +2. The string of adornment characters on the line following the heading should +be the same length as the title. + +3. The title of a document should use the '=' adornment character on the next +line and only one heading of this level should appear in each file. + +4. Sections within a document should have their titles adorned with the '-' +character:: + + Section Title + ------------- +5. Subsections within a section should have their titles adorned with the '~' +character:: -Heirarchy of headings ---------------------- + Subsection Title + ~~~~~~~~~~~~~~~~ -show the heirarchy of sphinx headings we use, and the conventions (underline -only, no overline) +6. Use two blank lines before every title unless it's the first heading in the +file. Use one blank line after every heading. -Use '=', then '-', then '~'. If your document needs more levels than that, -break it into separate documents. +7. If your document needs more than three levels of sections, break it into +separate documents. -You only have to worry about the heirarchy of headings within a single file. -Sphinx handles the intra-file heirarchy magically on it's own. -Use two blank lines before every heading unless it's the first heading in the -file. Use one blank line after every heading +Indenting +--------- + +In reStructuredText all indenting is significant. Use 2 spaces per indenting +level. Code @@ -85,9 +120,6 @@ and See :ref:`custom title<features/argument_processing:Help Messages>` -[TODO what's the right way to link to source code? Can we make it link to the -tag that the documentation is rendered from?] - Autolinking ----------- @@ -96,6 +128,63 @@ Autolinking Referencing cmd2 API documentation ---------------------------------- +[TODO what's the right way to link to source code? Can we make it link to the +tag that the documentation is rendered from?] + +It's easy to reference and create a link to the API documentation for classes, +methods, functions, or attributes. + + +Referencing Methods +~~~~~~~~~~~~~~~~~~~ + +To reference a method, use one of the following approaches: + +1. Reference the full dotted path of the method:: + + The :meth:`cmd2.cmd2.Cmd.poutput` method is similar to the Python built-in + print function. + +Which renders as: The :meth:`cmd2.cmd2.Cmd.poutput` method is similar to the +Python built-in print function. + +2. Reference the full dotted path to the method, but only display the method +name:: + + The :meth:`~cmd2.cmd2.Cmd.poutput` method is similar to the Python built-in print function. + +Which renders as: The :meth:`~cmd2.cmd2.Cmd.poutput` method is similar to the +Python built-in print function. + +3. Reference a portion of the dotted path of the method:: + + The :meth:`.cmd2.Cmd.poutput` method is similar to the Python built-in print + function. + +Which renders as: The :meth:`.cmd2.Cmd.poutput` method is similar to the Python +built-in print function. + +Avoid either of these approaches: + +1. Reference just the class name without enough dotted path:: + + The :meth:`.Cmd.poutput` method is similar to the Python built-in print + function. + +Because ``cmd2.Cmd`` subclasses ``cmd.Cmd`` from the standard library, this +approach does not clarify which class it is referring to. + +2. Reference just a method name:: + + The :meth:`poutput` method is similar to the Python built-in print + function. + +While Sphinx may be smart enough to generate the correct output, the potential +for multiple matching references is high, which causes Sphinx to generate +warnings. The build pipeline that renders the documentation treats warnings as +fatal errors. It's best to just be specific about what you are referencing. + + Info and Warning Callouts ------------------------- diff --git a/docs/features/generating_output.rst b/docs/features/generating_output.rst index 2ee820f1..58e53962 100644 --- a/docs/features/generating_output.rst +++ b/docs/features/generating_output.rst @@ -1,71 +1,148 @@ Generating Output ================= -how to generate output +A standard ``cmd`` application can produce output by using either of these +methods:: + + print("Greetings, Professor Falken.", file=self.stdout) + self.stdout.write("Shall we play a game?\n") + +While you could send output directly to ``sys.stdout``, ``cmd`` can be +initialized with a ``stdin`` and ``stdout`` variables, which it stores +as ``self.stdin`` and ``self.stdout``. By using these variables every +time you produce output, you can trivially change where all the output +goes by changing how you initialize your class. + +``cmd2`` extends this approach in a number of convenient ways. See +:ref:`features/redirection:Output Redirection And Pipes` for information on how +users can change where the output of a command is sent. In order for those +features to work, the output you generate must be sent to ``self.stdout``. You +can use the methods described above, and everything will work fine. ``cmd2`` +also adds a number of output related methods to ``Cmd2.Cmd`` which you may use +to enhance the output your application produces. + + +TODO: + -- poutput - perror -- paging -- exceptions -- color support +- pwarning +- pexcept +- ppaging + +- column formatting? +- wcswidth? -Standard ``cmd`` applications produce their output with -``self.stdout.write('output')`` (or with ``print``, but ``print`` decreases -output flexibility). ``cmd2`` applications can use ``self.poutput('output')``, -``self.pfeedback('message')``, ``self.perror('errmsg')``, and -``self.ppaged('text')`` instead. These methods have these advantages: +- exceptions -- Handle output redirection to file and/or pipe appropriately -- More concise - - ``.pfeedback()`` destination is controlled by ``quiet`` parameter. -- Option to display long output using a pager via ``ppaged()`` +Ordinary Output +--------------- -.. automethod:: cmd2.cmd2.Cmd.poutput - :noindex: -.. automethod:: cmd2.cmd2.Cmd.perror - :noindex: -.. automethod:: cmd2.cmd2.Cmd.pfeedback - :noindex: -.. automethod:: cmd2.cmd2.Cmd.ppaged - :noindex: +The :meth:`.cmd2.Cmd.poutput` method is similar to the Python +`built-in print function <https://docs.python.org/3/library/functions.html#print>`_. :meth:`~cmd2.cmd2.Cmd.poutput` adds two +conveniences. + 1. Since users can pipe output to a shell command, it catches + ``BrokenPipeError`` and outputs the contents of + ``self.broken_pipe_warning`` to ``stderr``. ``self.broken_pipe_warning`` + defaults to an empty string so this method will just swallow the exception. + If you want to show an error message, put it in + ``self.broken_pipe_warning`` when you initialize ``Cmd2.cmd``. -Suppressing non-essential output --------------------------------- + 2. It examines and honors the :ref:`features/settings:allow_ansi` setting. + See :ref:`features/generating_output:Colored Output` below for more details. -The ``quiet`` setting controls whether ``self.pfeedback()`` actually produces -any output. If ``quiet`` is ``False``, then the output will be produced. If -``quiet`` is ``True``, no output will be produced. +Here's a simple command that shows this method in action:: -This makes ``self.pfeedback()`` useful for non-essential output like status -messages. Users can control whether they would like to see these messages by -changing the value of the ``quiet`` setting. + def do_echo(self, args): + """A simple command showing how poutput() works""" + self.poutput(args) Colored Output -------------- -The output methods in the previous section all honor the ``allow_ansi`` -setting, which has three possible values: +You may want to generate output in different colors, which is typically done by +adding `ANSI escape sequences +<https://en.wikipedia.org/wiki/ANSI_escape_code#Colors>`_ which tell the +terminal to change the foreground and background colors. If you want to give +yourself a headache, you can generate these by hand. You could also use another +Python color library like `plumbum.colors +<https://plumbum.readthedocs.io/en/latest/colors.html>`_, `colored +<https://gitlab.com/dslackw/colored>`_, or `colorama +<https://github.com/tartley/colorama>`_. Colorama is unique because when it's +running on Windows, it wraps ``stdout``, looks for ANSI escape sequences, and +converts them into the appropriate ``win32`` calls to modify the state of the +terminal. + +``cmd2`` imports and uses Colorama and provides a number of convenience methods +for generating colorized output, measuring the screen width of colorized +output, setting the window title in the terminal, and removing ANSI escape +codes from a string. These functions are all documentated in +:mod:`cmd2.ansi`. + +:mod:`cmd2.cmd2.Cmd` includes an :ref:`features/settings:allow_ansi` setting, +which controls whether ANSI escape sequences that instruct the terminal to +colorize output are stripped from the output. The recommended approach is to +construct your application so that it generates colorized output, and then +allow your users to use this setting to remove the colorization if they do not +want it. + +Output generated by any of these +methods will honor the :ref:`features/settings:allow_ansi` setting: + +- :meth:`cmd2.cmd2.Cmd.poutput` +- :meth:`cmd2.cmd2.Cmd.perror` +- :meth:`cmd2.cmd2.Cmd.pwarning` +- :meth:`cmd2.cmd2.Cmd.pexcept` +- :meth:`cmd2.cmd2.Cmd.pfeedback` +- :meth:`cmd2.cmd2.Cmd.ppaged` + + +Error Messages +-------------- + -Never - poutput(), pfeedback(), and ppaged() strip all ANSI escape sequences - which instruct the terminal to colorize output -Terminal - (the default value) poutput(), pfeedback(), and ppaged() do not strip any - ANSI escape sequences when the output is a terminal, but if the output is a - pipe or a file the escape sequences are stripped. If you want colorized - output you must add ANSI escape sequences using either cmd2's internal ansi - module or another color library such as `plumbum.colors`, `colorama`, or - `colored`. +Warning Messages +---------------- + + +Feedback +-------- + +You may have the need to display information to the user which is not intended +to be part of the generated output. This could be debugging information or +status information about the progress of long running commands. It's not +output, it's not error messages, it's feedback. If you use the +:ref:`features/settings:Timing` setting, the output of how long it took the +command to run will be output as feedback. ``cmd2`` has a ``self.pfeedback()`` +method to produce this type of output, and several +:ref:`features/settings:Settings` to control how this output is handled. + +If the ``quiet`` setting is ``True``, then calling ``self.pfeedback()`` +produces no output. If ``quiet`` is ``False``, then the ``feedback_to_output`` +setting is consulted to determine which file descriptor the feedback will be +sent to. The default value of ``False`` means all feedback is sent to +``sys.stderr``. If set to ``True``, then the feedback output will be sent to +``self.stdout`` along with the rest of the generated output. + + +Exceptions +---------- + + +Paging Output +------------- + + +Centering Text +-------------- -Always - poutput(), pfeedback(), and ppaged() never strip ANSI escape sequences, - regardless of the output destination +utils.center_text() -Colored and otherwise styled output can be generated using the `ansi.style()` -function: -.. automethod:: cmd2.ansi.style +Columnar Output +--------------- +Using wcswidth() and ansi.ansi_safe_wcswidth() diff --git a/docs/features/settings.rst b/docs/features/settings.rst index b0575468..25162aed 100644 --- a/docs/features/settings.rst +++ b/docs/features/settings.rst @@ -11,6 +11,26 @@ Built In Settings ``cmd2`` has a number of built in settings, which a developer can set a default value, and which users can modify to change the behavior of the application. +A list of all user-settable parameters, with brief comments, is viewable from +within a running application:: + + (Cmd) set --long + allow_ansi: Terminal # Allow ANSI escape sequences in output (valid values: Terminal, Always, Never) + continuation_prompt: > # On 2nd+ line of input + debug: False # Show full error stack on error + echo: False # Echo command issued into output + editor: vim # Program used by ``edit`` + feedback_to_output: False # include nonessentials in `|`, `>` results + locals_in_py: False # Allow access to your application in py via self + max_completion_items: 50 # Maximum number of CompletionItems to display during tab completion + prompt: (Cmd) # The prompt issued to solicit input + quiet: False # Don't print nonessential feedback + timing: False # Report execution times + +Any of these user-settable parameters can be set while running your app with +the ``set`` command like so:: + + (Cmd) set allow_ansi Never Timing ~~~~~~ @@ -38,31 +58,29 @@ the application generates an error. |settable| .. _parameters: -Other user-settable parameters -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A list of all user-settable parameters, with brief -comments, is viewable from within a running application -with:: +allow_ansi +~~~~~~~~~~ - (Cmd) set --long - allow_ansi: Terminal # Allow ANSI escape sequences in output (valid values: Terminal, Always, Never) - continuation_prompt: > # On 2nd+ line of input - debug: False # Show full error stack on error - echo: False # Echo command issued into output - editor: vim # Program used by ``edit`` - feedback_to_output: False # include nonessentials in `|`, `>` results - locals_in_py: False # Allow access to your application in py via self - max_completion_items: 50 # Maximum number of CompletionItems to display during tab completion - prompt: (Cmd) # The prompt issued to solicit input - quiet: False # Don't print nonessential feedback - timing: False # Report execution times +The ``allow_ansi`` setting controls the behavior of ANSI escape sequences +in output generated with any of the following methods: -Any of these user-settable parameters can be set while running your app with -the ``set`` command like so:: +- ``poutput()`` +- ``perror()`` +- ``pwarning()`` +- ``pfeedback()`` +- ``ppaged()`` - set allow_ansi Never +This setting can be one of three values: +- ``Never`` - all ANSI escape sequences which instruct the terminal to colorize + output are stripped from the output. + +- ``Terminal`` - (the default value) pass through ANSI escape sequences when + the output is being sent to the terminal, but if the output is redirected to + a pipe or a file the escape sequences are stripped. + +- ``Always`` - ANSI escape sequences are always passed through, regardless Create New Settings @@ -100,3 +118,7 @@ changes a setting, and will receive both the old value and the new value. now: 13 (Cmd) sunbathe It's 13 C - are you a penguin? + + +Hide Built-in Settings +---------------------- diff --git a/docs/features/startup_commands.rst b/docs/features/startup_commands.rst index 93984db6..aaaf7722 100644 --- a/docs/features/startup_commands.rst +++ b/docs/features/startup_commands.rst @@ -5,7 +5,7 @@ Startup Commands after your application starts up: 1. Commands at Invocation -1. Startup Script +2. Startup Script Commands run as part of a startup script are always run immediately after the application finishes initializing so they are guaranteed to run before any diff --git a/examples/colors.py b/examples/colors.py index 8c54dfa4..7a4d15e6 100755 --- a/examples/colors.py +++ b/examples/colors.py @@ -24,9 +24,11 @@ Always regardless of the output destination """ import argparse +from typing import Any import cmd2 from cmd2 import ansi +from colorama import Fore, Back, Style class CmdLineApp(cmd2.Cmd): @@ -66,9 +68,31 @@ class CmdLineApp(cmd2.Cmd): repetitions = args.repeat or 1 output_str = ansi.style(' '.join(words), fg=args.fg, bg=args.bg, bold=args.bold, underline=args.underline) - for i in range(min(repetitions, self.maxrepeats)): + for _ in range(min(repetitions, self.maxrepeats)): # .poutput handles newlines, and accommodates output redirection too self.poutput(output_str) + self.perror('error message at the end') + + @staticmethod + def perror(msg: Any, *, end: str = '\n', apply_style: bool = True) -> None: + """Override perror() method from `cmd2.Cmd` + + Use colorama native approach for styling the text instead of `cmd2.ansi` methods + + :param msg: message to print (anything convertible to a str with '{}'.format() is OK) + :param end: string appended after the end of the message, default a newline + :param apply_style: If True, then ansi.style_error will be applied to the message text. Set to False in cases + where the message text already has the desired style. Defaults to True. + """ + if apply_style: + final_msg = "{}{}{}{}".format(Fore.RED, Back.YELLOW, msg, Style.RESET_ALL) + else: + final_msg = "{}".format(msg) + ansi.ansi_aware_write(sys.stderr, final_msg + end) + + def do_timetravel(self, args): + """A command which always generates an error message, to demonstrate custom error colors""" + self.perror('Mr. Fusion failed to start. Could not energize flux capacitor.') if __name__ == '__main__': diff --git a/examples/plumbum_colors.py b/examples/plumbum_colors.py index a969c4de..b4f0ad1c 100755 --- a/examples/plumbum_colors.py +++ b/examples/plumbum_colors.py @@ -104,7 +104,7 @@ class CmdLineApp(cmd2.Cmd): repetitions = args.repeat or 1 output_str = ansi.style(' '.join(words), fg=args.fg, bg=args.bg, bold=args.bold, underline=args.underline) - for i in range(min(repetitions, self.maxrepeats)): + for _ in range(min(repetitions, self.maxrepeats)): # .poutput handles newlines, and accommodates output redirection too self.poutput(output_str) |