summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2020-01-09 22:45:53 -0500
committerTodd Leonhardt <todd.leonhardt@gmail.com>2020-01-09 22:45:53 -0500
commit591bd29cb4a3bcb9b1f40ffc1f30429c6501ebdb (patch)
tree46c1e3af762c9cb222c6ae90bf0446d6eca4b388
parent10b844809e3a9500274dc4af4e780708975ba905 (diff)
parentd4556962799e68ea4d54ff86186428d17edcaef9 (diff)
downloadcmd2-git-591bd29cb4a3bcb9b1f40ffc1f30429c6501ebdb.tar.gz
Merge branch 'master' into generating_output_docs
# Conflicts: # docs/features/generating_output.rst # docs/features/settings.rst
-rw-r--r--.coveragerc2
-rw-r--r--CHANGELOG.md31
-rw-r--r--CONTRIBUTING.md1
-rw-r--r--LICENSE2
-rwxr-xr-xREADME.md2
-rw-r--r--cmd2/ansi.py93
-rw-r--r--cmd2/argparse_completer.py6
-rw-r--r--cmd2/argparse_custom.py4
-rw-r--r--cmd2/cmd2.py90
-rw-r--r--cmd2/history.py4
-rwxr-xr-xcmd2/parsing.py9
-rw-r--r--cmd2/rl_utils.py2
-rw-r--r--cmd2/transcript.py8
-rw-r--r--cmd2/utils.py165
-rw-r--r--docs/api/utility_functions.rst8
-rw-r--r--docs/conf.py2
-rw-r--r--docs/copyright.rst5
-rw-r--r--docs/features/generating_output.rst34
-rw-r--r--docs/features/settings.rst6
-rwxr-xr-xexamples/colors.py14
-rwxr-xr-xexamples/dynamic_commands.py26
-rwxr-xr-xexamples/plumbum_colors.py14
-rw-r--r--examples/transcripts/exampleSession.txt2
-rw-r--r--examples/transcripts/transcript_regex.txt2
-rwxr-xr-xsetup.py6
-rw-r--r--tests/conftest.py13
-rw-r--r--tests/scripts/postcmds.txt2
-rw-r--r--tests/scripts/precmds.txt2
-rw-r--r--tests/test_ansi.py25
-rw-r--r--tests/test_argparse_completer.py4
-rwxr-xr-xtests/test_cmd2.py139
-rwxr-xr-xtests/test_history.py28
-rw-r--r--tests/test_run_pyscript.py27
-rw-r--r--tests/test_utils.py198
-rw-r--r--tests/transcripts/regex_set.txt2
35 files changed, 673 insertions, 305 deletions
diff --git a/.coveragerc b/.coveragerc
index 307e3a9a..96bf5d72 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -20,5 +20,5 @@ precision = 1
[html]
-# (string, default “htmlcov”): where to write the HTML report files.
+# (string, default "htmlcov"): where to write the HTML report files.
directory = htmlcov
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c423836..0523ee5c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,27 @@
-## 0.9.22 (TBD, 2019)
+## 0.9.23 (January 9, 2020)
+* Bug Fixes
+ * Fixed bug where startup script containing a single quote in its file name was incorrectly quoted
+ * Added missing implicit dependency on `setuptools` due to build with `setuptools_scm`
+* Enhancements
+ * Added dim text style support via `style()` function and `ansi.INTENSITY_DIM` setting.
+* Breaking changes
+ * Renamed the following `ansi` members for accuracy in what types of ANSI escape sequences are handled
+ * `ansi.allow_ansi` -> `ansi.allow_style`
+ * `ansi.ansi_safe_wcswidth()` -> `ansi.style_aware_wcswidth()`
+ * `ansi.ansi_aware_write()` -> `ansi.style_aware_write()`
+ * Renamed the following `ansi` members for clarification
+ * `ansi.BRIGHT` -> `ansi.INTENSITY_BRIGHT`
+ * `ansi.NORMAL` -> `ansi.INTENSITY_NORMAL`
+
+## 0.9.22 (December 9, 2019)
* Bug Fixes
* Fixed bug where a redefined `ansi.style_error` was not being used in all `cmd2` files
-* Other
- * Removed `bold=True` from `ansi.style_success` because it was difficult for red-greed colorblind users to
- distinguish that color from the `ansi.style_warning` color in certain terminals.
-
+* Enhancements
+ * Enabled line buffering when redirecting output to a file
+ * Added `align_left()`, `align_center()`, and `align_right()` to utils.py. All 3 of these functions support
+ ANSI escape sequences and characters with display widths greater than 1. They wrap `align_text()` which
+ is also in utils.py.
+
## 0.9.21 (November 26, 2019)
* Bug Fixes
* Fixed bug where pipe processes were not being stopped by Ctrl-C
@@ -250,11 +267,13 @@
* Removed *** from beginning of error messages printed by `do_help()` and `default()`
* Significantly refactored ``cmd.Cmd`` class so that all class attributes got converted to instance attributes, also:
* Added ``allow_redirection``, ``terminators``, ``multiline_commands``, and ``shortcuts`` as optional arguments
- to ``cmd.Cmd.__init__()`
+ to ``cmd2.Cmd.__init__()``
* A few instance attributes were moved inside ``StatementParser`` and properties were created for accessing them
* ``self.pipe_proc`` is now called ``self.cur_pipe_proc_reader`` and is a ``ProcReader`` class.
* Shell commands and commands being piped to while in a *pyscript* will function as if their output is going
to a pipe and not a tty. This was necessary to be able to capture their output.
+ * Removed `reserved_words` class attribute due to lack of use
+ * Removed `keywords` instance attribute due to lack of use
## 0.9.11 (March 13, 2019)
* Bug Fixes
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6055fa7d..96c2d312 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -51,6 +51,7 @@ The tables below list all prerequisites along with the minimum required version
| [attrs](https://github.com/python-attrs/attrs) | `16.3` |
| [colorama](https://github.com/tartley/colorama) | `0.3.7` |
| [pyperclip](https://github.com/asweigart/pyperclip) | `1.6` |
+| [setuptools](https://pypi.org/project/setuptools/) | `34.4` |
| [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.1.7` |
diff --git a/LICENSE b/LICENSE
index 7f916117..8c187203 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2008-2019 Catherine Devlin and others
+Copyright (c) 2008-2020 Catherine Devlin and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 7b68965f..f90875de 100755
--- a/README.md
+++ b/README.md
@@ -317,7 +317,7 @@ example/transcript_regex.txt:
# The regex for editor will match whatever program you use.
# regexes on prompts just make the trailing space obvious
(Cmd) set
-allow_ansi: Terminal
+allow_style: Terminal
continuation_prompt: >/ /
debug: False
echo: False
diff --git a/cmd2/ansi.py b/cmd2/ansi.py
index 86161ac2..78e0df81 100644
--- a/cmd2/ansi.py
+++ b/cmd2/ansi.py
@@ -1,5 +1,8 @@
# coding=utf-8
-"""Support for ANSI escape sequences which are used for things like applying style to text"""
+"""
+Support for ANSI escape sequences which are used for things like applying style to text,
+setting the window title, and asynchronous alerts.
+ """
import functools
import re
from typing import Any, IO
@@ -11,16 +14,16 @@ from wcwidth import wcswidth
# On Windows, filter ANSI escape codes out of text sent to stdout/stderr, and replace them with equivalent Win32 calls
colorama.init(strip=False)
-# Values for allow_ansi setting
-ANSI_NEVER = 'Never'
-ANSI_TERMINAL = 'Terminal'
-ANSI_ALWAYS = 'Always'
+# Values for allow_style setting
+STYLE_NEVER = 'Never'
+STYLE_TERMINAL = 'Terminal'
+STYLE_ALWAYS = 'Always'
-# Controls when ANSI escape sequences are allowed in output
-allow_ansi = ANSI_TERMINAL
+# Controls when ANSI style style sequences are allowed in output
+allow_style = STYLE_TERMINAL
-# Regular expression to match ANSI escape sequences
-ANSI_ESCAPE_RE = re.compile(r'\x1b[^m]*m')
+# Regular expression to match ANSI style sequences (including 8-bit and 24-bit colors)
+ANSI_STYLE_RE = re.compile(r'\x1b\[[^m]*m')
# Foreground color presets
FG_COLORS = {
@@ -68,51 +71,51 @@ FG_RESET = FG_COLORS['reset']
BG_RESET = BG_COLORS['reset']
RESET_ALL = Style.RESET_ALL
-BRIGHT = Style.BRIGHT
-NORMAL = Style.NORMAL
+# Text intensities
+INTENSITY_BRIGHT = Style.BRIGHT
+INTENSITY_DIM = Style.DIM
+INTENSITY_NORMAL = Style.NORMAL
-# ANSI escape sequences not provided by colorama
+# ANSI style sequences not provided by colorama
UNDERLINE_ENABLE = colorama.ansi.code_to_chars(4)
UNDERLINE_DISABLE = colorama.ansi.code_to_chars(24)
-def strip_ansi(text: str) -> str:
+def strip_style(text: str) -> str:
"""
- Strip ANSI escape sequences from a string.
+ Strip ANSI style sequences from a string.
- :param text: string which may contain ANSI escape sequences
- :return: the same string with any ANSI escape sequences removed
+ :param text: string which may contain ANSI style sequences
+ :return: the same string with any ANSI style sequences removed
"""
- return ANSI_ESCAPE_RE.sub('', text)
+ return ANSI_STYLE_RE.sub('', text)
-def ansi_safe_wcswidth(text: str) -> int:
+def style_aware_wcswidth(text: str) -> int:
"""
- Wrap wcswidth to make it compatible with strings that contains ANSI escape sequences
-
+ Wrap wcswidth to make it compatible with strings that contains ANSI style 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))
+ # Strip ANSI style sequences since they cause wcswidth to return -1
+ return wcswidth(strip_style(text))
-def ansi_aware_write(fileobj: IO, msg: str) -> None:
+def style_aware_write(fileobj: IO, msg: str) -> None:
"""
- Write a string to a fileobject and strip its ANSI escape sequences if required by allow_ansi setting
-
+ Write a string to a fileobject and strip its ANSI style sequences if required by allow_style setting
:param fileobj: the file object being written to
:param msg: the string being written
"""
- if allow_ansi.lower() == ANSI_NEVER.lower() or \
- (allow_ansi.lower() == ANSI_TERMINAL.lower() and not fileobj.isatty()):
- msg = strip_ansi(msg)
+ if allow_style.lower() == STYLE_NEVER.lower() or \
+ (allow_style.lower() == STYLE_TERMINAL.lower() and not fileobj.isatty()):
+ msg = strip_style(msg)
fileobj.write(msg)
def fg_lookup(fg_name: str) -> str:
- """Look up ANSI escape codes based on foreground color name.
-
+ """
+ Look up ANSI escape codes based on foreground color name.
: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
@@ -125,8 +128,8 @@ def fg_lookup(fg_name: str) -> str:
def bg_lookup(bg_name: str) -> str:
- """Look up ANSI escape codes based on background color name.
-
+ """
+ Look up ANSI escape codes based on background color name.
: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
@@ -138,16 +141,18 @@ def bg_lookup(bg_name: str) -> str:
return ansi_escape
-def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False, underline: bool = False) -> str:
- """Styles a string with ANSI colors and/or styles and returns the new string.
-
+def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False,
+ dim: bool = False, underline: bool = False) -> str:
+ """
+ Apply ANSI colors and/or styles to a string and return it.
The styling is self contained which means that at the end of the string reset code(s) are issued
to undo whatever styling was done at the beginning.
:param text: Any object compatible with str.format()
:param fg: foreground color. Relies on `fg_lookup()` to retrieve ANSI escape based on name. Defaults to no color.
:param bg: background color. Relies on `bg_lookup()` to retrieve ANSI escape based on name. Defaults to no color.
- :param bold: apply the bold style if True. Defaults to False.
+ :param bold: apply the bold style if True. Can be combined with dim. Defaults to False.
+ :param dim: apply the dim style if True. Can be combined with bold. Defaults to False.
:param underline: apply the underline style if True. Defaults to False.
:return: the stylized string
"""
@@ -170,14 +175,18 @@ def style(text: Any, *, fg: str = '', bg: str = '', bold: bool = False, underlin
removals.append(BG_RESET)
if bold:
- additions.append(Style.BRIGHT)
- removals.append(Style.NORMAL)
+ additions.append(INTENSITY_BRIGHT)
+ removals.append(INTENSITY_NORMAL)
+
+ if dim:
+ additions.append(INTENSITY_DIM)
+ removals.append(INTENSITY_NORMAL)
if underline:
additions.append(UNDERLINE_ENABLE)
removals.append(UNDERLINE_DISABLE)
- # Combine the ANSI escape sequences with the text
+ # Combine the ANSI style sequences with the text
return "".join(additions) + text + "".join(removals)
@@ -212,14 +221,14 @@ def async_alert_str(*, terminal_columns: int, prompt: str, line: str, cursor_off
# That will be included in the input lines calculations since that is where the cursor is.
num_prompt_terminal_lines = 0
for line in prompt_lines[:-1]:
- line_width = ansi_safe_wcswidth(line)
+ line_width = style_aware_wcswidth(line)
num_prompt_terminal_lines += int(line_width / terminal_columns) + 1
# Now calculate how many terminal lines are take up by the input
last_prompt_line = prompt_lines[-1]
- last_prompt_line_width = ansi_safe_wcswidth(last_prompt_line)
+ last_prompt_line_width = style_aware_wcswidth(last_prompt_line)
- input_width = last_prompt_line_width + ansi_safe_wcswidth(line)
+ input_width = last_prompt_line_width + style_aware_wcswidth(line)
num_input_terminal_lines = int(input_width / terminal_columns) + 1
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index a2690dd0..23fd930e 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -444,11 +444,11 @@ class AutoCompleter(object):
completions.sort(key=self._cmd2_app.default_sort_key)
self._cmd2_app.matches_sorted = True
- token_width = ansi.ansi_safe_wcswidth(action.dest)
+ token_width = ansi.style_aware_wcswidth(action.dest)
completions_with_desc = []
for item in completions:
- item_width = ansi.ansi_safe_wcswidth(item)
+ item_width = ansi.style_aware_wcswidth(item)
if item_width > token_width:
token_width = item_width
@@ -585,7 +585,7 @@ class AutoCompleter(object):
def _print_message(msg: str) -> None:
"""Print a message instead of tab completions and redraw the prompt and input line"""
import sys
- ansi.ansi_aware_write(sys.stdout, msg + '\n')
+ ansi.style_aware_write(sys.stdout, msg + '\n')
rl_force_redisplay()
def _print_arg_hint(self, arg_action: argparse.Action) -> None:
diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py
index 51c3375e..f735498d 100644
--- a/cmd2/argparse_custom.py
+++ b/cmd2/argparse_custom.py
@@ -802,11 +802,11 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
return formatter.format_help() + '\n'
def _print_message(self, message, file=None):
- # Override _print_message to use ansi_aware_write() since we use ANSI escape characters to support color
+ # Override _print_message to use style_aware_write() since we use ANSI escape characters to support color
if message:
if file is None:
file = sys.stderr
- ansi.ansi_aware_write(file, message)
+ ansi.style_aware_write(file, message)
# The default ArgumentParser class for a cmd2 app
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index b025043e..ec8d67b2 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -209,11 +209,11 @@ class Cmd(cmd.Cmd):
# To make an attribute settable with the "do_set" command, add it to this ...
self.settable = \
{
- # allow_ansi is a special case in which it's an application-wide setting defined in ansi.py
- 'allow_ansi': ('Allow ANSI escape sequences in output '
- '(valid values: {}, {}, {})'.format(ansi.ANSI_TERMINAL,
- ansi.ANSI_ALWAYS,
- ansi.ANSI_NEVER)),
+ # allow_style is a special case in which it's an application-wide setting defined in ansi.py
+ 'allow_style': ('Allow ANSI text style sequences in output '
+ '(valid values: {}, {}, {})'.format(ansi.STYLE_TERMINAL,
+ ansi.STYLE_ALWAYS,
+ ansi.STYLE_NEVER)),
'continuation_prompt': 'On 2nd+ line of input',
'debug': 'Show full error stack on error',
'echo': 'Echo command issued into output',
@@ -309,7 +309,7 @@ class Cmd(cmd.Cmd):
if startup_script:
startup_script = os.path.abspath(os.path.expanduser(startup_script))
if os.path.exists(startup_script):
- self._startup_commands.append("run_script '{}'".format(startup_script))
+ self._startup_commands.append("run_script {}".format(utils.quote_string(startup_script)))
# Transcript files to run instead of interactive command loop
self._transcript_files = None
@@ -375,7 +375,7 @@ class Cmd(cmd.Cmd):
else:
# Here is the meaning of the various flags we are using with the less command:
# -S causes lines longer than the screen width to be chopped (truncated) rather than wrapped
- # -R causes ANSI "color" escape sequences to be output in raw form (i.e. colors are displayed)
+ # -R causes ANSI "style" escape sequences to be output in raw form (i.e. colors are displayed)
# -X disables sending the termcap initialization and deinitialization strings to the terminal
# -F causes less to automatically exit if the entire file can be displayed on the first screen
self.pager = 'less -RXF'
@@ -404,23 +404,23 @@ class Cmd(cmd.Cmd):
# ----- Methods related to presenting output to the user -----
@property
- def allow_ansi(self) -> str:
- """Read-only property needed to support do_set when it reads allow_ansi"""
- return ansi.allow_ansi
+ def allow_style(self) -> str:
+ """Read-only property needed to support do_set when it reads allow_style"""
+ return ansi.allow_style
- @allow_ansi.setter
- def allow_ansi(self, new_val: str) -> None:
- """Setter property needed to support do_set when it updates allow_ansi"""
+ @allow_style.setter
+ def allow_style(self, new_val: str) -> None:
+ """Setter property needed to support do_set when it updates allow_style"""
new_val = new_val.lower()
- if new_val == ansi.ANSI_TERMINAL.lower():
- ansi.allow_ansi = ansi.ANSI_TERMINAL
- elif new_val == ansi.ANSI_ALWAYS.lower():
- ansi.allow_ansi = ansi.ANSI_ALWAYS
- elif new_val == ansi.ANSI_NEVER.lower():
- ansi.allow_ansi = ansi.ANSI_NEVER
+ if new_val == ansi.STYLE_TERMINAL.lower():
+ ansi.allow_style = ansi.STYLE_TERMINAL
+ elif new_val == ansi.STYLE_ALWAYS.lower():
+ ansi.allow_style = ansi.STYLE_ALWAYS
+ elif new_val == ansi.STYLE_NEVER.lower():
+ ansi.allow_style = ansi.STYLE_NEVER
else:
- self.perror('Invalid value: {} (valid values: {}, {}, {})'.format(new_val, ansi.ANSI_TERMINAL,
- ansi.ANSI_ALWAYS, ansi.ANSI_NEVER))
+ self.perror('Invalid value: {} (valid values: {}, {}, {})'.format(new_val, ansi.STYLE_TERMINAL,
+ ansi.STYLE_ALWAYS, ansi.STYLE_NEVER))
@property
def broken_pipe_warning(self) -> str:
@@ -446,14 +446,14 @@ class Cmd(cmd.Cmd):
@property
def visible_prompt(self) -> str:
- """Read-only property to get the visible prompt with any ANSI escape codes stripped.
+ """Read-only property to get the visible prompt with any ANSI style escape codes stripped.
Used by transcript testing to make it easier and more reliable when users are doing things like coloring the
prompt using ANSI color codes.
:return: prompt stripped of any ANSI escape codes
"""
- return ansi.strip_ansi(self.prompt)
+ return ansi.strip_style(self.prompt)
def poutput(self, msg: Any = '', *, end: str = '\n') -> None:
"""Print message to self.stdout and appends a newline by default
@@ -466,7 +466,7 @@ class Cmd(cmd.Cmd):
:param end: string appended after the end of the message, default a newline
"""
try:
- ansi.ansi_aware_write(self.stdout, "{}{}".format(msg, end))
+ ansi.style_aware_write(self.stdout, "{}{}".format(msg, end))
except BrokenPipeError:
# This occurs if a command's output is being piped to another
# process and that process closes before the command is
@@ -489,7 +489,7 @@ class Cmd(cmd.Cmd):
final_msg = ansi.style_error(msg)
else:
final_msg = "{}".format(msg)
- ansi.ansi_aware_write(sys.stderr, final_msg + end)
+ ansi.style_aware_write(sys.stderr, final_msg + end)
def pwarning(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> None:
"""Wraps perror, but applies ansi.style_warning by default
@@ -578,8 +578,8 @@ class Cmd(cmd.Cmd):
# Don't attempt to use a pager that can block if redirecting or running a script (either text or Python)
# Also only attempt to use a pager if actually running in a real fully functional terminal
if functional_terminal and not self._redirecting and not self.in_pyscript() and not self.in_script():
- if ansi.allow_ansi.lower() == ansi.ANSI_NEVER.lower():
- msg_str = ansi.strip_ansi(msg_str)
+ if ansi.allow_style.lower() == ansi.STYLE_NEVER.lower():
+ msg_str = ansi.strip_style(msg_str)
msg_str += end
pager = self.pager
@@ -1123,7 +1123,7 @@ class Cmd(cmd.Cmd):
longest_match_length = 0
for cur_match in matches_to_display:
- cur_length = ansi.ansi_safe_wcswidth(cur_match)
+ cur_length = ansi.style_aware_wcswidth(cur_match)
if cur_length > longest_match_length:
longest_match_length = cur_length
else:
@@ -1901,22 +1901,25 @@ class Cmd(cmd.Cmd):
self.perror("Cannot redirect to paste buffer; missing 'pyperclip' and/or pyperclip dependencies")
redir_error = True
+ # Redirecting to a file
elif statement.output_to:
- # going to a file
- mode = 'w'
- # statement.output can only contain
- # REDIRECTION_APPEND or REDIRECTION_OUTPUT
+ # statement.output can only contain REDIRECTION_APPEND or REDIRECTION_OUTPUT
if statement.output == constants.REDIRECTION_APPEND:
mode = 'a'
+ else:
+ mode = 'w'
+
try:
- new_stdout = open(utils.strip_quotes(statement.output_to), mode)
+ # Use line buffering
+ new_stdout = open(utils.strip_quotes(statement.output_to), mode=mode, buffering=1)
saved_state.redirecting = True
sys.stdout = self.stdout = new_stdout
except OSError as ex:
self.pexcept('Failed to redirect because - {}'.format(ex))
redir_error = True
+
+ # Redirecting to a paste buffer
else:
- # going to a paste buffer
new_stdout = tempfile.TemporaryFile(mode="w+")
saved_state.redirecting = True
sys.stdout = self.stdout = new_stdout
@@ -2677,7 +2680,7 @@ class Cmd(cmd.Cmd):
widest = 0
# measure the commands
for command in cmds:
- width = ansi.ansi_safe_wcswidth(command)
+ width = ansi.style_aware_wcswidth(command)
if width > widest:
widest = width
# add a 4-space pad
@@ -2742,6 +2745,7 @@ class Cmd(cmd.Cmd):
self.stdout.write("\n")
shortcuts_parser = DEFAULT_ARGUMENT_PARSER(description="List available shortcuts")
+
@with_argparser(shortcuts_parser)
def do_shortcuts(self, _: argparse.Namespace) -> None:
"""List available shortcuts"""
@@ -2751,6 +2755,7 @@ class Cmd(cmd.Cmd):
self.poutput("Shortcuts for other commands:\n{}".format(result))
eof_parser = DEFAULT_ARGUMENT_PARSER(description="Called when <Ctrl>-D is pressed", epilog=INTERNAL_COMMAND_EPILOG)
+
@with_argparser(eof_parser)
def do_eof(self, _: argparse.Namespace) -> bool:
"""Called when <Ctrl>-D is pressed"""
@@ -2758,6 +2763,7 @@ class Cmd(cmd.Cmd):
return True
quit_parser = DEFAULT_ARGUMENT_PARSER(description="Exit this application")
+
@with_argparser(quit_parser)
def do_quit(self, _: argparse.Namespace) -> bool:
"""Exit this application"""
@@ -3239,6 +3245,7 @@ class Cmd(cmd.Cmd):
# Only include the do_ipy() method if IPython is available on the system
if ipython_available: # pragma: no cover
ipython_parser = DEFAULT_ARGUMENT_PARSER(description="Enter an interactive IPython shell")
+
@with_argparser(ipython_parser)
def do_ipy(self, _: argparse.Namespace) -> None:
"""Enter an interactive IPython shell"""
@@ -3247,6 +3254,7 @@ class Cmd(cmd.Cmd):
'Run Python code from external files with: run filename.py\n')
exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0])
+ # noinspection PyUnusedLocal
def load_ipy(cmd2_app: Cmd, py_bridge: PyBridge):
"""
Embed an IPython shell in an environment that is restricted to only the variables in this function
@@ -3377,7 +3385,7 @@ class Cmd(cmd.Cmd):
with os.fdopen(fd, 'w') as fobj:
for command in history:
if command.statement.multiline_command:
- fobj.write('{}\n'.format(command.expanded.rstrip()))
+ fobj.write('{}\n'.format(command.expanded))
else:
fobj.write('{}\n'.format(command.raw))
try:
@@ -3391,7 +3399,7 @@ class Cmd(cmd.Cmd):
with open(os.path.expanduser(args.output_file), 'w') as fobj:
for item in history:
if item.statement.multiline_command:
- fobj.write('{}\n'.format(item.expanded.rstrip()))
+ fobj.write('{}\n'.format(item.expanded))
else:
fobj.write('{}\n'.format(item.raw))
plural = 's' if len(history) > 1 else ''
@@ -3739,7 +3747,7 @@ class Cmd(cmd.Cmd):
verinfo = ".".join(map(str, sys.version_info[:3]))
num_transcripts = len(transcripts_expanded)
plural = '' if len(transcripts_expanded) == 1 else 's'
- self.poutput(ansi.style(utils.center_text('cmd2 transcript test', pad='='), bold=True))
+ self.poutput(ansi.style(utils.align_center(' cmd2 transcript test ', fill_char='='), bold=True))
self.poutput('platform {} -- Python {}, cmd2-{}, readline-{}'.format(sys.platform, verinfo, cmd2.__version__,
rl_type))
self.poutput('cwd: {}'.format(os.getcwd()))
@@ -3756,9 +3764,9 @@ class Cmd(cmd.Cmd):
test_results = runner.run(testcase)
execution_time = time.time() - start_time
if test_results.wasSuccessful():
- ansi.ansi_aware_write(sys.stderr, stream.read())
- finish_msg = '{0} transcript{1} passed in {2:.3f} seconds'.format(num_transcripts, plural, execution_time)
- finish_msg = ansi.style_success(utils.center_text(finish_msg, pad='='))
+ ansi.style_aware_write(sys.stderr, stream.read())
+ finish_msg = ' {0} transcript{1} passed in {2:.3f} seconds '.format(num_transcripts, plural, execution_time)
+ finish_msg = ansi.style_success(utils.align_center(finish_msg, fill_char='='))
self.poutput(finish_msg)
else:
# Strip off the initial traceback which isn't particularly useful for end users
diff --git a/cmd2/history.py b/cmd2/history.py
index 576ac37d..3b18fbeb 100644
--- a/cmd2/history.py
+++ b/cmd2/history.py
@@ -45,14 +45,14 @@ class HistoryItem():
"""
if verbose:
raw = self.raw.rstrip()
- expanded = self.expanded.rstrip()
+ expanded = self.expanded
ret_str = self._listformat.format(self.idx, raw)
if raw != expanded:
ret_str += '\n' + self._ex_listformat.format(self.idx, expanded)
else:
if expanded:
- ret_str = self.expanded.rstrip()
+ ret_str = self.expanded
else:
ret_str = self.raw.rstrip()
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index 4e690b0b..cef0b088 100755
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -13,9 +13,10 @@ from . import utils
def shlex_split(str_to_split: str) -> List[str]:
- """A wrapper around shlex.split() that uses cmd2's preferred arguments.
+ """
+ A wrapper around shlex.split() that uses cmd2's preferred arguments.
+ This allows other classes to easily call split() the same way StatementParser does.
- This allows other classes to easily call split() the same way StatementParser does
:param str_to_split: the string being split
:return: A list of tokens
"""
@@ -26,8 +27,8 @@ def shlex_split(str_to_split: str) -> List[str]:
class MacroArg:
"""
Information used to replace or unescape arguments in a macro value when the macro is resolved
- Normal argument syntax : {5}
- Escaped argument syntax: {{5}}
+ Normal argument syntax: {5}
+ Escaped argument syntax: {{5}}
"""
# The starting index of this argument in the macro value
start_index = attr.ib(validator=attr.validators.instance_of(int))
diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py
index 9a23cbcd..4df733db 100644
--- a/cmd2/rl_utils.py
+++ b/cmd2/rl_utils.py
@@ -193,7 +193,7 @@ def rl_set_prompt(prompt: str) -> None: # pragma: no cover
def rl_make_safe_prompt(prompt: str) -> str: # pragma: no cover
- """Overcome bug in GNU Readline in relation to calculation of prompt length in presence of ANSI escape codes.
+ """Overcome bug in GNU Readline in relation to calculation of prompt length in presence of ANSI escape codes
:param prompt: original prompt
:return: prompt safe to pass to GNU Readline
diff --git a/cmd2/transcript.py b/cmd2/transcript.py
index 25a79310..940c97db 100644
--- a/cmd2/transcript.py
+++ b/cmd2/transcript.py
@@ -56,13 +56,13 @@ class Cmd2TestCase(unittest.TestCase):
def _test_transcript(self, fname: str, transcript):
line_num = 0
finished = False
- line = ansi.strip_ansi(next(transcript))
+ line = ansi.strip_style(next(transcript))
line_num += 1
while not finished:
# Scroll forward to where actual commands begin
while not line.startswith(self.cmdapp.visible_prompt):
try:
- line = ansi.strip_ansi(next(transcript))
+ line = ansi.strip_style(next(transcript))
except StopIteration:
finished = True
break
@@ -89,7 +89,7 @@ class Cmd2TestCase(unittest.TestCase):
result = self.cmdapp.stdout.read()
stop_msg = 'Command indicated application should quit, but more commands in transcript'
# Read the expected result from transcript
- if ansi.strip_ansi(line).startswith(self.cmdapp.visible_prompt):
+ if ansi.strip_style(line).startswith(self.cmdapp.visible_prompt):
message = '\nFile {}, line {}\nCommand was:\n{}\nExpected: (nothing)\nGot:\n{}\n'.format(
fname, line_num, command, result)
self.assertTrue(not (result.strip()), message)
@@ -97,7 +97,7 @@ class Cmd2TestCase(unittest.TestCase):
self.assertFalse(stop, stop_msg)
continue
expected = []
- while not ansi.strip_ansi(line).startswith(self.cmdapp.visible_prompt):
+ while not ansi.strip_style(line).startswith(self.cmdapp.visible_prompt):
expected.append(line)
try:
line = next(transcript)
diff --git a/cmd2/utils.py b/cmd2/utils.py
index a1a0d377..ffbe5a64 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -5,11 +5,11 @@ import collections
import glob
import os
import re
-import shutil
import subprocess
import sys
import threading
import unicodedata
+from enum import Enum
from typing import Any, Iterable, List, Optional, TextIO, Union
from . import constants
@@ -363,21 +363,6 @@ def get_exes_in_path(starts_with: str) -> List[str]:
return list(exes_set)
-def center_text(msg: str, *, pad: str = ' ') -> str:
- """Centers text horizontally for display within the current terminal, optionally padding both sides.
-
- :param msg: message to display in the center
- :param pad: if provided, the first character will be used to pad both sides of the message
- :return: centered message, optionally padded on both sides with pad_char
- """
- term_width = shutil.get_terminal_size().columns
- surrounded_msg = ' {} '.format(msg)
- if not pad:
- pad = ' '
- fill_char = pad[:1]
- return surrounded_msg.center(term_width, fill_char)
-
-
class StdSim(object):
"""
Class to simulate behavior of sys.stdout or sys.stderr.
@@ -644,3 +629,151 @@ def basic_complete(text: str, line: str, begidx: int, endidx: int, match_against
:return: a list of possible tab completions
"""
return [cur_match for cur_match in match_against if cur_match.startswith(text)]
+
+
+class TextAlignment(Enum):
+ LEFT = 1
+ CENTER = 2
+ RIGHT = 3
+
+
+def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ',
+ width: Optional[int] = None, tab_width: int = 4) -> 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.
+
+ There are convenience wrappers around this function: align_left(), align_center(), and align_right()
+
+ :param text: text to align (can contain multiple lines)
+ :param alignment: how to align the text
+ :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character)
+ :param width: display width of the aligned text. Defaults to width of the terminal.
+ :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
+ be converted to a space.
+ :return: aligned text
+ :raises: TypeError if fill_char is more than one character
+ ValueError if text or fill_char contains an unprintable character
+ """
+ import io
+ import shutil
+
+ from . import ansi
+
+ # Handle tabs
+ text = text.replace('\t', ' ' * tab_width)
+ if fill_char == '\t':
+ fill_char = ' '
+
+ if len(fill_char) != 1:
+ raise TypeError("Fill character must be exactly one character long")
+
+ fill_char_width = ansi.style_aware_wcswidth(fill_char)
+ if fill_char_width == -1:
+ raise (ValueError("Fill character is an unprintable character"))
+
+ if text:
+ lines = text.splitlines()
+ else:
+ lines = ['']
+
+ if width is None:
+ width = shutil.get_terminal_size().columns
+
+ text_buf = io.StringIO()
+
+ for index, line in enumerate(lines):
+ if index > 0:
+ text_buf.write('\n')
+
+ # Use style_aware_wcswidth to support characters with display widths
+ # greater than 1 as well as ANSI style sequences
+ line_width = ansi.style_aware_wcswidth(line)
+ if line_width == -1:
+ raise(ValueError("Text to align contains an unprintable character"))
+
+ # Check if line is wider than the desired final width
+ if width <= line_width:
+ text_buf.write(line)
+ continue
+
+ # Calculate how wide each side of filling needs to be
+ total_fill_width = width - line_width
+
+ if alignment == TextAlignment.LEFT:
+ left_fill_width = 0
+ right_fill_width = total_fill_width
+ elif alignment == TextAlignment.CENTER:
+ left_fill_width = total_fill_width // 2
+ right_fill_width = total_fill_width - left_fill_width
+ else:
+ left_fill_width = total_fill_width
+ right_fill_width = 0
+
+ # Determine how many fill characters are needed to cover the width
+ left_fill = (left_fill_width // fill_char_width) * fill_char
+ right_fill = (right_fill_width // fill_char_width) * fill_char
+
+ # In cases where the fill character display width didn't divide evenly into
+ # the gaps being filled, pad the remainder with spaces.
+ left_fill += ' ' * (left_fill_width - ansi.style_aware_wcswidth(left_fill))
+ right_fill += ' ' * (right_fill_width - ansi.style_aware_wcswidth(right_fill))
+
+ text_buf.write(left_fill + line + right_fill)
+
+ return text_buf.getvalue()
+
+
+def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> 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.
+
+ :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)
+ :param width: display width of the aligned text. Defaults to width of the terminal.
+ :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
+ be converted to a space.
+ :return: left-aligned text
+ :raises: TypeError if fill_char is more than one character
+ ValueError if text or fill_char contains an unprintable character
+ """
+ return align_text(text, TextAlignment.LEFT, fill_char=fill_char, width=width, tab_width=tab_width)
+
+
+def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> 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.
+
+ :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)
+ :param width: display width of the aligned text. Defaults to width of the terminal.
+ :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
+ be converted to a space.
+ :return: centered text
+ :raises: TypeError if fill_char is more than one character
+ ValueError if text or fill_char contains an unprintable character
+ """
+ return align_text(text, TextAlignment.CENTER, fill_char=fill_char, width=width, tab_width=tab_width)
+
+
+def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> 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.
+
+ :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)
+ :param width: display width of the aligned text. Defaults to width of the terminal.
+ :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will
+ be converted to a space.
+ :return: right-aligned text
+ :raises: TypeError if fill_char is more than one character
+ ValueError if text or fill_char contains an unprintable character
+ """
+ return align_text(text, TextAlignment.RIGHT, fill_char=fill_char, width=width, tab_width=tab_width)
diff --git a/docs/api/utility_functions.rst b/docs/api/utility_functions.rst
index 86fb656c..e083cafe 100644
--- a/docs/api/utility_functions.rst
+++ b/docs/api/utility_functions.rst
@@ -9,7 +9,13 @@ Utility Functions
.. autofunction:: cmd2.decorators.categorize
-.. autofunction:: cmd2.utils.center_text
+.. autofunction:: cmd2.utils.align_text
+
+.. autofunction:: cmd2.utils.align_left
+
+.. autofunction:: cmd2.utils.align_center
+
+.. autofunction:: cmd2.utils.align_right
.. autofunction:: cmd2.utils.strip_quotes
diff --git a/docs/conf.py b/docs/conf.py
index 5463d0bb..7a8da9d1 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -56,7 +56,7 @@ master_doc = 'index'
# General information about the project.
project = 'cmd2'
-copyright = '2010-2019, cmd2 contributors'
+copyright = '2010-2020, cmd2 contributors'
author = 'cmd2 contributors'
# The version info for the project you're documenting, acts as replacement for
diff --git a/docs/copyright.rst b/docs/copyright.rst
index b7183e62..30cf255c 100644
--- a/docs/copyright.rst
+++ b/docs/copyright.rst
@@ -1,7 +1,6 @@
Copyright
=========
-The ``cmd2`` documentation is Copyright 2010-2019 by the `cmd2 contributors
+The ``cmd2`` documentation is Copyright 2010-2020 by the `cmd2 contributors
<https://github.com/python-cmd2/cmd2/graphs/contributors>`_ and is licensed
-under a `Creative Commons Attribution 4.0 International License
-<http://creativecommons.org/licenses/by/4.0/>`_.
+under an `MIT License <https://github.com/python-cmd2/cmd2/blob/master/LICENSE>`_.
diff --git a/docs/features/generating_output.rst b/docs/features/generating_output.rst
index 34416092..78ff7657 100644
--- a/docs/features/generating_output.rst
+++ b/docs/features/generating_output.rst
@@ -36,7 +36,7 @@ conveniences.
If you want to show an error message, put it in
``self.broken_pipe_warning`` when you initialize ``Cmd2.cmd``.
- 2. It examines and honors the :ref:`features/settings:allow_ansi` setting.
+ 2. It examines and honors the :ref:`features/settings:allow_style` setting.
See :ref:`features/generating_output:Colored Output` below for more details.
Here's a simple command that shows this method in action::
@@ -48,6 +48,30 @@ Here's a simple command that shows this method in action::
Colored Output
--------------
+The output methods in the previous section all honor the ``allow_style``
+setting, which has three possible values:
+
+Never
+ poutput(), pfeedback(), and ppaged() strip all ANSI style sequences
+ which instruct the terminal to colorize output
+
+Terminal
+ (the default value) poutput(), pfeedback(), and ppaged() do not strip any
+ ANSI style sequences when the output is a terminal, but if the output is a
+ pipe or a file the style sequences are stripped. If you want colorized
+ output you must add ANSI style sequences using either cmd2's internal ansi
+ module or another color library such as `plumbum.colors`, `colorama`, or
+ `colored`.
+
+Always
+ poutput(), pfeedback(), and ppaged() never strip ANSI style sequences,
+ regardless of the output destination
+
+Colored and otherwise styled output can be generated using the `ansi.style()`
+function:
+
+.. automethod:: cmd2.ansi.style
+ :noindex:
You may want to generate output in different colors, which is typically done by
adding `ANSI escape sequences
@@ -68,7 +92,7 @@ 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,
+:mod:`cmd2.cmd2.Cmd` includes an :ref:`features/settings:allow_style` 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
@@ -76,7 +100,7 @@ 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:
+methods will honor the :ref:`features/settings:allow_style` setting:
- :meth:`~.cmd2.Cmd.poutput`
- :meth:`~.cmd2.Cmd.perror`
@@ -144,7 +168,7 @@ Centering Text
--------------
If you would like to generate output which is centered in the user's terminal,
-the :meth:`cmd2.utils.center_text` method can help. Pass it a string and it
+the :meth:`cmd2.utils.align_center` method can help. Pass it a string and it
will figure out the width of the terminal and return you a new string,
appropriately padded so it will be centered.
@@ -158,7 +182,7 @@ are categories of Unicode characters that occupy 2 cells, and other that occupy
0. To further complicate matters, you might have included ANSI escape sequences
in the output to generate colors on the terminal.
-The :meth:`cmd2.ansi.ansi_safe_wcswidth` function solves both of these
+The :meth:`cmd2.ansi.style_aware_wcswidth` function solves both of these
problems. Pass it a string, and regardless of which Unicode characters and ANSI
escape sequences it contains, it will tell you how many characters on the
screen that string will consume when printed.
diff --git a/docs/features/settings.rst b/docs/features/settings.rst
index 88d76dc8..5e2280cd 100644
--- a/docs/features/settings.rst
+++ b/docs/features/settings.rst
@@ -20,10 +20,10 @@ Commands`. Users can use the :ref:`features/builtin_commands:set` command to
show all settings and to modify the value of any setting.
-allow_ansi
-~~~~~~~~~~
+allow_style
+~~~~~~~~~~~
-The ``allow_ansi`` setting controls the behavior of ANSI escape sequences
+The ``allow_style`` setting controls the behavior of ANSI escape sequences
in output generated with any of the following methods:
- ``poutput()``
diff --git a/examples/colors.py b/examples/colors.py
index 7a4d15e6..bbb3b2ad 100755
--- a/examples/colors.py
+++ b/examples/colors.py
@@ -6,21 +6,21 @@ A sample application for cmd2. Demonstrating colorized output.
Experiment with the command line options on the `speak` command to see how
different output colors ca
-The allow_ansi setting has three possible values:
+The allow_style setting has three possible values:
Never
- poutput(), pfeedback(), and ppaged() strip all ANSI escape sequences
+ poutput(), pfeedback(), and ppaged() strip all ANSI style 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
+ ANSI style sequences when the output is a terminal, but if the output is
+ a pipe or a file the style sequences are stripped. If you want colorized
+ output you must add ANSI style sequences using either cmd2's internal ansi
module or another color library such as `plumbum.colors` or `colorama`.
Always
- poutput(), pfeedback(), and ppaged() never strip ANSI escape sequences,
+ poutput(), pfeedback(), and ppaged() never strip ANSI style sequences,
regardless of the output destination
"""
import argparse
@@ -42,7 +42,7 @@ class CmdLineApp(cmd2.Cmd):
self.settable['maxrepeats'] = 'max repetitions for speak command'
# Should ANSI color output be allowed
- self.allow_ansi = ansi.ANSI_TERMINAL
+ self.allow_style = ansi.STYLE_TERMINAL
speak_parser = argparse.ArgumentParser()
speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
diff --git a/examples/dynamic_commands.py b/examples/dynamic_commands.py
index 69816d40..620acb7f 100755
--- a/examples/dynamic_commands.py
+++ b/examples/dynamic_commands.py
@@ -3,13 +3,32 @@
"""A simple example demonstrating how do_* commands can be created in a loop.
"""
import functools
+
import cmd2
-COMMAND_LIST = ['foo', 'bar', 'baz']
+from cmd2.constants import COMMAND_FUNC_PREFIX, HELP_FUNC_PREFIX
+
+COMMAND_LIST = ['foo', 'bar']
+CATEGORY = 'Dynamic Commands'
class CommandsInLoop(cmd2.Cmd):
"""Example of dynamically adding do_* commands."""
def __init__(self):
+ # Add dynamic commands before calling cmd2.Cmd's init since it validates command names
+ for command in COMMAND_LIST:
+ # Create command function and add help category to it
+ cmd_func = functools.partial(self.send_text, text=command)
+ cmd2.categorize(cmd_func, CATEGORY)
+
+ # Add command function to CLI object
+ cmd_func_name = COMMAND_FUNC_PREFIX + command
+ setattr(self, cmd_func_name, cmd_func)
+
+ # Add help function to CLI object
+ help_func = functools.partial(self.text_help, text=command)
+ help_func_name = HELP_FUNC_PREFIX + command
+ setattr(self, help_func_name, help_func)
+
super().__init__(use_ipython=True)
def send_text(self, args: cmd2.Statement, *, text: str):
@@ -21,11 +40,6 @@ class CommandsInLoop(cmd2.Cmd):
self.poutput("Simulate sending {!r} to a server and printing the response".format(text))
-for command in COMMAND_LIST:
- setattr(CommandsInLoop, 'do_{}'.format(command), functools.partialmethod(CommandsInLoop.send_text, text=command))
- setattr(CommandsInLoop, 'help_{}'.format(command), functools.partialmethod(CommandsInLoop.text_help, text=command))
-
-
if __name__ == '__main__':
app = CommandsInLoop()
app.cmdloop()
diff --git a/examples/plumbum_colors.py b/examples/plumbum_colors.py
index b4f0ad1c..fe692805 100755
--- a/examples/plumbum_colors.py
+++ b/examples/plumbum_colors.py
@@ -6,21 +6,21 @@ A sample application for cmd2. Demonstrating colorized output using the plumbum
Experiment with the command line options on the `speak` command to see how
different output colors ca
-The allow_ansi setting has three possible values:
+The allow_style setting has three possible values:
Never
- poutput(), pfeedback(), and ppaged() strip all ANSI escape sequences
+ poutput(), pfeedback(), and ppaged() strip all ANSI style 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
+ ANSI style sequences when the output is a terminal, but if the output is
+ a pipe or a file the style sequences are stripped. If you want colorized
+ output you must add ANSI style sequences using either cmd2's internal ansi
module or another color library such as `plumbum.colors` or `colorama`.
Always
- poutput(), pfeedback(), and ppaged() never strip ANSI escape sequences,
+ poutput(), pfeedback(), and ppaged() never strip ANSI style sequences,
regardless of the output destination
WARNING: This example requires the plumbum package, which isn't normally required by cmd2.
@@ -78,7 +78,7 @@ class CmdLineApp(cmd2.Cmd):
self.settable['maxrepeats'] = 'max repetitions for speak command'
# Should ANSI color output be allowed
- self.allow_ansi = ansi.ANSI_TERMINAL
+ self.allow_style = ansi.STYLE_TERMINAL
speak_parser = argparse.ArgumentParser()
speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
diff --git a/examples/transcripts/exampleSession.txt b/examples/transcripts/exampleSession.txt
index 3e579c28..5a75235b 100644
--- a/examples/transcripts/exampleSession.txt
+++ b/examples/transcripts/exampleSession.txt
@@ -3,7 +3,7 @@
# The regex for editor will match whatever program you use.
# regexes on prompts just make the trailing space obvious
(Cmd) set
-allow_ansi: /(Terminal|Always|Never)/
+allow_style: /(Terminal|Always|Never)/
continuation_prompt: >/ /
debug: False
echo: False
diff --git a/examples/transcripts/transcript_regex.txt b/examples/transcripts/transcript_regex.txt
index d94c442b..276f7d22 100644
--- a/examples/transcripts/transcript_regex.txt
+++ b/examples/transcripts/transcript_regex.txt
@@ -3,7 +3,7 @@
# The regex for editor will match whatever program you use.
# regexes on prompts just make the trailing space obvious
(Cmd) set
-allow_ansi: /(Terminal|Always|Never)/
+allow_style: /(Terminal|Always|Never)/
continuation_prompt: >/ /
debug: False
echo: False
diff --git a/setup.py b/setup.py
index b59d72ff..b19ae7f5 100755
--- a/setup.py
+++ b/setup.py
@@ -29,9 +29,9 @@ Programming Language :: Python :: Implementation :: CPython
Topic :: Software Development :: Libraries :: Python Modules
""".splitlines()))) # noqa: E128
-SETUP_REQUIRES = ['setuptools_scm']
+SETUP_REQUIRES = ['setuptools_scm >= 3.0.0']
-INSTALL_REQUIRES = ['pyperclip >= 1.6', 'colorama >= 0.3.7', 'attrs >= 16.3.0', 'wcwidth >= 0.1.7']
+INSTALL_REQUIRES = ['attrs >= 16.3.0', 'colorama >= 0.3.7', 'pyperclip >= 1.6', 'setuptools >= 34.4', 'wcwidth >= 0.1.7']
EXTRAS_REQUIRE = {
# Windows also requires pyreadline to ensure tab completion works
@@ -39,7 +39,7 @@ EXTRAS_REQUIRE = {
# Extra dependencies for running unit tests
'test': ["gnureadline; sys_platform=='darwin'", # include gnureadline on macOS to ensure it is available in tox env
"mock ; python_version<'3.6'", # for python 3.5 we need the third party mock module
- 'codecov', 'pytest', 'pytest-cov', 'pytest-mock'],
+ 'codecov', 'coverage', 'pytest', 'pytest-cov', 'pytest-mock'],
# development only dependencies: install with 'pip install -e .[dev]'
'dev': ["mock ; python_version<'3.6'", # for python 3.5 we need the third party mock module
'pytest', 'codecov', 'pytest-cov', 'pytest-mock', 'tox', 'flake8',
diff --git a/tests/conftest.py b/tests/conftest.py
index e09e07b1..fe74ffe2 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -88,7 +88,7 @@ SHORTCUTS_TXT = """Shortcuts for other commands:
"""
# Output from the show command with default settings
-SHOW_TXT = """allow_ansi: Terminal
+SHOW_TXT = """allow_style: Terminal
continuation_prompt: >
debug: False
echo: False
@@ -102,7 +102,7 @@ timing: False
"""
SHOW_LONG = """
-allow_ansi: Terminal # Allow ANSI escape sequences in output (valid values: Terminal, Always, Never)
+allow_style: Terminal # Allow ANSI text style 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
@@ -157,6 +157,15 @@ def base_app():
return cmd2.Cmd()
+# These are odd file names for testing quoting of them
+odd_file_names = [
+ 'nothingweird',
+ 'has spaces',
+ '"is_double_quoted"',
+ "'is_single_quoted'"
+]
+
+
def complete_tester(text: str, line: str, begidx: int, endidx: int, app) -> Optional[str]:
"""
This is a convenience function to test cmd2.complete() since
diff --git a/tests/scripts/postcmds.txt b/tests/scripts/postcmds.txt
index 74f1e226..30f47055 100644
--- a/tests/scripts/postcmds.txt
+++ b/tests/scripts/postcmds.txt
@@ -1 +1 @@
-set allow_ansi Never
+set allow_style Never
diff --git a/tests/scripts/precmds.txt b/tests/scripts/precmds.txt
index 0167aa22..7d036acf 100644
--- a/tests/scripts/precmds.txt
+++ b/tests/scripts/precmds.txt
@@ -1 +1 @@
-set allow_ansi Always
+set allow_style Always
diff --git a/tests/test_ansi.py b/tests/test_ansi.py
index 056bb2db..cb68cb28 100644
--- a/tests/test_ansi.py
+++ b/tests/test_ansi.py
@@ -10,17 +10,17 @@ import cmd2.ansi as ansi
HELLO_WORLD = 'Hello, world!'
-def test_strip_ansi():
+def test_strip_style():
base_str = HELLO_WORLD
ansi_str = ansi.style(base_str, fg='green')
assert base_str != ansi_str
- assert base_str == ansi.strip_ansi(ansi_str)
+ assert base_str == ansi.strip_style(ansi_str)
-def test_ansi_safe_wcswidth():
+def test_style_aware_wcswidth():
base_str = HELLO_WORLD
ansi_str = ansi.style(base_str, fg='green')
- assert ansi.ansi_safe_wcswidth(ansi_str) != len(ansi_str)
+ assert ansi.style_aware_wcswidth(ansi_str) != len(ansi_str)
def test_style_none():
@@ -45,10 +45,16 @@ def test_style_bg():
def test_style_bold():
base_str = HELLO_WORLD
- ansi_str = ansi.BRIGHT + base_str + ansi.NORMAL
+ ansi_str = ansi.INTENSITY_BRIGHT + base_str + ansi.INTENSITY_NORMAL
assert ansi.style(base_str, bold=True) == ansi_str
+def test_style_dim():
+ base_str = HELLO_WORLD
+ ansi_str = ansi.INTENSITY_DIM + base_str + ansi.INTENSITY_NORMAL
+ assert ansi.style(base_str, dim=True) == ansi_str
+
+
def test_style_underline():
base_str = HELLO_WORLD
ansi_str = ansi.UNDERLINE_ENABLE + base_str + ansi.UNDERLINE_DISABLE
@@ -59,9 +65,12 @@ def test_style_multi():
base_str = HELLO_WORLD
fg_color = 'blue'
bg_color = 'green'
- ansi_str = ansi.FG_COLORS[fg_color] + ansi.BG_COLORS[bg_color] + ansi.BRIGHT + ansi.UNDERLINE_ENABLE + \
- base_str + ansi.FG_RESET + ansi.BG_RESET + ansi.NORMAL + ansi.UNDERLINE_DISABLE
- assert ansi.style(base_str, fg=fg_color, bg=bg_color, bold=True, underline=True) == ansi_str
+ ansi_str = (ansi.FG_COLORS[fg_color] + ansi.BG_COLORS[bg_color] +
+ ansi.INTENSITY_BRIGHT + ansi.INTENSITY_DIM + ansi.UNDERLINE_ENABLE +
+ base_str +
+ ansi.FG_RESET + ansi.BG_RESET +
+ ansi.INTENSITY_NORMAL + ansi.INTENSITY_NORMAL + ansi.UNDERLINE_DISABLE)
+ assert ansi.style(base_str, fg=fg_color, bg=bg_color, bold=True, dim=True, underline=True) == ansi_str
def test_style_color_not_exist():
diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py
index b904a6ac..97c75ef3 100644
--- a/tests/test_argparse_completer.py
+++ b/tests/test_argparse_completer.py
@@ -535,11 +535,11 @@ def test_autocomp_blank_token(ac_app):
def test_completion_items(ac_app, num_aliases, show_description):
# Create aliases
for i in range(0, num_aliases):
- run_cmd(ac_app, 'alias create fake{} help'.format(i))
+ run_cmd(ac_app, 'alias create fake_alias{} help'.format(i))
assert len(ac_app.aliases) == num_aliases
- text = 'fake'
+ text = 'fake_alias'
line = 'alias list {}'.format(text)
endidx = len(line)
begidx = endidx - len(text)
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 88447416..b5473609 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -21,8 +21,8 @@ except ImportError:
import cmd2
from cmd2 import ansi, clipboard, constants, plugin, utils, COMMAND_NAME
-from .conftest import run_cmd, normalize, verify_help_text, HELP_HISTORY
-from .conftest import SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, complete_tester
+from .conftest import (run_cmd, normalize, verify_help_text, HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT,
+ SHOW_LONG, complete_tester, odd_file_names)
def CreateOutsimApp():
c = cmd2.Cmd()
@@ -187,26 +187,26 @@ now: True
assert out == ['quiet: True']
@pytest.mark.parametrize('new_val, is_valid, expected', [
- (ansi.ANSI_NEVER, False, ansi.ANSI_NEVER),
- ('neVeR', False, ansi.ANSI_NEVER),
- (ansi.ANSI_TERMINAL, False, ansi.ANSI_TERMINAL),
- ('TeRMInal', False, ansi.ANSI_TERMINAL),
- (ansi.ANSI_ALWAYS, False, ansi.ANSI_ALWAYS),
- ('AlWaYs', False, ansi.ANSI_ALWAYS),
- ('invalid', True, ansi.ANSI_TERMINAL),
+ (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),
])
-def test_set_allow_ansi(base_app, new_val, is_valid, expected):
- # Initialize allow_ansi for this test
- ansi.allow_ansi = ansi.ANSI_TERMINAL
+def test_set_allow_style(base_app, new_val, is_valid, expected):
+ # Initialize allow_style for this test
+ ansi.allow_style = ansi.STYLE_TERMINAL
# Use the set command to alter it
- out, err = run_cmd(base_app, 'set allow_ansi {}'.format(new_val))
+ out, err = run_cmd(base_app, 'set allow_style {}'.format(new_val))
# Verify the results
assert bool(err) == is_valid
- assert ansi.allow_ansi == expected
+ assert ansi.allow_style == expected
- # Reload ansi module to reset allow_ansi to its default since it's an
+ # 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)
@@ -376,11 +376,11 @@ def test_run_script_nested_run_scripts(base_app, request):
expected = """
%s
_relative_run_script precmds.txt
-set allow_ansi Always
+set allow_style Always
help
shortcuts
_relative_run_script postcmds.txt
-set allow_ansi Never""" % initial_run
+set allow_style Never""" % initial_run
out, err = run_cmd(base_app, 'history -s')
assert out == normalize(expected)
@@ -395,11 +395,11 @@ def test_runcmds_plus_hooks(base_app, request):
'run_script ' + postfilepath])
expected = """
run_script %s
-set allow_ansi Always
+set allow_style Always
help
shortcuts
run_script %s
-set allow_ansi Never""" % (prefilepath, postfilepath)
+set allow_style Never""" % (prefilepath, postfilepath)
out, err = run_cmd(base_app, 'history -s')
assert out == normalize(expected)
@@ -431,31 +431,15 @@ def test_relative_run_script(base_app, request):
assert script_out == manual_out
assert script_err == manual_err
-def test_relative_run_script_with_odd_file_names(base_app, monkeypatch):
+@pytest.mark.parametrize('file_name', odd_file_names)
+def test_relative_run_script_with_odd_file_names(base_app, file_name, monkeypatch):
"""Test file names with various patterns"""
# Mock out the do_run_script call to see what args are passed to it
run_script_mock = mock.MagicMock(name='do_run_script')
monkeypatch.setattr("cmd2.Cmd.do_run_script", run_script_mock)
- file_name = utils.quote_string('nothingweird.txt')
- out, err = run_cmd(base_app, "_relative_run_script {}".format(file_name))
- run_script_mock.assert_called_once_with('"nothingweird.txt"')
- run_script_mock.reset_mock()
-
- file_name = utils.quote_string('has spaces.txt')
- out, err = run_cmd(base_app, "_relative_run_script {}".format(file_name))
- run_script_mock.assert_called_once_with('"has spaces.txt"')
- run_script_mock.reset_mock()
-
- file_name = utils.quote_string('"is_double_quoted.txt"')
- out, err = run_cmd(base_app, "_relative_run_script {}".format(file_name))
- run_script_mock.assert_called_once_with('\'"is_double_quoted.txt"\'')
- run_script_mock.reset_mock()
-
- file_name = utils.quote_string("'is_single_quoted.txt'")
- out, err = run_cmd(base_app, "_relative_run_script {}".format(file_name))
- run_script_mock.assert_called_once_with('"\'is_single_quoted.txt\'"')
- run_script_mock.reset_mock()
+ run_cmd(base_app, "_relative_run_script {}".format(utils.quote_string(file_name)))
+ run_script_mock.assert_called_once_with(utils.quote_string(file_name))
def test_relative_run_script_requires_an_argument(base_app):
out, err = run_cmd(base_app, '_relative_run_script')
@@ -715,7 +699,8 @@ def test_edit_file(base_app, request, monkeypatch):
# We think we have an editor, so should expect a Popen call
m.assert_called_once()
-def test_edit_file_with_odd_file_names(base_app, monkeypatch):
+@pytest.mark.parametrize('file_name', odd_file_names)
+def test_edit_file_with_odd_file_names(base_app, file_name, monkeypatch):
"""Test editor and file names with various patterns"""
# Mock out the do_shell call to see what args are passed to it
shell_mock = mock.MagicMock(name='do_shell')
@@ -723,27 +708,8 @@ def test_edit_file_with_odd_file_names(base_app, monkeypatch):
base_app.editor = 'fooedit'
file_name = utils.quote_string('nothingweird.py')
- out, err = run_cmd(base_app, "edit {}".format(file_name))
- shell_mock.assert_called_once_with('"fooedit" "nothingweird.py"')
- shell_mock.reset_mock()
-
- base_app.editor = 'foo edit'
- file_name = utils.quote_string('has spaces.py')
- out, err = run_cmd(base_app, "edit {}".format(file_name))
- shell_mock.assert_called_once_with('"foo edit" "has spaces.py"')
- shell_mock.reset_mock()
-
- base_app.editor = '"fooedit"'
- file_name = utils.quote_string('"is_double_quoted.py"')
- out, err = run_cmd(base_app, "edit {}".format(file_name))
- shell_mock.assert_called_once_with('\'"fooedit"\' \'"is_double_quoted.py"\'')
- shell_mock.reset_mock()
-
- base_app.editor = "'fooedit'"
- file_name = utils.quote_string("'is_single_quoted.py'")
- out, err = run_cmd(base_app, "edit {}".format(file_name))
- shell_mock.assert_called_once_with('"\'fooedit\'" "\'is_single_quoted.py\'"')
- shell_mock.reset_mock()
+ run_cmd(base_app, "edit {}".format(utils.quote_string(file_name)))
+ shell_mock.assert_called_once_with('"fooedit" {}'.format(utils.quote_string(file_name)))
def test_edit_file_with_spaces(base_app, request, monkeypatch):
# Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock
@@ -1564,7 +1530,7 @@ def test_poutput_none(outsim_app):
def test_poutput_ansi_always(outsim_app):
msg = 'Hello World'
- ansi.allow_ansi = ansi.ANSI_ALWAYS
+ ansi.allow_style = ansi.STYLE_ALWAYS
colored_msg = ansi.style(msg, fg='cyan')
outsim_app.poutput(colored_msg)
out = outsim_app.stdout.getvalue()
@@ -1574,7 +1540,7 @@ def test_poutput_ansi_always(outsim_app):
def test_poutput_ansi_never(outsim_app):
msg = 'Hello World'
- ansi.allow_ansi = ansi.ANSI_NEVER
+ ansi.allow_style = ansi.STYLE_NEVER
colored_msg = ansi.style(msg, fg='cyan')
outsim_app.poutput(colored_msg)
out = outsim_app.stdout.getvalue()
@@ -1885,7 +1851,7 @@ def test_nonexistent_macro(base_app):
def test_perror_style(base_app, capsys):
msg = 'testing...'
end = '\n'
- ansi.allow_ansi = ansi.ANSI_ALWAYS
+ ansi.allow_style = ansi.STYLE_ALWAYS
base_app.perror(msg)
out, err = capsys.readouterr()
assert err == ansi.style_error(msg) + end
@@ -1893,7 +1859,7 @@ def test_perror_style(base_app, capsys):
def test_perror_no_style(base_app, capsys):
msg = 'testing...'
end = '\n'
- ansi.allow_ansi = ansi.ANSI_ALWAYS
+ ansi.allow_style = ansi.STYLE_ALWAYS
base_app.perror(msg, apply_style=False)
out, err = capsys.readouterr()
assert err == msg + end
@@ -1901,7 +1867,7 @@ def test_perror_no_style(base_app, capsys):
def test_pwarning_style(base_app, capsys):
msg = 'testing...'
end = '\n'
- ansi.allow_ansi = ansi.ANSI_ALWAYS
+ ansi.allow_style = ansi.STYLE_ALWAYS
base_app.pwarning(msg)
out, err = capsys.readouterr()
assert err == ansi.style_warning(msg) + end
@@ -1909,7 +1875,7 @@ def test_pwarning_style(base_app, capsys):
def test_pwarning_no_style(base_app, capsys):
msg = 'testing...'
end = '\n'
- ansi.allow_ansi = ansi.ANSI_ALWAYS
+ ansi.allow_style = ansi.STYLE_ALWAYS
base_app.pwarning(msg, apply_style=False)
out, err = capsys.readouterr()
assert err == msg + end
@@ -1936,7 +1902,7 @@ def test_ppaged_none(outsim_app):
def test_ppaged_strips_ansi_when_redirecting(outsim_app):
msg = 'testing...'
end = '\n'
- ansi.allow_ansi = ansi.ANSI_TERMINAL
+ ansi.allow_style = ansi.STYLE_TERMINAL
outsim_app._redirecting = True
outsim_app.ppaged(ansi.style(msg, fg='red'))
out = outsim_app.stdout.getvalue()
@@ -1945,7 +1911,7 @@ def test_ppaged_strips_ansi_when_redirecting(outsim_app):
def test_ppaged_strips_ansi_when_redirecting_if_always(outsim_app):
msg = 'testing...'
end = '\n'
- ansi.allow_ansi = ansi.ANSI_ALWAYS
+ ansi.allow_style = ansi.STYLE_ALWAYS
outsim_app._redirecting = True
colored_msg = ansi.style(msg, fg='red')
outsim_app.ppaged(colored_msg)
@@ -2112,13 +2078,13 @@ class AnsiApp(cmd2.Cmd):
def test_ansi_pouterr_always_tty(mocker, capsys):
app = AnsiApp()
- ansi.allow_ansi = ansi.ANSI_ALWAYS
+ ansi.allow_style = ansi.STYLE_ALWAYS
mocker.patch.object(app.stdout, 'isatty', return_value=True)
mocker.patch.object(sys.stderr, 'isatty', return_value=True)
app.onecmd_plus_hooks('echo_error oopsie')
out, err = capsys.readouterr()
- # if colors are on, the output should have some escape sequences in it
+ # if colors are on, the output should have some ANSI style sequences in it
assert len(out) > len('oopsie\n')
assert 'oopsie' in out
assert len(err) > len('oopsie\n')
@@ -2134,13 +2100,13 @@ def test_ansi_pouterr_always_tty(mocker, capsys):
def test_ansi_pouterr_always_notty(mocker, capsys):
app = AnsiApp()
- ansi.allow_ansi = ansi.ANSI_ALWAYS
+ ansi.allow_style = ansi.STYLE_ALWAYS
mocker.patch.object(app.stdout, 'isatty', return_value=False)
mocker.patch.object(sys.stderr, 'isatty', return_value=False)
app.onecmd_plus_hooks('echo_error oopsie')
out, err = capsys.readouterr()
- # if colors are on, the output should have some escape sequences in it
+ # if colors are on, the output should have some ANSI style sequences in it
assert len(out) > len('oopsie\n')
assert 'oopsie' in out
assert len(err) > len('oopsie\n')
@@ -2156,12 +2122,12 @@ def test_ansi_pouterr_always_notty(mocker, capsys):
def test_ansi_terminal_tty(mocker, capsys):
app = AnsiApp()
- ansi.allow_ansi = ansi.ANSI_TERMINAL
+ ansi.allow_style = ansi.STYLE_TERMINAL
mocker.patch.object(app.stdout, 'isatty', return_value=True)
mocker.patch.object(sys.stderr, 'isatty', return_value=True)
app.onecmd_plus_hooks('echo_error oopsie')
- # if colors are on, the output should have some escape sequences in it
+ # if colors are on, the output should have some ANSI style sequences in it
out, err = capsys.readouterr()
assert len(out) > len('oopsie\n')
assert 'oopsie' in out
@@ -2177,7 +2143,7 @@ def test_ansi_terminal_tty(mocker, capsys):
def test_ansi_terminal_notty(mocker, capsys):
app = AnsiApp()
- ansi.allow_ansi = ansi.ANSI_TERMINAL
+ ansi.allow_style = ansi.STYLE_TERMINAL
mocker.patch.object(app.stdout, 'isatty', return_value=False)
mocker.patch.object(sys.stderr, 'isatty', return_value=False)
@@ -2191,7 +2157,7 @@ def test_ansi_terminal_notty(mocker, capsys):
def test_ansi_never_tty(mocker, capsys):
app = AnsiApp()
- ansi.allow_ansi = ansi.ANSI_NEVER
+ ansi.allow_style = ansi.STYLE_NEVER
mocker.patch.object(app.stdout, 'isatty', return_value=True)
mocker.patch.object(sys.stderr, 'isatty', return_value=True)
@@ -2205,7 +2171,7 @@ def test_ansi_never_tty(mocker, capsys):
def test_ansi_never_notty(mocker, capsys):
app = AnsiApp()
- ansi.allow_ansi = ansi.ANSI_NEVER
+ ansi.allow_style = ansi.STYLE_NEVER
mocker.patch.object(app.stdout, 'isatty', return_value=False)
mocker.patch.object(sys.stderr, 'isatty', return_value=False)
@@ -2386,7 +2352,7 @@ def test_startup_script(request):
startup_script = os.path.join(test_dir, '.cmd2rc')
app = cmd2.Cmd(allow_cli_args=False, startup_script=startup_script)
assert len(app._startup_commands) == 1
- assert app._startup_commands[0] == "run_script '{}'".format(startup_script)
+ assert app._startup_commands[0] == "run_script {}".format(utils.quote_string(startup_script))
app._startup_commands.append('quit')
app.cmdloop()
out, err = run_cmd(app, 'alias list')
@@ -2394,6 +2360,21 @@ def test_startup_script(request):
assert 'alias create ls' in out[0]
+@pytest.mark.parametrize('startup_script', odd_file_names)
+def test_startup_script_with_odd_file_names(startup_script):
+ """Test file names with various patterns"""
+ # Mock os.path.exists to trick cmd2 into adding this script to its startup commands
+ saved_exists = os.path.exists
+ os.path.exists = mock.MagicMock(name='exists', return_value=True)
+
+ app = cmd2.Cmd(allow_cli_args=False, startup_script=startup_script)
+ assert len(app._startup_commands) == 1
+ assert app._startup_commands[0] == "run_script {}".format(utils.quote_string(os.path.abspath(startup_script)))
+
+ # Restore os.path.exists
+ os.path.exists = saved_exists
+
+
def test_transcripts_at_init():
transcript_files = ['foo', 'bar']
app = cmd2.Cmd(allow_cli_args=False, transcript_files=transcript_files)
diff --git a/tests/test_history.py b/tests/test_history.py
index 4b900030..11f189f6 100755
--- a/tests/test_history.py
+++ b/tests/test_history.py
@@ -447,15 +447,17 @@ def test_history_with_span_index_error(base_app):
with pytest.raises(ValueError):
base_app.onecmd('history "hal :"')
-def test_history_output_file(base_app):
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'shortcuts')
- run_cmd(base_app, 'help history')
+def test_history_output_file():
+ app = cmd2.Cmd(multiline_commands=['alias'])
+ run_cmd(app, 'help')
+ run_cmd(app, 'shortcuts')
+ run_cmd(app, 'help history')
+ run_cmd(app, 'alias create my_alias history;')
fd, fname = tempfile.mkstemp(prefix='', suffix='.txt')
os.close(fd)
- run_cmd(base_app, 'history -o "{}"'.format(fname))
- expected = normalize('\n'.join(['help', 'shortcuts', 'help history']))
+ run_cmd(app, 'history -o "{}"'.format(fname))
+ expected = normalize('\n'.join(['help', 'shortcuts', 'help history', 'alias create my_alias history;']))
with open(fname) as f:
content = normalize(f.read())
assert content == expected
@@ -471,10 +473,12 @@ def test_history_bad_output_file(base_app):
assert not out
assert "Error saving" in err[0]
-def test_history_edit(base_app, monkeypatch):
+def test_history_edit(monkeypatch):
+ app = cmd2.Cmd(multiline_commands=['alias'])
+
# Set a fake editor just to make sure we have one. We aren't really
# going to call it due to the mock
- base_app.editor = 'fooedit'
+ app.editor = 'fooedit'
# Mock out the _run_editor call so we don't actually open an editor
edit_mock = mock.MagicMock(name='_run_editor')
@@ -484,9 +488,11 @@ def test_history_edit(base_app, monkeypatch):
run_script_mock = mock.MagicMock(name='do_run_script')
monkeypatch.setattr("cmd2.Cmd.do_run_script", run_script_mock)
- # Run help command just so we have a command in history
- run_cmd(base_app, 'help')
- run_cmd(base_app, 'history -e 1')
+ # Put commands in history
+ run_cmd(app, 'help')
+ run_cmd(app, 'alias create my_alias history;')
+
+ run_cmd(app, 'history -e 1:2')
# Make sure both functions were called
edit_mock.assert_called_once()
diff --git a/tests/test_run_pyscript.py b/tests/test_run_pyscript.py
index a4ff097f..d717758c 100644
--- a/tests/test_run_pyscript.py
+++ b/tests/test_run_pyscript.py
@@ -6,8 +6,10 @@ Unit/functional testing for run_pytest in cmd2
import builtins
import os
+import pytest
+
from cmd2 import plugin, utils
-from .conftest import run_cmd
+from .conftest import run_cmd, odd_file_names
# Python 3.5 had some regressions in the unitest.mock module, so use 3rd party mock if available
try:
@@ -52,30 +54,19 @@ def test_run_pyscript_with_non_python_file(base_app, request):
out, err = run_cmd(base_app, 'run_pyscript {}'.format(filename))
assert "does not have a .py extension" in err[0]
-def test_run_pyscript_with_odd_file_names(base_app):
+@pytest.mark.parametrize('python_script', odd_file_names)
+def test_run_pyscript_with_odd_file_names(base_app, python_script):
"""
Pass in file names with various patterns. Since these files don't exist, we will rely
on the error text to make sure the file names were processed correctly.
"""
- python_script = utils.quote_string('nothingweird.py')
- out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
- assert "Error reading script file 'nothingweird.py'" in err[0]
-
- python_script = utils.quote_string('has spaces.py')
- out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
- assert "Error reading script file 'has spaces.py'" in err[0]
-
- # For remaining tests, mock input to get us passed the warning about not ending in .py
+ # Mock input to get us passed the warning about not ending in .py
input_mock = mock.MagicMock(name='input', return_value='1')
builtins.input = input_mock
- python_script = utils.quote_string('"is_double_quoted.py"')
- out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
- assert "Error reading script file '\"is_double_quoted.py\"'" in err[1]
-
- python_script = utils.quote_string("'is_single_quoted.py'")
- out, err = run_cmd(base_app, "run_pyscript {}".format(python_script))
- assert "Error reading script file ''is_single_quoted.py''" in err[1]
+ out, err = run_cmd(base_app, "run_pyscript {}".format(utils.quote_string(python_script)))
+ err = ''.join(err)
+ assert "Error reading script file '{}'".format(python_script) in err
def test_run_pyscript_with_exception(base_app, request):
test_dir = os.path.dirname(request.module.__file__)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index e4b9169c..b5231172 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -293,24 +293,182 @@ def test_context_flag_exit_err(context_flag):
context_flag.__exit__()
-def test_center_text_pad_none():
- msg = 'foo'
- centered = cu.center_text(msg, pad=None)
- expected_center = ' ' + msg + ' '
- assert expected_center in centered
- letters_in_centered = set(centered)
- letters_in_msg = set(msg)
- assert len(letters_in_centered) == len(letters_in_msg) + 1
-
-def test_center_text_pad_equals():
- msg = 'foo'
- pad = '='
- centered = cu.center_text(msg, pad=pad)
- expected_center = ' ' + msg + ' '
- assert expected_center in centered
- assert centered.startswith(pad)
- assert centered.endswith(pad)
- letters_in_centered = set(centered)
- letters_in_msg = set(msg)
- assert len(letters_in_centered) == len(letters_in_msg) + 2
+def test_align_text_fill_char_is_tab():
+ text = 'foo'
+ fill_char = '\t'
+ width = 5
+ aligned = cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT)
+ assert aligned == text + ' '
+
+def test_align_text_fill_char_is_too_long():
+ text = 'foo'
+ fill_char = 'fill'
+ width = 5
+ with pytest.raises(TypeError):
+ cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT)
+def test_align_text_fill_char_is_unprintable():
+ text = 'foo'
+ fill_char = '\n'
+ width = 5
+ with pytest.raises(ValueError):
+ cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT)
+
+def test_align_text_has_tabs():
+ text = '\t\tfoo'
+ fill_char = '-'
+ width = 10
+ aligned = cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT, tab_width=2)
+ assert aligned == ' ' + 'foo' + '---'
+
+def test_align_text_blank():
+ text = ''
+ fill_char = '-'
+ width = 5
+ aligned = cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT)
+ assert aligned == fill_char * width
+
+def test_align_text_wider_than_width():
+ text = 'long'
+ fill_char = '-'
+ width = 3
+ aligned = cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT)
+ assert aligned == text
+
+def test_align_text_has_unprintable():
+ text = 'foo\x02'
+ fill_char = '-'
+ width = 5
+ with pytest.raises(ValueError):
+ cu.align_text(text, fill_char=fill_char, width=width, alignment=cu.TextAlignment.LEFT)
+
+def test_align_text_term_width():
+ import shutil
+ from cmd2 import ansi
+ text = 'foo'
+ fill_char = ' '
+
+ term_width = shutil.get_terminal_size().columns
+ expected_fill = (term_width - ansi.style_aware_wcswidth(text)) * fill_char
+
+ aligned = cu.align_text(text, fill_char=fill_char, alignment=cu.TextAlignment.LEFT)
+ assert aligned == text + expected_fill
+
+def test_align_left():
+ text = 'foo'
+ fill_char = '-'
+ width = 5
+ aligned = cu.align_left(text, fill_char=fill_char, width=width)
+ assert aligned == text + fill_char + fill_char
+
+def test_align_left_multiline():
+ text = "foo\nshoes"
+ fill_char = '-'
+ width = 7
+ aligned = cu.align_left(text, fill_char=fill_char, width=width)
+ assert aligned == ('foo----\n'
+ 'shoes--')
+
+def test_align_left_wide_text():
+ text = '苹'
+ fill_char = '-'
+ width = 4
+ aligned = cu.align_left(text, fill_char=fill_char, width=width)
+ assert aligned == text + fill_char + fill_char
+
+def test_align_left_wide_fill():
+ text = 'foo'
+ fill_char = '苹'
+ width = 5
+ aligned = cu.align_left(text, fill_char=fill_char, width=width)
+ assert aligned == text + fill_char
+
+def test_align_left_wide_fill_needs_padding():
+ """Test when fill_char's display width does not divide evenly into gap"""
+ text = 'foo'
+ fill_char = '苹'
+ width = 6
+ aligned = cu.align_left(text, fill_char=fill_char, width=width)
+ assert aligned == text + fill_char + ' '
+
+def test_align_center():
+ text = 'foo'
+ fill_char = '-'
+ width = 5
+ aligned = cu.align_center(text, fill_char=fill_char, width=width)
+ assert aligned == fill_char + text + fill_char
+
+def test_align_center_multiline():
+ text = "foo\nshoes"
+ fill_char = '-'
+ width = 7
+ aligned = cu.align_center(text, fill_char=fill_char, width=width)
+ assert aligned == ('--foo--\n'
+ '-shoes-')
+
+def test_align_center_wide_text():
+ text = '苹'
+ fill_char = '-'
+ width = 4
+ aligned = cu.align_center(text, fill_char=fill_char, width=width)
+ assert aligned == fill_char + text + fill_char
+
+def test_align_center_wide_fill():
+ text = 'foo'
+ fill_char = '苹'
+ width = 7
+ aligned = cu.align_center(text, fill_char=fill_char, width=width)
+ assert aligned == fill_char + text + fill_char
+
+def test_align_center_wide_fill_needs_right_padding():
+ """Test when fill_char's display width does not divide evenly into right gap"""
+ text = 'foo'
+ fill_char = '苹'
+ width = 8
+ aligned = cu.align_center(text, fill_char=fill_char, width=width)
+ assert aligned == fill_char + text + fill_char + ' '
+
+def test_align_center_wide_fill_needs_left_and_right_padding():
+ """Test when fill_char's display width does not divide evenly into either gap"""
+ text = 'foo'
+ fill_char = '苹'
+ width = 9
+ aligned = cu.align_center(text, fill_char=fill_char, width=width)
+ assert aligned == fill_char + ' ' + text + fill_char + ' '
+
+def test_align_right():
+ text = 'foo'
+ fill_char = '-'
+ width = 5
+ aligned = cu.align_right(text, fill_char=fill_char, width=width)
+ assert aligned == fill_char + fill_char + text
+
+def test_align_right_multiline():
+ text = "foo\nshoes"
+ fill_char = '-'
+ width = 7
+ aligned = cu.align_right(text, fill_char=fill_char, width=width)
+ assert aligned == ('----foo\n'
+ '--shoes')
+
+def test_align_right_wide_text():
+ text = '苹'
+ fill_char = '-'
+ width = 4
+ aligned = cu.align_right(text, fill_char=fill_char, width=width)
+ assert aligned == fill_char + fill_char + text
+
+def test_align_right_wide_fill():
+ text = 'foo'
+ fill_char = '苹'
+ width = 5
+ aligned = cu.align_right(text, fill_char=fill_char, width=width)
+ assert aligned == fill_char + text
+
+def test_align_right_wide_fill_needs_padding():
+ """Test when fill_char's display width does not divide evenly into gap"""
+ text = 'foo'
+ fill_char = '苹'
+ width = 6
+ aligned = cu.align_right(text, fill_char=fill_char, width=width)
+ assert aligned == fill_char + ' ' + text
diff --git a/tests/transcripts/regex_set.txt b/tests/transcripts/regex_set.txt
index fdcca3a8..17f43ede 100644
--- a/tests/transcripts/regex_set.txt
+++ b/tests/transcripts/regex_set.txt
@@ -4,7 +4,7 @@
# Regexes on prompts just make the trailing space obvious
(Cmd) set
-allow_ansi: /(Terminal|Always|Never)/
+allow_style: /(Terminal|Always|Never)/
continuation_prompt: >/ /
debug: False
echo: False