diff options
-rw-r--r-- | .appveyor.yml | 10 | ||||
-rw-r--r-- | CHANGELOG.md | 36 | ||||
-rwxr-xr-x | README.md | 22 | ||||
-rw-r--r-- | cmd2/ansi.py | 15 | ||||
-rw-r--r-- | cmd2/argparse_completer.py | 6 | ||||
-rw-r--r-- | cmd2/argparse_custom.py | 14 | ||||
-rw-r--r-- | cmd2/cmd2.py | 24 | ||||
-rw-r--r-- | cmd2/constants.py | 3 | ||||
-rw-r--r-- | cmd2/utils.py | 110 | ||||
-rw-r--r-- | docs/features/argument_processing.rst | 5 | ||||
-rw-r--r-- | docs/features/completion.rst | 22 | ||||
-rw-r--r-- | docs/features/embedded_python_shells.rst | 2 | ||||
-rw-r--r-- | docs/features/help.rst | 74 | ||||
-rw-r--r-- | docs/features/initialization.rst | 2 | ||||
-rw-r--r-- | docs/features/os.rst | 2 | ||||
-rw-r--r-- | docs/features/scripting.rst | 4 | ||||
-rw-r--r-- | docs/features/settings.rst | 4 | ||||
-rw-r--r-- | docs/overview/installation.rst | 2 | ||||
-rw-r--r-- | examples/argparse_completion.py | 122 | ||||
-rwxr-xr-x | examples/basic_completion.py | 91 | ||||
-rwxr-xr-x | examples/python_scripting.py | 1 | ||||
-rwxr-xr-x | examples/tab_autocompletion.py | 268 | ||||
-rwxr-xr-x | examples/tab_completion.py | 81 | ||||
-rw-r--r-- | tests/conftest.py | 2 | ||||
-rwxr-xr-x | tests/test_cmd2.py | 23 | ||||
-rwxr-xr-x | tests/test_completion.py | 14 | ||||
-rw-r--r-- | tests/test_utils.py | 54 |
27 files changed, 521 insertions, 492 deletions
diff --git a/.appveyor.yml b/.appveyor.yml index b335fe55..3e147335 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -27,9 +27,13 @@ install: - "python --version" - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" - # Upgrade to the latest version of pip to avoid it displaying warnings - # about it being out of date. - - "python -m pip install --upgrade pip wheel setuptools tox" + # Update conda stuff to make sure pip, setuptools, wheel etc are up to date + - "conda update --all -y" + + # Install tox + - "python -m pip install --upgrade tox" + + test_script: - "tox -e %TOXENV%" diff --git a/CHANGELOG.md b/CHANGELOG.md index 3879cdde..f9d71831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,15 @@ +## 0.10.1 (TBD) +* Bug Fixes + * Corrected issue where the actual new value was not always being printed in do_set. This occurred in cases where + the typed value differed from what the setter had converted it to. + * Fixed bug where ANSI style sequences were not correctly handled in `utils.truncate_line()`. +* Enhancements + * Renamed set command's `-l/--long` flag to `-v/--verbose` for consistency with help and history commands. + ## 0.10.0 (February 7, 2020) * Enhancements * Changed the default help text to make `help -v` more discoverable - * **set** command now supports tab-completion of values + * **set** command now supports tab completion of values * Added `add_settable()` and `remove_settable()` convenience methods to update `self.settable` dictionary * Added convenience `ansi.fg` and `ansi.bg` enums of foreground and background colors * `ansi.style()` `fg` argument can now either be of type `str` or `ansi.fg` @@ -177,7 +185,7 @@ `delimiter_complete`, `flag_based_complete`, `index_based_complete`, `path_complete`, `shell_cmd_complete` * Renamed history option from `--output-file` to `--output_file` * Renamed `matches_sort_key` to `default_sort_key`. This value determines the default sort ordering of string - results like alias, command, category, macro, settable, and shortcut names. Unsorted tab-completion results + results like alias, command, category, macro, settable, and shortcut names. Unsorted tab completion results also are sorted with this key. Its default value (ALPHABETICAL_SORT_KEY) performs a case-insensitive alphabetical sort, but it can be changed to a natural sort by setting the value to NATURAL_SORT_KEY. * `StatementParser` now expects shortcuts to be passed in as dictionary. This eliminates the step of converting the @@ -426,7 +434,7 @@ * ``ACHelpFormatter`` now inherits from ``argparse.RawTextHelpFormatter`` to make it easier for formatting help/description text * Aliases are now sorted alphabetically - * The **set** command now tab-completes settable parameter names + * The **set** command now tab completes settable parameter names * Added ``async_alert``, ``async_update_prompt``, and ``set_window_title`` functions * These allow you to provide feedback to the user in an asychronous fashion, meaning alerts can display when the user is still entering text at the prompt. See [async_printing.py](https://github.com/python-cmd2/cmd2/blob/master/examples/async_printing.py) @@ -460,7 +468,7 @@ * Improved implementation of lifecycle hooks to support a plugin framework, see ``docs/hooks.rst`` for details. * New dependency on ``attrs`` third party module - * Added ``matches_sorted`` member to support custom sorting of tab-completion matches + * Added ``matches_sorted`` member to support custom sorting of tab completion matches * Added [tab_autocomp_dynamic.py](https://github.com/python-cmd2/cmd2/blob/master/examples/tab_autocomp_dynamic.py) example * Demonstrates updating the argparse object during init instead of during class construction * Deprecations @@ -490,7 +498,7 @@ * Bug Fixes * Fixed issue where piping and redirecting did not work correctly with paths that had spaces * Enhancements - * Added ability to print a header above tab-completion suggestions using `completion_header` member + * Added ability to print a header above tab completion suggestions using `completion_header` member * Added ``pager`` and ``pager_chop`` attributes to the ``cmd2.Cmd`` class * ``pager`` defaults to **less -RXF** on POSIX and **more** on Windows * ``pager_chop`` defaults to **less -SRXF** on POSIX and **more** on Windows @@ -562,7 +570,7 @@ * Fixed ``AttributeError`` on Windows when running a ``select`` command cause by **pyreadline** not implementing ``remove_history_item`` * Enhancements * Added warning about **libedit** variant of **readline** not being supported on macOS - * Added tab-completion of alias names in value field of **alias** command + * Added tab completion of alias names in value field of **alias** command * Enhanced the ``py`` console in the following ways * Added tab completion of Python identifiers instead of **cmd2** commands * Separated the ``py`` console history from the **cmd2** history @@ -620,7 +628,7 @@ ## 0.8.2 (March 21, 2018) * Bug Fixes - * Fixed a bug in tab-completion of command names within sub-menus + * Fixed a bug in tab completion of command names within sub-menus * Fixed a bug when using persistent readline history in Python 2.7 * Fixed a bug where the ``AddSubmenu`` decorator didn't work with a default value for ``shared_attributes`` * Added a check to ``ppaged()`` to only use a pager when running in a real fully functional terminal @@ -677,7 +685,7 @@ and [arg_print.py](https://github.com/python-cmd2/cmd2/blob/master/examples/arg_print.py) examples * Added support for Argparse subcommands when using the **with_argument_parser** or **with_argparser_and_unknown_args** decorators * See [subcommands.py](https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py) for an example of how to use subcommands - * Tab-completion of subcommand names is automatically supported + * Tab completion of subcommand names is automatically supported * The **__relative_load** command is now hidden from the help menu by default * This command is not intended to be called from the command line, only from within scripts * The **set** command now has an additional **-a/--all** option to also display read-only settings @@ -704,7 +712,7 @@ * Fixed a couple broken examples * Enhancements * Improved documentation for modifying shortcuts (command aliases) - * Made ``pyreadline`` a dependency on Windows to ensure tab-completion works + * Made ``pyreadline`` a dependency on Windows to ensure tab completion works * Other changes * Abandoned official support for Python 3.3. It should still work, just don't have an easy way to test it anymore. @@ -742,7 +750,7 @@ * Fixed some pyperclip clipboard interaction bugs on Linux * Fixed some timing bugs when running unit tests in parallel by using monkeypatch * Enhancements - * Enhanced tab-completion of cmd2 command names to support case-insensitive completion + * Enhanced tab completion of cmd2 command names to support case-insensitive completion * Added an example showing how to remove unused commands * Improved how transcript testing handles prompts with ANSI escape codes by stripping them * Greatly improved implementation for how command output gets piped to a shell command @@ -758,7 +766,7 @@ * Enhancements * Organized all attributes used to configure the ParserManager into a single location * Set the default value of `abbrev` to `False` (which controls whether or not abbreviated commands are allowed) - * With good tab-completion of command names, using abbreviated commands isn't particularly useful + * With good tab completion of command names, using abbreviated commands isn't particularly useful * And it can create complications if you are't careful * Improved implementation of `load` to use command queue instead of nested inner loop @@ -770,7 +778,7 @@ * Ability to pipe ``cmd2`` command output to a shell command is now more reliable, particularly on Windows * Fixed a bug in ``pyscript`` command on Windows related to ``\`` being interpreted as an escape * Enhancements - * Ensure that path and shell command tab-completion results are alphabetically sorted + * Ensure that path and shell command tab completion results are alphabetically sorted * Removed feature for load command to load scripts from URLS * It didn't work, there were no unit tests, and it felt out of place * Removed presence of a default file name and default file extension @@ -793,8 +801,8 @@ * Enhancements * Added the ability to exclude commands from the help menu (**eof** included by default) * Redundant **list** command removed and features merged into **history** command - * Added **pyscript** command which supports tab-completion and running Python scripts with arguments - * Improved tab-completion of file system paths, command names, and shell commands + * Added **pyscript** command which supports tab completion and running Python scripts with arguments + * Improved tab completion of file system paths, command names, and shell commands * Thanks to Kevin Van Brunt for all of the help with debugging and testing this * Changed default value of USE_ARG_LIST to True - this affects the beavhior of all **@options** commands * **WARNING**: This breaks backwards compatibility, to restore backwards compatibility, add this to the @@ -37,8 +37,8 @@ Main Features - Settable environment parameters - Parsing commands with arguments using `argparse`, including support for subcommands - Unicode character support -- Good tab-completion of commands, subcommands, file system paths, and shell commands -- Automatic tab-completion of `argparse` flags when using one of the `cmd2` `argparse` decorators +- Good tab completion of commands, subcommands, file system paths, and shell commands +- Automatic tab completion of `argparse` flags when using one of the `cmd2` `argparse` decorators - Support for Python 3.5+ on Windows, macOS, and Linux - Trivial to provide built-in help for all commands - Built-in regression testing framework for your applications (transcript-based testing) @@ -96,7 +96,7 @@ Instructions for implementing each feature follow. - By default the docstring for your **do_foo** method is the help for the **foo** command - NOTE: This doesn't apply if you use one of the `argparse` decorators mentioned below - Can provide more custom help by creating a **help_foo** method (except when using `argparse` decorators) - - Can provide custom tab-completion for the **foo** command by creating a **complete_foo** method + - Can provide custom tab completion for the **foo** command by creating a **complete_foo** method - Easy to upgrade an existing `cmd` app to `cmd2` - Run your `cmd2` app using the built-in REPL by executing the **cmdloop** method @@ -164,25 +164,25 @@ Instructions for implementing each feature follow. - Option to display long output using a pager with ``cmd2.Cmd.ppaged()`` - Optionally specify a startup script that end users can use to customize their environment -- Top-notch tab-completion capabilities which are easy to use but very powerful +- Top-notch tab completion capabilities which are easy to use but very powerful - For a command **foo** implement a **complete_foo** method to provide custom tab completion for that command - But the helper methods within `cmd2` discussed below mean you would rarely have to implement this from scratch - - Commands which use one of the `argparse` decorators have automatic tab-completion of `argparse` flags + - Commands which use one of the `argparse` decorators have automatic tab completion of `argparse` flags - And also provide help hints for values associated with these flags - Experiment with the [argprint.py](https://github.com/python-cmd2/cmd2/blob/master/examples/arg_print.py) example using the **oprint** and **pprint** commands to get a feel for how this works - - `path_complete` helper method provides flexible tab-completion of file system paths + - `path_complete` helper method provides flexible tab completion of file system paths - See the [paged_output.py](https://github.com/python-cmd2/cmd2/blob/master/examples/paged_output.py) example for a simple use case - See the [python_scripting.py](https://github.com/python-cmd2/cmd2/blob/master/examples/python_scripting.py) example for a more full-featured use case - `flag_based_complete` helper method for tab completion based on a particular flag preceding the token being completed - - See the [tab_completion.py](https://github.com/python-cmd2/cmd2/blob/master/examples/tab_completion.py) example for a demonstration of how to use this feature + - See the [basic_completion.py](https://github.com/python-cmd2/cmd2/blob/master/examples/basic_completion.py) example for a demonstration of how to use this feature - `index_based_complete` helper method for tab completion based on a fixed position in the input string - - See the [tab_completion.py](https://github.com/python-cmd2/cmd2/blob/master/examples/tab_completion.py) example for a demonstration of how to use this feature + - See the [basic_completion.py](https://github.com/python-cmd2/cmd2/blob/master/examples/basic_completion.py) example for a demonstration of how to use this feature - `basic_complete` helper method for tab completion against a list - `delimiter_complete` helper method for tab completion against a list but each match is split on a delimiter - - See the [tab_autocompletion.py](https://github.com/python-cmd2/cmd2/blob/master/examples/tab_autocompletion.py) example for a demonstration of how to use this feature - - `cmd2` in combination with `argparse` also provide several advanced capabilities for automatic tab-completion - - See the [tab_autocompletion.py](https://github.com/python-cmd2/cmd2/blob/master/examples/tab_autocompletion.py) example for more info + - See the [basic_completion.py](https://github.com/python-cmd2/cmd2/blob/master/examples/basic_completion.py) example for a demonstration of how to use this feature + - `cmd2` in combination with `argparse` also provide several advanced capabilities for automatic tab completion + - See the [argparse_completion.py](https://github.com/python-cmd2/cmd2/blob/master/examples/argparse_completion.py) example for more info - Multi-line commands diff --git a/cmd2/ansi.py b/cmd2/ansi.py index fbe51b9a..27c9e87a 100644 --- a/cmd2/ansi.py +++ b/cmd2/ansi.py @@ -282,22 +282,13 @@ def style(text: Any, *, fg: Union[str, fg] = '', bg: Union[str, bg] = '', bold: # 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=fg.green) -""" -Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors -text green to signify success. -""" +"""Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors text to signify success""" style_warning = functools.partial(style, fg=fg.bright_yellow) -""" -Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors -text yellow to signify a warning. -""" +"""Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors text to signify a warning""" style_error = functools.partial(style, fg=fg.bright_red) -""" -Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors -text red to signify an error. -""" +"""Partial function supplying arguments to :meth:`cmd2.ansi.style()` which colors text to signify an error""" def async_alert_str(*, terminal_columns: int, prompt: str, line: str, cursor_offset: int, alert_msg: str) -> str: diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 6513fe13..185e01a2 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -444,7 +444,9 @@ class AutoCompleter: completions.sort(key=self._cmd2_app.default_sort_key) self._cmd2_app.matches_sorted = True - token_width = ansi.style_aware_wcswidth(action.dest) + # If a metavar was defined, use that instead of the dest field + destination = action.metavar if action.metavar else action.dest + token_width = ansi.style_aware_wcswidth(destination) completions_with_desc = [] for item in completions: @@ -463,7 +465,7 @@ class AutoCompleter: desc_header = getattr(action, ATTR_DESCRIPTIVE_COMPLETION_HEADER, None) if desc_header is None: desc_header = DEFAULT_DESCRIPTIVE_HEADER - header = '\n{: <{token_width}}{}'.format(action.dest.upper(), desc_header, token_width=token_width + 2) + header = '\n{: <{token_width}}{}'.format(destination.upper(), desc_header, token_width=token_width + 2) self._cmd2_app.completion_header = header self._cmd2_app.display_matches = completions_with_desc diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index a0e05ae9..a59270c3 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -62,8 +62,8 @@ Tab Completion: return my_generated_list completer_function - Pass a tab-completion function that does custom completion. Since custom tab completion operations commonly - need to modify cmd2's instance variables related to tab-completion, it will be rare to need a completer + Pass a tab completion function that does custom completion. Since custom tab completion operations commonly + need to modify cmd2's instance variables related to tab completion, it will be rare to need a completer function. completer_method should be used in those cases. Example: @@ -90,7 +90,7 @@ Tab Completion: path_filter=lambda path: os.path.isdir(path)) parser.add_argument('-o', '--options', choices_method=completer_method) - Of the 5 tab-completion parameters, choices is the only one where argparse validates user input against items + Of the 5 tab completion parameters, choices is the only one where argparse validates user input against items in the choices list. This is because the other 4 parameters are meant to tab complete data sets that are viewed as dynamic. Therefore it is up to the developer to validate if the user has typed an acceptable value for these arguments. @@ -118,7 +118,7 @@ Tab Completion: the developer to determine if the user entered the correct argument type (e.g. int) and validate their values. CompletionError Class: - Raised during tab-completion operations to report any sort of error you want printed by the AutoCompleter + Raised during tab completion operations to report any sort of error you want printed by the AutoCompleter Example use cases - Reading a database to retrieve a tab completion data set failed @@ -231,7 +231,7 @@ def generate_range_error(range_min: int, range_max: Union[int, float]) -> str: class CompletionError(Exception): """ - Raised during tab-completion operations to report any sort of error you want printed by the AutoCompleter + Raised during tab completion operations to report any sort of error you want printed by the AutoCompleter Example use cases - Reading a database to retrieve a tab completion data set failed @@ -356,8 +356,8 @@ def _add_argument_wrapper(self, *args, # Added args used by AutoCompleter :param choices_function: function that provides choices for this argument :param choices_method: cmd2-app method that provides choices for this argument - :param completer_function: tab-completion function that provides choices for this argument - :param completer_method: cmd2-app tab-completion method that provides choices for this argument + :param completer_function: tab completion function that provides choices for this argument + :param completer_method: cmd2-app tab completion method that provides choices for this argument :param suppress_tab_hint: when AutoCompleter has no results to show during tab completion, it displays the current argument's help text as a hint. Set this to True to suppress the hint. If this argument's help text is set to argparse.SUPPRESS, then tab hints will not display regardless of the diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 2c35a163..8f2cdca3 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -431,9 +431,8 @@ class Cmd(cmd.Cmd): if new_val in [ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER]: ansi.allow_style = new_val else: - raise ValueError('Invalid value: {} (valid values: {}, {}, {})'.format(new_val, ansi.STYLE_TERMINAL, - ansi.STYLE_ALWAYS, - ansi.STYLE_NEVER)) + raise ValueError("must be {}, {}, or {} (case-insensitive)".format(ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, + ansi.STYLE_NEVER)) def _completion_supported(self) -> bool: """Return whether tab completion is supported""" @@ -2852,7 +2851,7 @@ class Cmd(cmd.Cmd): "Call without arguments for a list of all settable parameters with their values.\n" "Call with just param to view that parameter's value.") set_parser_parent = DEFAULT_ARGUMENT_PARSER(description=set_description, add_help=False) - set_parser_parent.add_argument('-l', '--long', action='store_true', + set_parser_parent.add_argument('-v', '--verbose', action='store_true', help='include description of parameters when viewing') set_parser_parent.add_argument('param', nargs=argparse.OPTIONAL, help='parameter to set or view', choices_method=_get_settable_completion_items, descriptive_header='Description') @@ -2886,8 +2885,8 @@ class Cmd(cmd.Cmd): # Try to update the settable's value try: orig_value = getattr(self, args.param) - new_value = settable.val_type(args.value) - setattr(self, args.param, new_value) + setattr(self, args.param, settable.val_type(args.value)) + new_value = getattr(self, args.param) # noinspection PyBroadException except Exception as e: err_msg = "Error setting {}: {}".format(args.param, e) @@ -2917,7 +2916,7 @@ class Cmd(cmd.Cmd): # Display the results for param in sorted(results, key=self.default_sort_key): result_str = results[param] - if args.long: + if args.verbose: self.poutput('{} # {}'.format(utils.align_left(result_str, width=max_len), self.settables[param].description)) else: @@ -3814,9 +3813,6 @@ class Cmd(cmd.Cmd): # Sanity check that can't fail if self.terminal_lock was acquired before calling this function if self.terminal_lock.acquire(blocking=False): - # Figure out what prompt is displaying - current_prompt = self.continuation_prompt if self._at_continuation_prompt else self.prompt - # Only update terminal if there are changes update_terminal = False @@ -3835,6 +3831,8 @@ class Cmd(cmd.Cmd): if update_terminal: import shutil + + current_prompt = self.continuation_prompt if self._at_continuation_prompt else self.prompt terminal_str = ansi.async_alert_str(terminal_columns=shutil.get_terminal_size().columns, prompt=current_prompt, line=readline.get_line_buffer(), cursor_offset=rl_get_point(), alert_msg=alert_msg) @@ -3867,9 +3865,9 @@ class Cmd(cmd.Cmd): a prompt is onscreen. Therefore it is best to acquire the lock before calling this function to guarantee the prompt changes. - If a continuation prompt is currently being displayed while entering a multiline - command, the onscreen prompt will not change. However self.prompt will still be updated - and display immediately after the multiline line command completes. + If user is at a continuation prompt while entering a multiline command, the onscreen prompt will + not change. However self.prompt will still be updated and display immediately after the multiline + line command completes. :param new_prompt: what to change the prompt to """ diff --git a/cmd2/constants.py b/cmd2/constants.py index 9e8e7780..bc72817f 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -15,6 +15,9 @@ MULTILINE_TERMINATOR = ';' LINE_FEED = '\n' +# One character ellipsis +HORIZONTAL_ELLIPSIS = '…' + DEFAULT_SHORTCUTS = {'?': 'help', '!': 'shell', '@': 'run_script', '@@': '_relative_run_script'} # Used as the command name placeholder in disabled command messages. diff --git a/cmd2/utils.py b/cmd2/utils.py index cfe75f53..e324c2f1 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -11,7 +11,7 @@ import sys import threading import unicodedata from enum import Enum -from typing import Any, Callable, Iterable, List, Optional, TextIO, Union +from typing import Any, Callable, Dict, Iterable, List, Optional, TextIO, Union from . import constants @@ -682,8 +682,8 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False) -> str: """ Align text for display within a given width. Supports characters with display widths greater than 1. - ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is - supported. If text has line breaks, then each line is aligned independently. + ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned + independently. There are convenience wrappers around this function: align_left(), align_center(), and align_right() @@ -696,7 +696,7 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ', :param truncate: if True, then each line will be shortened to fit within the display width. The truncated portions are replaced by a '…' character. Defaults to False. :return: aligned text - :raises: TypeError if fill_char is more than one character + :raises: TypeError if fill_char is more than one character (not including ANSI style sequences) ValueError if text or fill_char contains an unprintable character ValueError if width is less than 1 """ @@ -716,7 +716,7 @@ def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ', if fill_char == '\t': fill_char = ' ' - if len(fill_char) != 1: + if len(ansi.strip_style(fill_char)) != 1: raise TypeError("Fill character must be exactly one character long") fill_char_width = ansi.style_aware_wcswidth(fill_char) @@ -777,8 +777,8 @@ def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False) -> str: """ Left align text for display within a given width. Supports characters with display widths greater than 1. - ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is - supported. If text has line breaks, then each line is aligned independently. + ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned + independently. :param text: text to left align (can contain multiple lines) :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) @@ -788,7 +788,7 @@ def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None, :param truncate: if True, then text will be shortened to fit within the display width. The truncated portion is replaced by a '…' character. Defaults to False. :return: left-aligned text - :raises: TypeError if fill_char is more than one character + :raises: TypeError if fill_char is more than one character (not including ANSI style sequences) ValueError if text or fill_char contains an unprintable character ValueError if width is less than 1 """ @@ -800,8 +800,8 @@ def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None tab_width: int = 4, truncate: bool = False) -> str: """ Center text for display within a given width. Supports characters with display widths greater than 1. - ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is - supported. If text has line breaks, then each line is aligned independently. + ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned + independently. :param text: text to center (can contain multiple lines) :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) @@ -811,7 +811,7 @@ def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None :param truncate: if True, then text will be shortened to fit within the display width. The truncated portion is replaced by a '…' character. Defaults to False. :return: centered text - :raises: TypeError if fill_char is more than one character + :raises: TypeError if fill_char is more than one character (not including ANSI style sequences) ValueError if text or fill_char contains an unprintable character ValueError if width is less than 1 """ @@ -823,8 +823,8 @@ def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4, truncate: bool = False) -> str: """ Right align text for display within a given width. Supports characters with display widths greater than 1. - ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is - supported. If text has line breaks, then each line is aligned independently. + ANSI style sequences do not count toward the display width. If text has line breaks, then each line is aligned + independently. :param text: text to right align (can contain multiple lines) :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) @@ -834,7 +834,7 @@ def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None, :param truncate: if True, then text will be shortened to fit within the display width. The truncated portion is replaced by a '…' character. Defaults to False. :return: right-aligned text - :raises: TypeError if fill_char is more than one character + :raises: TypeError if fill_char is more than one character (not including ANSI style sequences) ValueError if text or fill_char contains an unprintable character ValueError if width is less than 1 """ @@ -845,8 +845,15 @@ def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None, def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str: """ Truncate a single line to fit within a given display width. Any portion of the string that is truncated - is replaced by a '…' character. Supports characters with display widths greater than 1. ANSI style sequences are - safely ignored and do not count toward the display width. This means colored text is supported. + is replaced by a '…' character. Supports characters with display widths greater than 1. ANSI style sequences + do not count toward the display width. + + If there are ANSI style sequences in the string after where truncation occurs, this function will append them + to the returned string. + + This is done to prevent issues caused in cases like: truncate_string(fg.blue + hello + fg.reset, 3) + In this case, "hello" would be truncated before fg.reset resets the color from blue. Appending the remaining style + sequences makes sure the style is in the same state had the entire string been printed. :param line: text to truncate :param max_width: the maximum display width the resulting string is allowed to have @@ -855,6 +862,7 @@ def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str: :raises: ValueError if text contains an unprintable character like a new line ValueError if max_width is less than 1 """ + import io from . import ansi # Handle tabs @@ -866,12 +874,68 @@ def truncate_line(line: str, max_width: int, *, tab_width: int = 4) -> str: if max_width < 1: raise ValueError("max_width must be at least 1") - if ansi.style_aware_wcswidth(line) > max_width: - # Remove characters until we fit. Leave room for the ellipsis. - line = line[:max_width - 1] - while ansi.style_aware_wcswidth(line) > max_width - 1: - line = line[:-1] + if ansi.style_aware_wcswidth(line) <= max_width: + return line + + # Find all style sequences in the line + styles = get_styles_in_text(line) + + # Add characters one by one and preserve all style sequences + done = False + index = 0 + total_width = 0 + truncated_buf = io.StringIO() + + while not done: + # Check if a style sequence is at this index. These don't count toward display width. + if index in styles: + truncated_buf.write(styles[index]) + style_len = len(styles[index]) + styles.pop(index) + index += style_len + continue + + char = line[index] + char_width = ansi.style_aware_wcswidth(char) + + # This char will make the text too wide, add the ellipsis instead + if char_width + total_width >= max_width: + char = constants.HORIZONTAL_ELLIPSIS + char_width = ansi.style_aware_wcswidth(char) + done = True + + total_width += char_width + truncated_buf.write(char) + index += 1 + + # Append remaining style sequences from original string + truncated_buf.write(''.join(styles.values())) + + return truncated_buf.getvalue() + + +def get_styles_in_text(text: str) -> Dict[int, str]: + """ + Return an OrderedDict containing all ANSI style sequences found in a string + + The structure of the dictionary is: + key: index where sequences begins + value: ANSI style sequence found at index in text + + Keys are in ascending order + + :param text: text to search for style sequences + """ + from . import ansi + + start = 0 + styles = collections.OrderedDict() - line += "\N{HORIZONTAL ELLIPSIS}" + while True: + match = ansi.ANSI_STYLE_RE.search(text, start) + if match is None: + break + styles[match.start()] = match.group() + start += len(match.group()) - return line + return styles diff --git a/docs/features/argument_processing.rst b/docs/features/argument_processing.rst index 9d98ea93..82244d7e 100644 --- a/docs/features/argument_processing.rst +++ b/docs/features/argument_processing.rst @@ -325,14 +325,13 @@ Subcommands are supported for commands using either the ``@with_argparser`` or is based on argparse sub-parsers. You may add multiple layers of subcommands for your command. ``cmd2`` will -automatically traverse and tab-complete subcommands for all commands using +automatically traverse and tab complete subcommands for all commands using argparse. -See the subcommands_ and tab_autocompletion_ example to learn more about how to +See the subcommands_ example to learn more about how to use subcommands in your ``cmd2`` application. .. _subcommands: https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py -.. _tab_autocompletion: https://github.com/python-cmd2/cmd2/blob/master/examples/tab_autocompletion.py Argparse Extensions diff --git a/docs/features/completion.rst b/docs/features/completion.rst index c8b0a813..ffb5f80b 100644 --- a/docs/features/completion.rst +++ b/docs/features/completion.rst @@ -1,7 +1,7 @@ Completion ========== -``cmd2`` adds tab-completion of file system paths for all built-in commands +``cmd2`` adds tab completion of file system paths for all built-in commands where it makes sense, including: - ``edit`` @@ -9,7 +9,7 @@ where it makes sense, including: - ``run_script`` - ``shell`` -``cmd2`` also adds tab-completion of shell commands to the ``shell`` command. +``cmd2`` also adds tab completion of shell commands to the ``shell`` command. Additionally, it is trivial to add identical file system path completion to your own custom commands. Suppose you have defined a custom command ``foo`` by @@ -17,7 +17,7 @@ implementing the ``do_foo`` method. To enable path completion for the ``foo`` command, then add a line of code similar to the following to your class which inherits from ``cmd2.Cmd``:: - complete_foo = self.path_complete + complete_foo = cmd2.Cmd.path_complete This will effectively define the ``complete_foo`` readline completer method in your class and make it utilize the same path completion logic as the built-in @@ -39,7 +39,7 @@ Tab Completion Using Argparse Decorators When using one the Argparse-based :ref:`api/decorators:cmd2.decorators`, ``cmd2`` provides automatic tab-completion of flag names. -Tab-completion of argument values can be configured by using one of five +Tab completion of argument values can be configured by using one of five parameters to ``argparse.ArgumentParser.add_argument()`` - ``choices`` @@ -47,26 +47,26 @@ parameters to ``argparse.ArgumentParser.add_argument()`` - ``completer_function`` / ``completer_method`` See the arg_decorators_ or colors_ example for a demonstration of how to -use the ``choices`` parameter. See the tab_autocompletion_ example for a +use the ``choices`` parameter. See the argparse_completion_ example for a demonstration of how to use the ``choices_function`` and ``choices_method`` -parameters. See the arg_decorators_ or tab_autocompletion_ example for a +parameters. See the arg_decorators_ or argparse_completion_ example for a demonstration of how to use the ``completer_method`` parameter. -When tab-completing flags and/or argument values for a ``cmd2`` command using +When tab completing flags and/or argument values for a ``cmd2`` command using one of these decorators, ``cmd2`` keeps track of state so that once a flag has -already previously been provided, it won't attempt to tab-complete it again. +already previously been provided, it won't attempt to tab complete it again. When no completion results exists, a hint for the current argument will be displayed to help the user. .. _arg_decorators: https://github.com/python-cmd2/cmd2/blob/master/examples/arg_decorators.py .. _colors: https://github.com/python-cmd2/cmd2/blob/master/examples/colors.py -.. _tab_autocompletion: https://github.com/python-cmd2/cmd2/blob/master/examples/tab_autocompletion.py +.. _argparse_completion: https://github.com/python-cmd2/cmd2/blob/master/examples/argparse_completion.py CompletionItem For Providing Extra Context ------------------------------------------ -When tab-completing things like a unique ID from a database, it can often be +When tab completing things like a unique ID from a database, it can often be beneficial to provide the user with some extra context about the item being completed, such as a description. To facilitate this, ``cmd2`` defines the ``CompletionItem`` class which can be returned from any of the 4 completion @@ -76,5 +76,5 @@ or ``completion_method``. .. autoclass:: cmd2.argparse_custom.CompletionItem :members: -See the tab_autocompletion_ example or the implementation of the built-in +See the argparse_completion_ example or the implementation of the built-in **set** command for demonstration of how this is used. diff --git a/docs/features/embedded_python_shells.rst b/docs/features/embedded_python_shells.rst index 68377876..70765b21 100644 --- a/docs/features/embedded_python_shells.rst +++ b/docs/features/embedded_python_shells.rst @@ -67,7 +67,7 @@ code directory for an example of how to achieve this in your own applications. Using ``py`` to run scripts directly is considered deprecated. The newer ``run_pyscript`` command is superior for doing this in two primary ways: -- it supports tab-completion of file system paths +- it supports tab completion of file system paths - it has the ability to pass command-line arguments to the scripts invoked There are no disadvantages to using ``run_pyscript`` as opposed to ``py diff --git a/docs/features/help.rst b/docs/features/help.rst index 920516f5..57aa2187 100644 --- a/docs/features/help.rst +++ b/docs/features/help.rst @@ -1,11 +1,60 @@ Help ==== -use the categorize() function to create help categories +From our experience, end users rarely read documentation no matter how high- +quality or useful that documentation might be. So it is important that you +provide good built-in help within your application. Fortunately, ``cmd2`` +makes this easy. -Use ``help_method()`` to custom roll your own help messages. +Getting Help +------------ -See :ref:`features/argument_processing:Help Messages` +``cmd2`` makes it easy for end users of ``cmd2`` applications to get help via +the built-in ``help`` command. The ``help`` command by itself displays a list +of the commands available: + +.. code-block:: text + + (Cmd) help + + Documented commands (use 'help -v' for verbose/'help <topic>' for details): + =========================================================================== + alias help ipy py run_pyscript set shortcuts + edit history macro quit run_script shell + +The ``help`` command can also be used to provide detailed help for a specific +command: + +.. code-block:: text + + (Cmd) help quit + Usage: quit [-h] + + Exit this application + + optional arguments: + -h, --help show this help message and exit + +Providing Help +-------------- + +``cmd2`` makes it easy for developers of ``cmd2`` applications to provide this +help. By default, the help for a command is the docstring for the ``do_*`` +method defining the command - e.g. for a command **foo**, that command is +implementd by defining the ``do_foo`` method and the docstring for that method +is the help. + +For commands which use one of the ``argparse`` decorators to parse arguments, +help is provided by ``argparse``. See +:ref:`features/argument_processing:Help Messages` for more information. + +Occasionally there might be an unusual circumstance where providing static help +text isn't good enough and you want to provide dynamic information in the help +text for a command. To meet this need, if a ``help_foo`` method is defined to +match the ``do_foo`` method, then that method will be used to provide the help +for command **foo**. This dynamic help is only supported for commands which +do not use an ``argparse`` decorator because didn't want different output for +``help cmd`` than for ``cmd -h``. Categorizing Commands --------------------- @@ -124,18 +173,21 @@ The ``help`` command also has a verbose option (``help -v`` or ``help Other ================================================================================ - alias Define or display aliases + alias Manage aliases config Config command - edit Edit a file in a text editor - help List available commands with "help" or detailed help with "help cmd" - history usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT] [arg] - py Invoke python command, shell, or script + edit Run a text editor and optionally open a file with it + help List available commands or provide detailed help for a specific command + history View, run, edit, save, or clear previously entered commands + macro Manage macros + py Invoke Python command or shell quit Exits this application run_pyscript Runs a python script file inside the console run_script Runs commands in script file that is encoded as either ASCII or UTF-8 text - set usage: set [-h] [-a] [-l] [settable [settable ...]] + set Set a settable parameter or show current settings of parameters shell Execute a command as if at the OS prompt - shortcuts Lists shortcuts available - unalias Unsets aliases + shortcuts List available shortcuts version Version command +When called with the ``-v`` flag for verbose help, the one-line description for +each command is provided by the first line of the docstring for that command's +associated ``do_*`` method. diff --git a/docs/features/initialization.rst b/docs/features/initialization.rst index 6824c7bf..b1ca4f05 100644 --- a/docs/features/initialization.rst +++ b/docs/features/initialization.rst @@ -133,7 +133,7 @@ override: command via ``self`` (Default: ``False``) - **macros**: dictionary of macro names and their values - **max_completion_items**: max number of CompletionItems to display during - tab-completion (Default: 50) + tab completion (Default: 50) - **pager**: sets the pager command used by the ``Cmd.ppaged()`` method for displaying wrapped output using a pager - **pager_chop**: sets the pager command used by the ``Cmd.ppaged()`` method diff --git a/docs/features/os.rst b/docs/features/os.rst index 89905d17..77bc6a66 100644 --- a/docs/features/os.rst +++ b/docs/features/os.rst @@ -20,7 +20,7 @@ to type:: (Cmd) !ls -al -NOTE: ``cmd2`` provides user-friendly tab-completion throughout the process of +NOTE: ``cmd2`` provides user-friendly tab completion throughout the process of running a shell command - first for the shell command name itself, and then for file paths in the argument section. diff --git a/docs/features/scripting.rst b/docs/features/scripting.rst index 62af2e6d..1128f5e1 100644 --- a/docs/features/scripting.rst +++ b/docs/features/scripting.rst @@ -34,7 +34,7 @@ Running Command Scripts Command script files can be executed using the built-in ``run_script`` command or ``@`` shortcut. Both ASCII and UTF-8 encoded unicode text files are -supported. The ``run_script`` command supports tab-completion of file system +supported. The ``run_script`` command supports tab completion of file system paths. There is a variant ``_relative_run_script`` command or ``@@`` shortcut for use within a script which uses paths relative to the first script. @@ -73,7 +73,7 @@ using ``run_pyscript`` is shown below along with the arg_printer_ script:: arg 2: 'bar' arg 3: 'baz 23' -``run_pyscript`` supports tab-completion of file system paths, and as shown +``run_pyscript`` supports tab completion of file system paths, and as shown above it has the ability to pass command-line arguments to the scripts invoked. Python scripts executed with ``run_pyscript`` can run ``cmd2`` application diff --git a/docs/features/settings.rst b/docs/features/settings.rst index 5a4a9c0f..aa3e5cec 100644 --- a/docs/features/settings.rst +++ b/docs/features/settings.rst @@ -89,11 +89,11 @@ max_completion_items ~~~~~~~~~~~~~~~~~~~~ Maximum number of CompletionItems to display during tab completion. A -CompletionItem is a special kind of tab-completion hint which displays both a +CompletionItem is a special kind of tab completion hint which displays both a value and description and uses one line for each hint. Tab complete the ``set`` command for an example. -If the number of tab-completion hints exceeds ``max_completion_items``, then +If the number of tab completion hints exceeds ``max_completion_items``, then they will be displayed in the typical columnized format and will not include the description text of the CompletionItem. diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index d8d24ebd..6080fe90 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -119,7 +119,7 @@ macOS Considerations -------------------- macOS comes with the `libedit <http://thrysoee.dk/editline/>`_ library which is -similar, but not identical, to GNU Readline. Tab-completion for ``cmd2`` +similar, but not identical, to GNU Readline. Tab completion for ``cmd2`` applications is only tested against GNU Readline. There are several ways GNU Readline can be installed within a Python diff --git a/examples/argparse_completion.py b/examples/argparse_completion.py new file mode 100644 index 00000000..90975d3f --- /dev/null +++ b/examples/argparse_completion.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +# coding=utf-8 +""" +A simple example demonstrating how to integrate tab completion with argparse-based commands. +""" +import argparse +from typing import Dict, List + +from cmd2 import Cmd, Cmd2ArgumentParser, with_argparser, CompletionError, CompletionItem +from cmd2.utils import basic_complete + +# Data source for argparse.choices +food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato'] + + +def choices_function() -> List[str]: + """Choices functions are useful when the choice list is dynamically generated (e.g. from data in a database)""" + return ['a', 'dynamic', 'list', 'goes', 'here'] + + +def completer_function(text: str, line: str, begidx: int, endidx: int) -> List[str]: + """ + A tab completion function not dependent on instance data. Since custom tab completion operations commonly + need to modify cmd2's instance variables related to tab completion, it will be rare to need a completer + function. completer_method should be used in those cases. + """ + match_against = ['a', 'dynamic', 'list', 'goes', 'here'] + return basic_complete(text, line, begidx, endidx, match_against) + + +def choices_completion_item() -> List[CompletionItem]: + """Return CompletionItem instead of strings. These give more context to what's being tab completed.""" + items = \ + { + 1: "My item", + 2: "Another item", + 3: "Yet another item" + } + return [CompletionItem(item_id, description) for item_id, description in items.items()] + + +def choices_arg_tokens(arg_tokens: Dict[str, List[str]]) -> List[str]: + """ + If a choices or completer function/method takes a value called arg_tokens, then it will be + passed a dictionary that maps the command line tokens up through the one being completed + to their argparse argument name. All values of the arg_tokens dictionary are lists, even if + a particular argument expects only 1 token. + """ + # Check if choices_function flag has appeared + values = ['choices_function', 'flag'] + if 'choices_function' in arg_tokens: + values.append('is {}'.format(arg_tokens['choices_function'][0])) + else: + values.append('not supplied') + return values + + +class ArgparseCompletion(Cmd): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] + + def choices_method(self) -> List[str]: + """Choices methods are useful when the choice list is based on instance data of your application""" + return self.sport_item_strs + + def choices_completion_error(self) -> List[str]: + """ + CompletionErrors can be raised if an error occurs while tab completing. + + Example use cases + - Reading a database to retrieve a tab completion data set failed + - A previous command line argument that determines the data set being completed is invalid + """ + if self.debug: + return self.sport_item_strs + raise CompletionError("debug must be true") + + # Parser for example command + example_parser = Cmd2ArgumentParser(description="Command demonstrating tab completion with argparse\n" + "Notice even the flags of this command tab complete") + + # Tab complete from a list using argparse choices. Set metavar if you don't + # want the entire choices list showing in the usage text for this command. + example_parser.add_argument('--choices', choices=food_item_strs, metavar="CHOICE", + help="tab complete using choices") + + # Tab complete from choices provided by a choices function and choices method + example_parser.add_argument('--choices_function', choices_function=choices_function, + help="tab complete using a choices_function") + example_parser.add_argument('--choices_method', choices_method=choices_method, + help="tab complete using a choices_method") + + # Tab complete using a completer function and completer method + example_parser.add_argument('--completer_function', completer_function=completer_function, + help="tab complete using a completer_function") + example_parser.add_argument('--completer_method', completer_method=Cmd.path_complete, + help="tab complete using a completer_method") + + # Demonstrate raising a CompletionError while tab completing + example_parser.add_argument('--completion_error', choices_method=choices_completion_error, + help="raise a CompletionError while tab completing if debug is False") + + # Demonstrate returning CompletionItems instead of strings + example_parser.add_argument('--completion_item', choices_function=choices_completion_item, metavar="ITEM_ID", + descriptive_header="Description", + help="demonstrate use of CompletionItems") + + # Demonstrate use of arg_tokens dictionary + example_parser.add_argument('--arg_tokens', choices_function=choices_arg_tokens, + help="demonstrate use of arg_tokens dictionary") + + @with_argparser(example_parser) + def do_example(self, _: argparse.Namespace) -> None: + """The example command""" + self.poutput("I do nothing") + + +if __name__ == '__main__': + import sys + app = ArgparseCompletion() + sys.exit(app.cmdloop()) diff --git a/examples/basic_completion.py b/examples/basic_completion.py new file mode 100755 index 00000000..e021828b --- /dev/null +++ b/examples/basic_completion.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# coding=utf-8 +""" +A simple example demonstrating how to enable tab completion by assigning a completer function to do_* commands. +This also demonstrates capabilities of the following completer methods included with cmd2: +- delimiter_completer +- flag_based_complete (see note below) +- index_based_complete (see note below) + +flag_based_complete() and index_based_complete() are basic methods and should only be used if you are not +familiar with argparse. The recommended approach for tab completing positional tokens and flags is to use +argparse-based completion. For an example integrating tab completion with argparse, see argparse_completion.py +""" +import functools + +import cmd2 + +# List of strings used with completion functions +food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato'] +sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] + +# This data is used to demonstrate delimiter_complete +file_strs = \ + [ + '/home/user/file.db', + '/home/user/file space.db', + '/home/user/another.db', + '/home/other user/maps.db', + '/home/other user/tests.db' + ] + + +class BasicCompletion(cmd2.Cmd): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def do_flag_based(self, statement: cmd2.Statement): + """Tab completes arguments based on a preceding flag using flag_based_complete + -f, --food [completes food items] + -s, --sport [completes sports] + -p, --path [completes local file system paths] + """ + self.poutput("Args: {}".format(statement.args)) + + def complete_flag_based(self, text, line, begidx, endidx): + """Completion function for do_flag_based""" + flag_dict = \ + { + # Tab complete food items after -f and --food flags in command line + '-f': food_item_strs, + '--food': food_item_strs, + + # Tab complete sport items after -s and --sport flags in command line + '-s': sport_item_strs, + '--sport': sport_item_strs, + + # Tab complete using path_complete function after -p and --path flags in command line + '-p': self.path_complete, + '--path': self.path_complete, + } + + return self.flag_based_complete(text, line, begidx, endidx, flag_dict=flag_dict) + + def do_index_based(self, statement: cmd2.Statement): + """Tab completes first 3 arguments using index_based_complete""" + self.poutput("Args: {}".format(statement.args)) + + def complete_index_based(self, text, line, begidx, endidx): + """Completion function for do_index_based""" + index_dict = \ + { + 1: food_item_strs, # Tab complete food items at index 1 in command line + 2: sport_item_strs, # Tab complete sport items at index 2 in command line + 3: self.path_complete, # Tab complete using path_complete function at index 3 in command line + } + + return self.index_based_complete(text, line, begidx, endidx, index_dict=index_dict) + + def do_delimiter_complete(self, statement: cmd2.Statement): + """Tab completes files from a list using delimiter_complete""" + self.poutput("Args: {}".format(statement.args)) + + # Use a partialmethod to set arguments to delimiter_complete + complete_delimiter_complete = functools.partialmethod(cmd2.Cmd.delimiter_complete, + match_against=file_strs, delimiter='/') + + +if __name__ == '__main__': + import sys + app = BasicCompletion() + sys.exit(app.cmdloop()) diff --git a/examples/python_scripting.py b/examples/python_scripting.py index fc23c562..198e784d 100755 --- a/examples/python_scripting.py +++ b/examples/python_scripting.py @@ -87,6 +87,7 @@ class CmdLineApp(cmd2.Cmd): # Enable tab completion for cd command def complete_cd(self, text, line, begidx, endidx): + # Tab complete only directories return self.path_complete(text, line, begidx, endidx, path_filter=os.path.isdir) dir_parser = argparse.ArgumentParser() diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py deleted file mode 100755 index 3561f968..00000000 --- a/examples/tab_autocompletion.py +++ /dev/null @@ -1,268 +0,0 @@ -#!/usr/bin/env python3 -# coding=utf-8 -""" -A example usage of the AutoCompleter -""" -import argparse -import functools -from typing import List - -import cmd2 -from cmd2 import utils, Cmd2ArgumentParser, CompletionItem - -actors = ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew', - 'Anthony Daniels', 'Adam Driver', 'Daisy Ridley', 'John Boyega', 'Oscar Isaac', - 'Lupita Nyong\'o', 'Andy Serkis', 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman', - 'Jake Lloyd', 'Hayden Christensen', 'Christopher Lee'] - - -def query_actors() -> List[str]: - """Simulating a function that queries and returns a completion values""" - return actors - - -class TabCompleteExample(cmd2.Cmd): - """ Example cmd2 application where we a base command which has a couple subcommands.""" - - CAT_AUTOCOMPLETE = 'AutoComplete Examples' - - def __init__(self): - super().__init__() - - # For mocking a data source for the example commands - ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17'] - show_ratings = ['TV-Y', 'TV-Y7', 'TV-G', 'TV-PG', 'TV-14', 'TV-MA'] - static_list_directors = ['J. J. Abrams', 'Irvin Kershner', 'George Lucas', 'Richard Marquand', - 'Rian Johnson', 'Gareth Edwards'] - USER_MOVIE_LIBRARY = ['ROGUE1', 'SW_EP04', 'SW_EP05'] - MOVIE_DATABASE_IDS = ['SW_EP1', 'SW_EP02', 'SW_EP03', 'ROGUE1', 'SW_EP04', - 'SW_EP05', 'SW_EP06', 'SW_EP07', 'SW_EP08', 'SW_EP09'] - MOVIE_DATABASE = {'SW_EP04': {'title': 'Star Wars: Episode IV - A New Hope', - 'rating': 'PG', - 'director': ['George Lucas'], - 'actor': ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', - 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels'] - }, - 'SW_EP05': {'title': 'Star Wars: Episode V - The Empire Strikes Back', - 'rating': 'PG', - 'director': ['Irvin Kershner'], - 'actor': ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', - 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels'] - }, - 'SW_EP06': {'title': 'Star Wars: Episode VI - Return of the Jedi', - 'rating': 'PG', - 'director': ['Richard Marquand'], - 'actor': ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', - 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels'] - }, - 'SW_EP1': {'title': 'Star Wars: Episode I - The Phantom Menace', - 'rating': 'PG', - 'director': ['George Lucas'], - 'actor': ['Liam Neeson', 'Ewan McGregor', 'Natalie Portman', 'Jake Lloyd'] - }, - 'SW_EP02': {'title': 'Star Wars: Episode II - Attack of the Clones', - 'rating': 'PG', - 'director': ['George Lucas'], - 'actor': ['Liam Neeson', 'Ewan McGregor', 'Natalie Portman', - 'Hayden Christensen', 'Christopher Lee'] - }, - 'SW_EP03': {'title': 'Star Wars: Episode III - Revenge of the Sith', - 'rating': 'PG-13', - 'director': ['George Lucas'], - 'actor': ['Liam Neeson', 'Ewan McGregor', 'Natalie Portman', - 'Hayden Christensen'] - }, - - } - USER_SHOW_LIBRARY = {'SW_REB': ['S01E01', 'S02E02']} - SHOW_DATABASE_IDS = ['SW_CW', 'SW_TCW', 'SW_REB'] - SHOW_DATABASE = {'SW_CW': {'title': 'Star Wars: Clone Wars', - 'rating': 'TV-Y7', - 'seasons': {1: ['S01E01', 'S01E02', 'S01E03'], - 2: ['S02E01', 'S02E02', 'S02E03']} - }, - 'SW_TCW': {'title': 'Star Wars: The Clone Wars', - 'rating': 'TV-PG', - 'seasons': {1: ['S01E01', 'S01E02', 'S01E03'], - 2: ['S02E01', 'S02E02', 'S02E03']} - }, - 'SW_REB': {'title': 'Star Wars: Rebels', - 'rating': 'TV-Y7', - 'seasons': {1: ['S01E01', 'S01E02', 'S01E03'], - 2: ['S02E01', 'S02E02', 'S02E03']} - }, - } - - file_list = \ - [ - '/home/user/file.db', - '/home/user/file space.db', - '/home/user/another.db', - '/home/other user/maps.db', - '/home/other user/tests.db' - ] - - # noinspection PyMethodMayBeStatic - def instance_query_actors(self) -> List[str]: - """Simulating a function that queries and returns a completion values""" - return actors - - def instance_query_movie_ids(self) -> List[str]: - """Demonstrates showing tabular hinting of tab completion information""" - completions_with_desc = [] - - # Sort the movie id strings with a natural sort since they contain numbers - for movie_id in utils.natural_sort(self.MOVIE_DATABASE_IDS): - if movie_id in self.MOVIE_DATABASE: - movie_entry = self.MOVIE_DATABASE[movie_id] - completions_with_desc.append(CompletionItem(movie_id, movie_entry['title'])) - - # Mark that we already sorted the matches - self.matches_sorted = True - return completions_with_desc - - # This demonstrates a number of customizations of the AutoCompleter version of ArgumentParser - # - The help output will separately group required vs optional flags - # - The help output for arguments with multiple flags or with append=True is more concise - # - cmd2 adds the ability to specify ranges of argument counts in 'nargs' - - suggest_description = "Suggest command demonstrates argparse customizations.\n" - suggest_description += "See hybrid_suggest and orig_suggest to compare the help output." - suggest_parser = Cmd2ArgumentParser(description=suggest_description) - - suggest_parser.add_argument('-t', '--type', choices=['movie', 'show'], required=True) - suggest_parser.add_argument('-d', '--duration', nargs=(1, 2), action='append', - help='Duration constraint in minutes.\n' - '\tsingle value - maximum duration\n' - '\t[a, b] - duration range') - - @cmd2.with_category(CAT_AUTOCOMPLETE) - @cmd2.with_argparser(suggest_parser) - def do_suggest(self, args) -> None: - """Suggest command demonstrates argparse customizations""" - if not args.type: - self.do_help('suggest') - - # If you prefer the original argparse help output but would like narg ranges, it's possible - # to enable narg ranges without the help changes using this method - - suggest_parser_hybrid = argparse.ArgumentParser() - suggest_parser_hybrid.add_argument('-t', '--type', choices=['movie', 'show'], required=True) - suggest_parser_hybrid.add_argument('-d', '--duration', nargs=(1, 2), action='append', - help='Duration constraint in minutes.\n' - '\tsingle value - maximum duration\n' - '\t[a, b] - duration range') - - @cmd2.with_category(CAT_AUTOCOMPLETE) - @cmd2.with_argparser(suggest_parser_hybrid) - def do_hybrid_suggest(self, args): - if not args.type: - self.do_help('orig_suggest') - - # This variant demonstrates the AutoCompleter working with the orginial argparse. - # Base argparse is unable to specify narg ranges. Autocompleter will keep expecting additional arguments - # for the -d/--duration flag until you specify a new flag or end processing of flags with '--' - - suggest_parser_orig = argparse.ArgumentParser() - - suggest_parser_orig.add_argument('-t', '--type', choices=['movie', 'show'], required=True) - suggest_parser_orig.add_argument('-d', '--duration', nargs='+', action='append', - help='Duration constraint in minutes.\n' - '\tsingle value - maximum duration\n' - '\t[a, b] - duration range') - - @cmd2.with_argparser(suggest_parser_orig) - @cmd2.with_category(CAT_AUTOCOMPLETE) - def do_orig_suggest(self, args) -> None: - if not args.type: - self.do_help('orig_suggest') - - def _do_vid_movies(self, args) -> None: - if not args.command: - self.do_help('video movies') - elif args.command == 'list': - for movie_id in TabCompleteExample.MOVIE_DATABASE: - movie = TabCompleteExample.MOVIE_DATABASE[movie_id] - print('{}\n-----------------------------\n{} ID: {}\nDirector: {}\nCast:\n {}\n\n' - .format(movie['title'], movie['rating'], movie_id, - ', '.join(movie['director']), - '\n '.join(movie['actor']))) - - def _do_vid_shows(self, args) -> None: - if not args.command: - self.do_help('video shows') - - elif args.command == 'list': - for show_id in TabCompleteExample.SHOW_DATABASE: - show = TabCompleteExample.SHOW_DATABASE[show_id] - print('{}\n-----------------------------\n{} ID: {}' - .format(show['title'], show['rating'], show_id)) - for season in show['seasons']: - ep_list = show['seasons'][season] - print(' Season {}:\n {}' - .format(season, - '\n '.join(ep_list))) - print() - - video_parser = Cmd2ArgumentParser() - - video_types_subparsers = video_parser.add_subparsers(title='Media Types', dest='type') - - vid_movies_parser = video_types_subparsers.add_parser('movies') - vid_movies_parser.set_defaults(func=_do_vid_movies) - - vid_movies_commands_subparsers = vid_movies_parser.add_subparsers(title='Commands', dest='command') - - vid_movies_list_parser = vid_movies_commands_subparsers.add_parser('list') - - vid_movies_list_parser.add_argument('-t', '--title', help='Title Filter') - vid_movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+', - choices=ratings_types) - vid_movies_list_parser.add_argument('-d', '--director', help='Director Filter', choices=static_list_directors) - vid_movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append', - choices_function=query_actors) - - vid_movies_add_parser = vid_movies_commands_subparsers.add_parser('add') - vid_movies_add_parser.add_argument('title', help='Movie Title') - vid_movies_add_parser.add_argument('rating', help='Movie Rating', choices=ratings_types) - - vid_movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True, - choices=static_list_directors) - vid_movies_add_parser.add_argument('actor', help='Actors', nargs='*', choices_method=instance_query_actors) - - vid_movies_load_parser = vid_movies_commands_subparsers.add_parser('load') - vid_movies_load_parser.add_argument('movie_file', help='Movie database', - completer_method=functools.partial(cmd2.Cmd.delimiter_complete, - delimiter='/', match_against=file_list)) - - vid_movies_read_parser = vid_movies_commands_subparsers.add_parser('read') - vid_movies_read_parser.add_argument('movie_file', help='Movie database', completer_method=cmd2.Cmd.path_complete) - - vid_movies_delete_parser = vid_movies_commands_subparsers.add_parser('delete') - vid_movies_delete_parser.add_argument('movie_id', help='Movie ID', choices_method=instance_query_movie_ids, - descriptive_header='Title') - - vid_shows_parser = video_types_subparsers.add_parser('shows') - vid_shows_parser.set_defaults(func=_do_vid_shows) - - vid_shows_commands_subparsers = vid_shows_parser.add_subparsers(title='Commands', dest='command') - - vid_shows_list_parser = vid_shows_commands_subparsers.add_parser('list') - - @cmd2.with_category(CAT_AUTOCOMPLETE) - @cmd2.with_argparser(video_parser) - def do_video(self, args): - """Video management command demonstrates multiple layers of subcommands being handled by AutoCompleter""" - func = getattr(args, 'func', None) - if func is not None: - # Call whatever subcommand function was selected - func(self, args) - else: - # No subcommand was provided, so call help - self.do_help('video') - - -if __name__ == '__main__': - import sys - app = TabCompleteExample() - sys.exit(app.cmdloop()) diff --git a/examples/tab_completion.py b/examples/tab_completion.py deleted file mode 100755 index 1a25238f..00000000 --- a/examples/tab_completion.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python -# coding=utf-8 -""" -A simple example demonstrating how to use flag and index based tab-completion functions -For argparse-based tab completion, see tab_autocompletion.py -""" -import argparse - -import cmd2 - -# List of strings used with flag and index based completion functions -food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato'] -sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] - - -class TabCompleteExample(cmd2.Cmd): - """ Example cmd2 application where we a base command which has a couple subcommands.""" - - def __init__(self): - super().__init__() - - add_item_parser = argparse.ArgumentParser() - add_item_group = add_item_parser.add_mutually_exclusive_group() - add_item_group.add_argument('-f', '--food', help='Adds food item') - add_item_group.add_argument('-s', '--sport', help='Adds sport item') - add_item_group.add_argument('-o', '--other', help='Adds other item') - - @cmd2.with_argparser(add_item_parser) - def do_add_item(self, args): - """Add item command help""" - if args.food: - add_item = args.food - elif args.sport: - add_item = args.sport - elif args.other: - add_item = args.other - else: - add_item = 'no items' - - self.poutput("You added {}".format(add_item)) - - # Add flag-based tab-completion to add_item command - def complete_add_item(self, text, line, begidx, endidx): - flag_dict = \ - { - # Tab-complete food items after -f and --food flags in command line - '-f': food_item_strs, - '--food': food_item_strs, - - # Tab-complete sport items after -s and --sport flags in command line - '-s': sport_item_strs, - '--sport': sport_item_strs, - - # Tab-complete using path_complete function after -o and --other flags in command line - '-o': self.path_complete, - '--other': self.path_complete, - } - - return self.flag_based_complete(text, line, begidx, endidx, flag_dict=flag_dict) - - @cmd2.with_argument_list - def do_list_item(self, args): - """List item command help""" - self.poutput("You listed {}".format(args)) - - # Add index-based tab-completion to list_item command - def complete_list_item(self, text, line, begidx, endidx): - index_dict = \ - { - 1: food_item_strs, # Tab-complete food items at index 1 in command line - 2: sport_item_strs, # Tab-complete sport items at index 2 in command line - 3: self.path_complete, # Tab-complete using path_complete function at index 3 in command line - } - - return self.index_based_complete(text, line, begidx, endidx, index_dict=index_dict) - - -if __name__ == '__main__': - import sys - app = TabCompleteExample() - sys.exit(app.cmdloop()) diff --git a/tests/conftest.py b/tests/conftest.py index 7f77a207..9ee8da19 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -185,7 +185,7 @@ def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Opti def get_endidx(): return endidx - # Run the readline tab-completion function with readline mocks in place + # Run the readline tab completion function with readline mocks in place with mock.patch.object(readline, 'get_line_buffer', get_line): with mock.patch.object(readline, 'get_begidx', get_begidx): with mock.patch.object(readline, 'get_endidx', get_endidx): diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 0b4c60d6..376658e5 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -79,7 +79,7 @@ def test_base_argparse_help(base_app): def test_base_invalid_option(base_app): out, err = run_cmd(base_app, 'set -z') - assert err[0] == 'Usage: set [-h] [-l] [param] [value]' + assert err[0] == 'Usage: set [-h] [-v] [param] [value]' assert 'Error: unrecognized arguments: -z' in err[1] def test_base_shortcuts(base_app): @@ -103,7 +103,7 @@ def test_base_show(base_app): def test_base_show_long(base_app): # force editor to be 'vim' so test is repeatable across platforms base_app.editor = 'vim' - out, err = run_cmd(base_app, 'set -l') + out, err = run_cmd(base_app, 'set -v') expected = normalize(SHOW_LONG) assert out == expected @@ -145,13 +145,13 @@ def test_set_no_settables(base_app): @pytest.mark.parametrize('new_val, is_valid, expected', [ - (ansi.STYLE_NEVER, False, ansi.STYLE_NEVER), - ('neVeR', False, ansi.STYLE_NEVER), - (ansi.STYLE_TERMINAL, False, ansi.STYLE_TERMINAL), - ('TeRMInal', False, ansi.STYLE_TERMINAL), - (ansi.STYLE_ALWAYS, False, ansi.STYLE_ALWAYS), - ('AlWaYs', False, ansi.STYLE_ALWAYS), - ('invalid', True, ansi.STYLE_TERMINAL), + (ansi.STYLE_NEVER, True, ansi.STYLE_NEVER), + ('neVeR', True, ansi.STYLE_NEVER), + (ansi.STYLE_TERMINAL, True, ansi.STYLE_TERMINAL), + ('TeRMInal', True, ansi.STYLE_TERMINAL), + (ansi.STYLE_ALWAYS, True, ansi.STYLE_ALWAYS), + ('AlWaYs', True, ansi.STYLE_ALWAYS), + ('invalid', False, ansi.STYLE_TERMINAL), ]) def test_set_allow_style(base_app, new_val, is_valid, expected): # Initialize allow_style for this test @@ -161,14 +161,17 @@ def test_set_allow_style(base_app, new_val, is_valid, expected): out, err = run_cmd(base_app, 'set allow_style {}'.format(new_val)) # Verify the results - assert bool(err) == is_valid assert ansi.allow_style == expected + if is_valid: + assert not err + assert "now: {!r}".format(new_val.capitalize()) in out[1] # Reload ansi module to reset allow_style to its default since it's an # application-wide setting that can affect other unit tests. import importlib importlib.reload(ansi) + class OnChangeHookApp(cmd2.Cmd): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/tests/test_completion.py b/tests/test_completion.py index 99f832a4..f545c8f9 100755 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -1,9 +1,9 @@ # coding=utf-8 # flake8: noqa E302 """ -Unit/functional testing for readline tab-completion functions in the cmd2.py module. +Unit/functional testing for readline tab completion functions in the cmd2.py module. -These are primarily tests related to readline completer functions which handle tab-completion of cmd2/cmd commands, +These are primarily tests related to readline completer functions which handle tab completion of cmd2/cmd commands, file system paths, and shell commands. """ # Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available @@ -39,11 +39,11 @@ delimited_strs = \ # Dictionary used with flag based completion functions flag_dict = \ { - # Tab-complete food items after -f and --food flag in command line + # Tab complete food items after -f and --food flag in command line '-f': food_item_strs, '--food': food_item_strs, - # Tab-complete sport items after -s and --sport flag in command line + # Tab complete sport items after -s and --sport flag in command line '-s': sport_item_strs, '--sport': sport_item_strs, } @@ -51,14 +51,14 @@ flag_dict = \ # Dictionary used with index based completion functions index_dict = \ { - 1: food_item_strs, # Tab-complete food items at index 1 in command line - 2: sport_item_strs, # Tab-complete sport items at index 2 in command line + 1: food_item_strs, # Tab complete food items at index 1 in command line + 2: sport_item_strs, # Tab complete sport items at index 2 in command line } class CompletionsExample(cmd2.Cmd): """ - Example cmd2 application used to exercise tab-completion tests + Example cmd2 application used to exercise tab completion tests """ def __init__(self): cmd2.Cmd.__init__(self, multiline_commands=['test_multiline']) diff --git a/tests/test_utils.py b/tests/test_utils.py index 5030ce0e..7546184e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -10,6 +10,7 @@ import time import pytest import cmd2.utils as cu +from cmd2.constants import HORIZONTAL_ELLIPSIS HELLO_WORLD = 'Hello, world!' @@ -297,7 +298,13 @@ def test_truncate_line(): line = 'long' max_width = 3 truncated = cu.truncate_line(line, max_width) - assert truncated == 'lo\N{HORIZONTAL ELLIPSIS}' + assert truncated == 'lo' + HORIZONTAL_ELLIPSIS + +def test_truncate_line_already_fits(): + line = 'long' + max_width = 4 + truncated = cu.truncate_line(line, max_width) + assert truncated == line def test_truncate_line_with_newline(): line = 'fo\no' @@ -315,20 +322,44 @@ def test_truncate_line_wide_text(): line = '苹苹other' max_width = 6 truncated = cu.truncate_line(line, max_width) - assert truncated == '苹苹o\N{HORIZONTAL ELLIPSIS}' + assert truncated == '苹苹o' + HORIZONTAL_ELLIPSIS def test_truncate_line_split_wide_text(): """Test when truncation results in a string which is shorter than max_width""" line = '1苹2苹' max_width = 3 truncated = cu.truncate_line(line, max_width) - assert truncated == '1\N{HORIZONTAL ELLIPSIS}' + assert truncated == '1' + HORIZONTAL_ELLIPSIS def test_truncate_line_tabs(): line = 'has\ttab' max_width = 9 truncated = cu.truncate_line(line, max_width) - assert truncated == 'has t\N{HORIZONTAL ELLIPSIS}' + assert truncated == 'has t' + HORIZONTAL_ELLIPSIS + +def test_truncate_with_style(): + from cmd2 import ansi + + before_style = ansi.fg.blue + ansi.UNDERLINE_ENABLE + after_style = ansi.fg.reset + ansi.UNDERLINE_DISABLE + + # Style only before truncated text + line = before_style + 'long' + max_width = 3 + truncated = cu.truncate_line(line, max_width) + assert truncated == before_style + 'lo' + HORIZONTAL_ELLIPSIS + + # Style before and after truncated text + line = before_style + 'long' + after_style + max_width = 3 + truncated = cu.truncate_line(line, max_width) + assert truncated == before_style + 'lo' + HORIZONTAL_ELLIPSIS + after_style + + # Style only after truncated text + line = 'long' + after_style + max_width = 3 + truncated = cu.truncate_line(line, max_width) + assert truncated == 'lo' + HORIZONTAL_ELLIPSIS + after_style def test_align_text_fill_char_is_tab(): text = 'foo' @@ -337,6 +368,15 @@ def test_align_text_fill_char_is_tab(): aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) assert aligned == text + ' ' +def test_align_text_fill_char_has_color(): + from cmd2 import ansi + + text = 'foo' + fill_char = ansi.fg.bright_yellow + '-' + ansi.fg.reset + width = 5 + aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) + assert aligned == text + fill_char * 2 + def test_align_text_width_is_too_small(): text = 'foo' fill_char = '-' @@ -351,7 +391,7 @@ def test_align_text_fill_char_is_too_long(): with pytest.raises(TypeError): cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width) -def test_align_text_fill_char_is_unprintable(): +def test_align_text_fill_char_is_newline(): text = 'foo' fill_char = '\n' width = 5 @@ -384,7 +424,7 @@ def test_align_text_wider_than_width_truncate(): fill_char = '-' width = 8 aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, truncate=True) - assert aligned == 'long te\N{HORIZONTAL ELLIPSIS}' + assert aligned == 'long te' + HORIZONTAL_ELLIPSIS def test_align_text_wider_than_width_truncate_add_fill(): """Test when truncation results in a string which is shorter than width and align_text adds filler""" @@ -392,7 +432,7 @@ def test_align_text_wider_than_width_truncate_add_fill(): fill_char = '-' width = 3 aligned = cu.align_text(text, cu.TextAlignment.LEFT, fill_char=fill_char, width=width, truncate=True) - assert aligned == '1\N{HORIZONTAL ELLIPSIS}-' + assert aligned == '1' + HORIZONTAL_ELLIPSIS + fill_char def test_align_text_has_unprintable(): text = 'foo\x02' |