summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2019-06-23 21:11:01 -0400
committerGitHub <noreply@github.com>2019-06-23 21:11:01 -0400
commitbef07746e33da9def33d814913891384a545a95c (patch)
tree86b162f79663f70cbe88e64deb4cecb93106ba68
parentc12ba0ff11b3a8fd083c641cb9149aff6494bbf9 (diff)
parenteb1936e568a2ca4817ab0cd640220a5bc355e226 (diff)
downloadcmd2-git-bef07746e33da9def33d814913891384a545a95c.tar.gz
Merge pull request #703 from python-cmd2/public_api
Minimize public API of cmd2.Cmd class
-rw-r--r--CHANGELOG.md13
-rwxr-xr-xREADME.md2
-rw-r--r--cmd2/__init__.py1
-rw-r--r--cmd2/cmd2.py300
-rw-r--r--cmd2/constants.py2
-rw-r--r--cmd2/parsing.py2
-rw-r--r--cmd2/pyscript_bridge.py6
-rw-r--r--cmd2/transcript.py30
-rw-r--r--cmd2/utils.py45
-rw-r--r--docs/argument_processing.rst2
-rw-r--r--docs/settingchanges.rst2
-rwxr-xr-xexamples/arg_print.py2
-rwxr-xr-xexamples/cmd_as_argument.py2
-rwxr-xr-xexamples/colors.py2
-rwxr-xr-xexamples/decorator_example.py2
-rwxr-xr-xexamples/example.py2
-rwxr-xr-xexamples/hooks.py6
-rwxr-xr-xexamples/pirate.py2
-rwxr-xr-xexamples/plumbum_colors.py2
-rwxr-xr-xexamples/python_scripting.py8
-rw-r--r--examples/scripts/conditional.py4
-rw-r--r--tests/conftest.py44
-rw-r--r--tests/test_cmd2.py192
-rw-r--r--tests/test_completion.py6
-rw-r--r--tests/test_transcript.py7
-rw-r--r--tests/test_utils.py23
-rw-r--r--tests/transcripts/from_cmdloop.txt21
27 files changed, 349 insertions, 381 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a66cad15..8abe6a6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,18 @@
-## 0.9.14 (TBD, 2019
+## 0.9.14 (TBD, 2019)
* Enhancements
* Added support for and testing with Python 3.8, starting with 3.8 beta
+ * Improved information displayed during transcript testing
* Breaking Changes
* Python 3.4 reached its [end of life](https://www.python.org/dev/peps/pep-0429/) on March 18, 2019 and is no longer supported by `cmd2`
+ * If you need to use Python 3.4, you should pin your requirements to use `cmd2` 0.9.13
+ * Made lots of changes to minimize the public API of the `cmd2.Cmd` class
+ * Attributes and methods we do not intend to be public now all begin with an underscore
+ * We make no API stability guarantees about these internal functions
* **Renamed Commands Notice**
* The following commands have been renamed. The old names will be supported until the next release.
- * load --> run_script
- * _relative_load --> _relative_run_script
- * pyscript --> run_pyscript
+ * `load` --> `run_script`
+ * `_relative_load` --> `_relative_run_script`
+ * `pyscript` --> `run_pyscript`
## 0.9.13 (June 14, 2019)
* Bug Fixes
diff --git a/README.md b/README.md
index c0fd8b7e..139c911d 100755
--- a/README.md
+++ b/README.md
@@ -241,7 +241,7 @@ class CmdLineApp(cmd2.Cmd):
def __init__(self):
self.maxrepeats = 3
- shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
diff --git a/cmd2/__init__.py b/cmd2/__init__.py
index 1072a3c7..e86fb9bb 100644
--- a/cmd2/__init__.py
+++ b/cmd2/__init__.py
@@ -12,4 +12,5 @@ except DistributionNotFound:
from .cmd2 import Cmd, Statement, EmptyStatement, categorize
from .cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category
+from .constants import DEFAULT_SHORTCUTS
from .pyscript_bridge import CommandResult
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 46b098c5..e5c2ac44 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -171,9 +171,9 @@ def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) ->
def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
- _, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
- statement,
- preserve_quotes)
+ _, parsed_arglist = cmd2_instance._statement_parser.get_command_arg_list(command_name,
+ statement,
+ preserve_quotes)
return func(cmd2_instance, parsed_arglist)
@@ -210,9 +210,9 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, *,
def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
- statement, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
- statement,
- preserve_quotes)
+ statement, parsed_arglist = cmd2_instance._statement_parser.get_command_arg_list(command_name,
+ statement,
+ preserve_quotes)
if ns_provider is None:
namespace = None
@@ -268,9 +268,9 @@ def with_argparser(argparser: argparse.ArgumentParser, *,
def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
- statement, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
- statement,
- preserve_quotes)
+ statement, parsed_arglist = cmd2_instance._statement_parser.get_command_arg_list(command_name,
+ statement,
+ preserve_quotes)
if ns_provider is None:
namespace = None
@@ -327,7 +327,6 @@ class Cmd(cmd.Cmd):
Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes.
"""
- DEFAULT_SHORTCUTS = {'?': 'help', '!': 'shell', '@': 'run_script', '@@': '_relative_run_script'}
DEFAULT_EDITOR = utils.find_editor()
def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
@@ -402,32 +401,31 @@ class Cmd(cmd.Cmd):
# Commands to exclude from the history command
# initialize history
- self.persistent_history_length = persistent_history_length
+ self._persistent_history_length = persistent_history_length
self._initialize_history(persistent_history_file)
self.exclude_from_history = '''history edit eof'''.split()
# Command aliases and macros
self.macros = dict()
- self.initial_stdout = sys.stdout
- self.pystate = {}
- self.py_history = []
+ self._pystate = {}
+ self._py_history = []
self.pyscript_name = 'app'
if shortcuts is None:
- shortcuts = self.DEFAULT_SHORTCUTS
+ shortcuts = constants.DEFAULT_SHORTCUTS
shortcuts = sorted(shortcuts.items(), reverse=True)
- self.statement_parser = StatementParser(allow_redirection=allow_redirection,
- terminators=terminators,
- multiline_commands=multiline_commands,
- shortcuts=shortcuts)
+ self._statement_parser = StatementParser(allow_redirection=allow_redirection,
+ terminators=terminators,
+ multiline_commands=multiline_commands,
+ shortcuts=shortcuts)
# True if running inside a Python script or interactive console, False otherwise
self._in_py = False
# Stores results from the last command run to enable usage of results in a Python script or interactive console
# Built-in commands don't make use of this. It is purely there for user-defined commands and convenience.
- self._last_result = None
+ self.last_result = None
# Used by run_script command to store current script dir as a LIFO queue to support _relative_run_script command
self._script_dir = []
@@ -437,16 +435,16 @@ class Cmd(cmd.Cmd):
# If the current command created a process to pipe to, then this will be a ProcReader object.
# Otherwise it will be None. Its used to know when a pipe process can be killed and/or waited upon.
- self.cur_pipe_proc_reader = None
+ self._cur_pipe_proc_reader = None
# Used by complete() for readline tab completion
self.completion_matches = []
# Used to keep track of whether we are redirecting or piping output
- self.redirecting = False
+ self._redirecting = False
# Used to keep track of whether a continuation prompt is being displayed
- self.at_continuation_prompt = False
+ self._at_continuation_prompt = False
# The error that prints when no help information can be found
self.help_error = "No help on {}"
@@ -535,7 +533,7 @@ class Cmd(cmd.Cmd):
self.pager_chop = 'less -SRXF'
# This boolean flag determines whether or not the cmd2 application can interact with the clipboard
- self.can_clip = can_clip
+ self._can_clip = can_clip
# This determines the value returned by cmdloop() when exiting the application
self.exit_code = 0
@@ -566,24 +564,19 @@ class Cmd(cmd.Cmd):
@property
def aliases(self) -> Dict[str, str]:
"""Read-only property to access the aliases stored in the StatementParser."""
- return self.statement_parser.aliases
-
- @property
- def shortcuts(self) -> Tuple[Tuple[str, str]]:
- """Read-only property to access the shortcuts stored in the StatementParser."""
- return self.statement_parser.shortcuts
+ return self._statement_parser.aliases
@property
def allow_redirection(self) -> bool:
"""Getter for the allow_redirection property that determines whether or not redirection of stdout is allowed."""
- return self.statement_parser.allow_redirection
+ return self._statement_parser.allow_redirection
@allow_redirection.setter
def allow_redirection(self, value: bool) -> None:
"""Setter for the allow_redirection property that determines whether or not redirection of stdout is allowed."""
- self.statement_parser.allow_redirection = value
+ self._statement_parser.allow_redirection = value
- def decolorized_write(self, fileobj: IO, msg: str) -> None:
+ def _decolorized_write(self, fileobj: IO, msg: str) -> None:
"""Write a string to a fileobject, stripping ANSI escape sequences if necessary
Honor the current colors setting, which requires us to check whether the
@@ -612,7 +605,7 @@ class Cmd(cmd.Cmd):
msg_str += end
if color:
msg_str = color + msg_str + Fore.RESET
- self.decolorized_write(self.stdout, msg_str)
+ self._decolorized_write(self.stdout, msg_str)
except BrokenPipeError:
# This occurs if a command's output is being piped to another
# process and that process closes before the command is
@@ -640,12 +633,12 @@ class Cmd(cmd.Cmd):
else:
err_msg = "{}\n".format(err)
err_msg = err_color + err_msg + Fore.RESET
- self.decolorized_write(sys.stderr, err_msg)
+ self._decolorized_write(sys.stderr, err_msg)
if traceback_war and not self.debug:
war = "To enable full traceback, run the following command: 'set debug true'\n"
war = war_color + war + Fore.RESET
- self.decolorized_write(sys.stderr, war)
+ self._decolorized_write(sys.stderr, war)
def pfeedback(self, msg: str) -> None:
"""For printing nonessential feedback. Can be silenced with `quiet`.
@@ -654,7 +647,7 @@ class Cmd(cmd.Cmd):
if self.feedback_to_output:
self.poutput(msg)
else:
- self.decolorized_write(sys.stderr, "{}\n".format(msg))
+ self._decolorized_write(sys.stderr, "{}\n".format(msg))
def ppaged(self, msg: str, end: str = '\n', chop: bool = False) -> None:
"""Print output using a pager if it would go off screen and stdout isn't currently being redirected.
@@ -689,7 +682,7 @@ 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_py and not self._script_dir:
+ if functional_terminal and not self._redirecting and not self._in_py and not self._script_dir:
if self.colors.lower() == constants.COLORS_NEVER.lower():
msg_str = utils.strip_ansi(msg_str)
@@ -703,7 +696,7 @@ class Cmd(cmd.Cmd):
pipe_proc = subprocess.Popen(pager, shell=True, stdin=subprocess.PIPE)
pipe_proc.communicate(msg_str.encode('utf-8', 'replace'))
else:
- self.decolorized_write(self.stdout, msg_str)
+ self._decolorized_write(self.stdout, msg_str)
except BrokenPipeError:
# This occurs if a command's output is being piped to another process and that process closes before the
# command is finished. If you would like your application to print a warning message, then set the
@@ -713,7 +706,7 @@ class Cmd(cmd.Cmd):
# ----- Methods related to tab completion -----
- def reset_completion_defaults(self) -> None:
+ def _reset_completion_defaults(self) -> None:
"""
Resets tab completion settings
Needs to be called each time readline runs tab completion
@@ -1157,35 +1150,6 @@ class Cmd(cmd.Cmd):
return matches
- @staticmethod
- def get_exes_in_path(starts_with: str) -> List[str]:
- """Returns names of executables in a user's path
-
- :param starts_with: what the exes should start with. leave blank for all exes in path.
- :return: a list of matching exe names
- """
- # Purposely don't match any executable containing wildcards
- wildcards = ['*', '?']
- for wildcard in wildcards:
- if wildcard in starts_with:
- return []
-
- # Get a list of every directory in the PATH environment variable and ignore symbolic links
- paths = [p for p in os.getenv('PATH').split(os.path.pathsep) if not os.path.islink(p)]
-
- # Use a set to store exe names since there can be duplicates
- exes_set = set()
-
- # Find every executable file in the user's path that matches the pattern
- for path in paths:
- full_path = os.path.join(path, starts_with)
- matches = utils.files_from_glob_pattern(full_path + '*', access=os.X_OK)
-
- for match in matches:
- exes_set.add(os.path.basename(match))
-
- return list(exes_set)
-
def shell_cmd_complete(self, text: str, line: str, begidx: int, endidx: int,
complete_blank: bool = False) -> List[str]:
"""Performs completion of executables either in a user's path or a given path
@@ -1204,7 +1168,7 @@ class Cmd(cmd.Cmd):
# If there are no path characters in the search text, then do shell command completion in the user's path
if not text.startswith('~') and os.path.sep not in text:
- return self.get_exes_in_path(text)
+ return utils.get_exes_in_path(text)
# Otherwise look for executables in the given path
else:
@@ -1382,7 +1346,7 @@ class Cmd(cmd.Cmd):
import functools
if state == 0 and rl_type != RlType.NONE:
unclosed_quote = ''
- self.reset_completion_defaults()
+ self._reset_completion_defaults()
# lstrip the original line
orig_line = readline.get_line_buffer()
@@ -1399,7 +1363,7 @@ class Cmd(cmd.Cmd):
# from text and update the indexes. This only applies if we are at the the beginning of the line.
shortcut_to_restore = ''
if begidx == 0:
- for (shortcut, _) in self.shortcuts:
+ for (shortcut, _) in self._statement_parser.shortcuts:
if text.startswith(shortcut):
# Save the shortcut to restore later
shortcut_to_restore = shortcut
@@ -1413,7 +1377,7 @@ class Cmd(cmd.Cmd):
if begidx > 0:
# Parse the command line
- statement = self.statement_parser.parse_command_only(line)
+ statement = self._statement_parser.parse_command_only(line)
command = statement.command
expanded_line = statement.command_and_args
@@ -1489,7 +1453,7 @@ class Cmd(cmd.Cmd):
# A valid command was not entered
else:
# Check if this command should be run as a shell command
- if self.default_to_shell and command in self.get_exes_in_path(command):
+ if self.default_to_shell and command in utils.get_exes_in_path(command):
compfunc = self.path_complete
else:
compfunc = self.completedefault
@@ -1556,7 +1520,7 @@ class Cmd(cmd.Cmd):
else:
# Complete token against anything a user can run
self.completion_matches = self.basic_complete(text, line, begidx, endidx,
- self.get_commands_aliases_and_macros_for_completion())
+ self._get_commands_aliases_and_macros_for_completion())
# Handle single result
if len(self.completion_matches) == 1:
@@ -1633,23 +1597,23 @@ class Cmd(cmd.Cmd):
return commands
- def get_alias_names(self) -> List[str]:
+ def _get_alias_names(self) -> List[str]:
"""Return list of current alias names"""
return list(self.aliases)
- def get_macro_names(self) -> List[str]:
+ def _get_macro_names(self) -> List[str]:
"""Return list of current macro names"""
return list(self.macros)
- def get_settable_names(self) -> List[str]:
+ def _get_settable_names(self) -> List[str]:
"""Return list of current settable names"""
return list(self.settable)
- def get_commands_aliases_and_macros_for_completion(self) -> List[str]:
+ def _get_commands_aliases_and_macros_for_completion(self) -> List[str]:
"""Return a list of visible commands, aliases, and macros for tab completion"""
visible_commands = set(self.get_visible_commands())
- alias_names = set(self.get_alias_names())
- macro_names = set(self.get_macro_names())
+ alias_names = set(self._get_alias_names())
+ macro_names = set(self._get_macro_names())
return list(visible_commands | alias_names | macro_names)
def get_help_topics(self) -> List[str]:
@@ -1666,9 +1630,9 @@ class Cmd(cmd.Cmd):
:param signum: signal number
:param frame
"""
- if self.cur_pipe_proc_reader is not None:
+ if self._cur_pipe_proc_reader is not None:
# Pass the SIGINT to the current pipe process
- self.cur_pipe_proc_reader.send_sigint()
+ self._cur_pipe_proc_reader.send_sigint()
# Check if we are allowed to re-raise the KeyboardInterrupt
if not self.sigint_protection:
@@ -1692,7 +1656,7 @@ class Cmd(cmd.Cmd):
:param line: line read by readline
:return: tuple containing (command, args, line)
"""
- statement = self.statement_parser.parse_command_only(line)
+ statement = self._statement_parser.parse_command_only(line)
return statement.command, statement.args, statement.command_and_args
def onecmd_plus_hooks(self, line: str, pyscript_bridge_call: bool = False) -> bool:
@@ -1732,8 +1696,8 @@ class Cmd(cmd.Cmd):
# we need to run the finalization hooks
raise EmptyStatement
- # Keep track of whether or not we were already redirecting before this command
- already_redirecting = self.redirecting
+ # Keep track of whether or not we were already _redirecting before this command
+ already_redirecting = self._redirecting
# This will be a utils.RedirectionSavedState object for the command
saved_state = None
@@ -1746,13 +1710,13 @@ class Cmd(cmd.Cmd):
self.stdout.pause_storage = False
redir_error, saved_state = self._redirect_output(statement)
- self.cur_pipe_proc_reader = saved_state.pipe_proc_reader
+ self._cur_pipe_proc_reader = saved_state.pipe_proc_reader
# Do not continue if an error occurred while trying to redirect
if not redir_error:
- # See if we need to update self.redirecting
+ # See if we need to update self._redirecting
if not already_redirecting:
- self.redirecting = saved_state.redirecting
+ self._redirecting = saved_state.redirecting
timestart = datetime.datetime.now()
@@ -1788,7 +1752,7 @@ class Cmd(cmd.Cmd):
self._restore_output(statement, saved_state)
if not already_redirecting:
- self.redirecting = False
+ self._redirecting = False
if pyscript_bridge_call:
# Stop saving command's stdout before command finalization hooks run
@@ -1856,7 +1820,7 @@ class Cmd(cmd.Cmd):
"""
while True:
try:
- statement = self.statement_parser.parse(line)
+ statement = self._statement_parser.parse(line)
if statement.multiline_command and statement.terminator:
# we have a completed multiline command, we are done
break
@@ -1867,7 +1831,7 @@ class Cmd(cmd.Cmd):
except ValueError:
# we have unclosed quotation marks, lets parse only the command
# and see if it's a multiline
- statement = self.statement_parser.parse_command_only(line)
+ statement = self._statement_parser.parse_command_only(line)
if not statement.multiline_command:
# not a multiline command, so raise the exception
raise
@@ -1876,8 +1840,8 @@ class Cmd(cmd.Cmd):
# - a multiline command with no terminator
# - a multiline command with unclosed quotation marks
try:
- self.at_continuation_prompt = True
- newline = self.pseudo_raw_input(self.continuation_prompt)
+ self._at_continuation_prompt = True
+ newline = self._pseudo_raw_input(self.continuation_prompt)
if newline == 'eof':
# they entered either a blank line, or we hit an EOF
# for some other reason. Turn the literal 'eof'
@@ -1891,10 +1855,10 @@ class Cmd(cmd.Cmd):
raise ex
else:
self.poutput('^C')
- statement = self.statement_parser.parse('')
+ statement = self._statement_parser.parse('')
break
finally:
- self.at_continuation_prompt = False
+ self._at_continuation_prompt = False
if not statement.command:
raise EmptyStatement()
@@ -2001,7 +1965,7 @@ class Cmd(cmd.Cmd):
redir_error = False
# Initialize the saved state
- saved_state = utils.RedirectionSavedState(self.stdout, sys.stdout, self.cur_pipe_proc_reader)
+ saved_state = utils.RedirectionSavedState(self.stdout, sys.stdout, self._cur_pipe_proc_reader)
if not self.allow_redirection:
return redir_error, saved_state
@@ -2055,7 +2019,7 @@ class Cmd(cmd.Cmd):
elif statement.output:
import tempfile
- if (not statement.output_to) and (not self.can_clip):
+ if (not statement.output_to) and (not self._can_clip):
self.perror("Cannot redirect to paste buffer; install 'pyperclip' and re-run to enable",
traceback_war=False)
redir_error = True
@@ -2109,22 +2073,22 @@ class Cmd(cmd.Cmd):
sys.stdout = saved_state.saved_sys_stdout
# Check if we need to wait for the process being piped to
- if self.cur_pipe_proc_reader is not None:
- self.cur_pipe_proc_reader.wait()
+ if self._cur_pipe_proc_reader is not None:
+ self._cur_pipe_proc_reader.wait()
- # Restore cur_pipe_proc_reader. This always is done, regardless of whether this command redirected.
- self.cur_pipe_proc_reader = saved_state.saved_pipe_proc_reader
+ # Restore _cur_pipe_proc_reader. This always is done, regardless of whether this command redirected.
+ self._cur_pipe_proc_reader = saved_state.saved_pipe_proc_reader
def cmd_func(self, command: str) -> Optional[Callable]:
"""
Get the function for a command
:param command: the name of the command
"""
- func_name = self.cmd_func_name(command)
+ func_name = self._cmd_func_name(command)
if func_name:
return getattr(self, func_name)
- def cmd_func_name(self, command: str) -> str:
+ def _cmd_func_name(self, command: str) -> str:
"""Get the method name associated with a given command.
:param command: command to look up method name which implements it
@@ -2175,9 +2139,9 @@ class Cmd(cmd.Cmd):
return self.do_shell(statement.command_and_args)
else:
err_msg = self.default_error.format(statement.command)
- self.decolorized_write(sys.stderr, "{}\n".format(err_msg))
+ self._decolorized_write(sys.stderr, "{}\n".format(err_msg))
- def pseudo_raw_input(self, prompt: str) -> str:
+ def _pseudo_raw_input(self, prompt: str) -> str:
"""Began life as a copy of cmd's cmdloop; like raw_input but
- accounts for changed stdin, stdout
@@ -2272,7 +2236,7 @@ class Cmd(cmd.Cmd):
while not stop:
# Get commands from user
try:
- line = self.pseudo_raw_input(self.prompt)
+ line = self._pseudo_raw_input(self.prompt)
except KeyboardInterrupt as ex:
if self.quit_on_sigint:
raise ex
@@ -2298,11 +2262,11 @@ class Cmd(cmd.Cmd):
# ----- Alias sub-command functions -----
- def alias_create(self, args: argparse.Namespace) -> None:
+ def _alias_create(self, args: argparse.Namespace) -> None:
"""Create or overwrite an alias"""
# Validate the alias name
- valid, errmsg = self.statement_parser.is_valid_command(args.name)
+ valid, errmsg = self._statement_parser.is_valid_command(args.name)
if not valid:
self.perror("Invalid alias name: {}".format(errmsg), traceback_war=False)
return
@@ -2313,7 +2277,7 @@ class Cmd(cmd.Cmd):
# Unquote redirection and terminator tokens
tokens_to_unquote = constants.REDIRECTION_TOKENS
- tokens_to_unquote.extend(self.statement_parser.terminators)
+ tokens_to_unquote.extend(self._statement_parser.terminators)
utils.unquote_specific_tokens(args.command_args, tokens_to_unquote)
# Build the alias value string
@@ -2326,7 +2290,7 @@ class Cmd(cmd.Cmd):
self.aliases[args.name] = value
self.poutput("Alias '{}' {}".format(args.name, result))
- def alias_delete(self, args: argparse.Namespace) -> None:
+ def _alias_delete(self, args: argparse.Namespace) -> None:
"""Delete aliases"""
if args.all:
self.aliases.clear()
@@ -2341,7 +2305,7 @@ class Cmd(cmd.Cmd):
else:
self.perror("Alias '{}' does not exist".format(cur_name), traceback_war=False)
- def alias_list(self, args: argparse.Namespace) -> None:
+ def _alias_list(self, args: argparse.Namespace) -> None:
"""List some or all aliases"""
if args.name:
for cur_name in utils.remove_duplicates(args.name):
@@ -2386,11 +2350,11 @@ class Cmd(cmd.Cmd):
epilog=alias_create_epilog)
alias_create_parser.add_argument('name', help='name of this alias')
setattr(alias_create_parser.add_argument('command', help='what the alias resolves to'),
- ACTION_ARG_CHOICES, get_commands_aliases_and_macros_for_completion)
+ ACTION_ARG_CHOICES, _get_commands_aliases_and_macros_for_completion)
setattr(alias_create_parser.add_argument('command_args', nargs=argparse.REMAINDER,
help='arguments to pass to command'),
ACTION_ARG_CHOICES, ('path_complete',))
- alias_create_parser.set_defaults(func=alias_create)
+ alias_create_parser.set_defaults(func=_alias_create)
# alias -> delete
alias_delete_help = "delete aliases"
@@ -2398,9 +2362,9 @@ class Cmd(cmd.Cmd):
alias_delete_parser = alias_subparsers.add_parser('delete', help=alias_delete_help,
description=alias_delete_description)
setattr(alias_delete_parser.add_argument('name', nargs='*', help='alias to delete'),
- ACTION_ARG_CHOICES, get_alias_names)
+ ACTION_ARG_CHOICES, _get_alias_names)
alias_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all aliases")
- alias_delete_parser.set_defaults(func=alias_delete)
+ alias_delete_parser.set_defaults(func=_alias_delete)
# alias -> list
alias_list_help = "list aliases"
@@ -2412,8 +2376,8 @@ class Cmd(cmd.Cmd):
alias_list_parser = alias_subparsers.add_parser('list', help=alias_list_help,
description=alias_list_description)
setattr(alias_list_parser.add_argument('name', nargs="*", help='alias to list'),
- ACTION_ARG_CHOICES, get_alias_names)
- alias_list_parser.set_defaults(func=alias_list)
+ ACTION_ARG_CHOICES, _get_alias_names)
+ alias_list_parser.set_defaults(func=_alias_list)
# Preserve quotes since we are passing strings to other commands
@with_argparser(alias_parser, preserve_quotes=True)
@@ -2429,11 +2393,11 @@ class Cmd(cmd.Cmd):
# ----- Macro sub-command functions -----
- def macro_create(self, args: argparse.Namespace) -> None:
+ def _macro_create(self, args: argparse.Namespace) -> None:
"""Create or overwrite a macro"""
# Validate the macro name
- valid, errmsg = self.statement_parser.is_valid_command(args.name)
+ valid, errmsg = self._statement_parser.is_valid_command(args.name)
if not valid:
self.perror("Invalid macro name: {}".format(errmsg), traceback_war=False)
return
@@ -2448,7 +2412,7 @@ class Cmd(cmd.Cmd):
# Unquote redirection and terminator tokens
tokens_to_unquote = constants.REDIRECTION_TOKENS
- tokens_to_unquote.extend(self.statement_parser.terminators)
+ tokens_to_unquote.extend(self._statement_parser.terminators)
utils.unquote_specific_tokens(args.command_args, tokens_to_unquote)
# Build the macro value string
@@ -2507,7 +2471,7 @@ class Cmd(cmd.Cmd):
self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, arg_list=arg_list)
self.poutput("Macro '{}' {}".format(args.name, result))
- def macro_delete(self, args: argparse.Namespace) -> None:
+ def _macro_delete(self, args: argparse.Namespace) -> None:
"""Delete macros"""
if args.all:
self.macros.clear()
@@ -2522,7 +2486,7 @@ class Cmd(cmd.Cmd):
else:
self.perror("Macro '{}' does not exist".format(cur_name), traceback_war=False)
- def macro_list(self, args: argparse.Namespace) -> None:
+ def _macro_list(self, args: argparse.Namespace) -> None:
"""List some or all macros"""
if args.name:
for cur_name in utils.remove_duplicates(args.name):
@@ -2590,11 +2554,11 @@ class Cmd(cmd.Cmd):
epilog=macro_create_epilog)
macro_create_parser.add_argument('name', help='name of this macro')
setattr(macro_create_parser.add_argument('command', help='what the macro resolves to'),
- ACTION_ARG_CHOICES, get_commands_aliases_and_macros_for_completion)
+ ACTION_ARG_CHOICES, _get_commands_aliases_and_macros_for_completion)
setattr(macro_create_parser.add_argument('command_args', nargs=argparse.REMAINDER,
help='arguments to pass to command'),
ACTION_ARG_CHOICES, ('path_complete',))
- macro_create_parser.set_defaults(func=macro_create)
+ macro_create_parser.set_defaults(func=_macro_create)
# macro -> delete
macro_delete_help = "delete macros"
@@ -2602,9 +2566,9 @@ class Cmd(cmd.Cmd):
macro_delete_parser = macro_subparsers.add_parser('delete', help=macro_delete_help,
description=macro_delete_description)
setattr(macro_delete_parser.add_argument('name', nargs='*', help='macro to delete'),
- ACTION_ARG_CHOICES, get_macro_names)
+ ACTION_ARG_CHOICES, _get_macro_names)
macro_delete_parser.add_argument('-a', '--all', action='store_true', help="delete all macros")
- macro_delete_parser.set_defaults(func=macro_delete)
+ macro_delete_parser.set_defaults(func=_macro_delete)
# macro -> list
macro_list_help = "list macros"
@@ -2615,8 +2579,8 @@ class Cmd(cmd.Cmd):
macro_list_parser = macro_subparsers.add_parser('list', help=macro_list_help, description=macro_list_description)
setattr(macro_list_parser.add_argument('name', nargs="*", help='macro to list'),
- ACTION_ARG_CHOICES, get_macro_names)
- macro_list_parser.set_defaults(func=macro_list)
+ ACTION_ARG_CHOICES, _get_macro_names)
+ macro_list_parser.set_defaults(func=_macro_list)
# Preserve quotes since we are passing strings to other commands
@with_argparser(macro_parser, preserve_quotes=True)
@@ -2707,7 +2671,7 @@ class Cmd(cmd.Cmd):
# If there is no help information then print an error
elif help_func is None and (func is None or not func.__doc__):
err_msg = self.help_error.format(args.command)
- self.decolorized_write(sys.stderr, "{}\n".format(err_msg))
+ self._decolorized_write(sys.stderr, "{}\n".format(err_msg))
# Otherwise delegate to cmd base class do_help()
else:
@@ -2841,7 +2805,7 @@ class Cmd(cmd.Cmd):
@with_argparser(ACArgumentParser())
def do_shortcuts(self, _: argparse.Namespace) -> None:
"""List available shortcuts"""
- result = "\n".join('%s: %s' % (sc[0], sc[1]) for sc in sorted(self.shortcuts))
+ result = "\n".join('%s: %s' % (sc[0], sc[1]) for sc in sorted(self._statement_parser.shortcuts))
self.poutput("Shortcuts for other commands:\n{}\n".format(result))
@with_argparser(ACArgumentParser(epilog=INTERNAL_COMMAND_EPILOG))
@@ -2902,7 +2866,7 @@ class Cmd(cmd.Cmd):
len(fulloptions)))
return result
- def cmdenvironment(self) -> str:
+ def _cmdenvironment(self) -> str:
"""Get a summary report of read-only settings which the user cannot modify at runtime.
:return: summary report of read-only settings which the user cannot modify at runtime
@@ -2910,9 +2874,9 @@ class Cmd(cmd.Cmd):
read_only_settings = """
Commands may be terminated with: {}
Output redirection and pipes allowed: {}"""
- return read_only_settings.format(str(self.statement_parser.terminators), self.allow_redirection)
+ return read_only_settings.format(str(self._statement_parser.terminators), self.allow_redirection)
- def show(self, args: argparse.Namespace, parameter: str = '') -> None:
+ def _show(self, args: argparse.Namespace, parameter: str = '') -> None:
"""Shows current settings of parameters.
:param args: argparse parsed arguments from the set command
@@ -2936,7 +2900,7 @@ class Cmd(cmd.Cmd):
# If user has requested to see all settings, also show read-only settings
if args.all:
- self.poutput('\nRead only settings:{}'.format(self.cmdenvironment()))
+ self.poutput('\nRead only settings:{}'.format(self._cmdenvironment()))
else:
self.perror("Parameter '{}' not supported (type 'set' for list of parameters).".format(param),
traceback_war=False)
@@ -2950,7 +2914,7 @@ class Cmd(cmd.Cmd):
set_parser.add_argument('-a', '--all', action='store_true', help='display read-only settings as well')
set_parser.add_argument('-l', '--long', action='store_true', help='describe function of parameter')
setattr(set_parser.add_argument('param', nargs='?', help='parameter to set or view'),
- ACTION_ARG_CHOICES, get_settable_names)
+ ACTION_ARG_CHOICES, _get_settable_names)
set_parser.add_argument('value', nargs='?', help='the new value for settable')
@with_argparser(set_parser)
@@ -2959,12 +2923,12 @@ class Cmd(cmd.Cmd):
# Check if param was passed in
if not args.param:
- return self.show(args)
+ return self._show(args)
param = utils.norm_fold(args.param.strip())
# Check if value was passed in
if not args.value:
- return self.show(args, param)
+ return self._show(args, param)
value = args.value
# Check if param points to just one settable
@@ -2973,7 +2937,7 @@ class Cmd(cmd.Cmd):
if len(hits) == 1:
param = hits[0]
else:
- return self.show(args, param)
+ return self._show(args, param)
# Update the settable's value
current_value = getattr(self, param)
@@ -3094,17 +3058,17 @@ class Cmd(cmd.Cmd):
raise EmbeddedConsoleExit
# Set up Python environment
- self.pystate[self.pyscript_name] = bridge
- self.pystate['run'] = py_run
- self.pystate['quit'] = py_quit
- self.pystate['exit'] = py_quit
+ self._pystate[self.pyscript_name] = bridge
+ self._pystate['run'] = py_run
+ self._pystate['quit'] = py_quit
+ self._pystate['exit'] = py_quit
if self.locals_in_py:
- self.pystate['self'] = self
- elif 'self' in self.pystate:
- del self.pystate['self']
+ self._pystate['self'] = self
+ elif 'self' in self._pystate:
+ del self._pystate['self']
- localvars = self.pystate
+ localvars = self._pystate
from code import InteractiveConsole
interp = InteractiveConsole(locals=localvars)
interp.runcode('import sys, os;sys.path.insert(0, os.getcwd())')
@@ -3139,7 +3103,7 @@ class Cmd(cmd.Cmd):
readline.clear_history()
# Restore py's history
- for item in self.py_history:
+ for item in self._py_history:
readline.add_history(item)
if self.use_rawinput and self.completekey:
@@ -3206,10 +3170,10 @@ class Cmd(cmd.Cmd):
# Set up readline for cmd2
if rl_type != RlType.NONE:
# Save py's history
- self.py_history.clear()
+ self._py_history.clear()
for i in range(1, readline.get_current_history_length() + 1):
# noinspection PyArgumentList
- self.py_history.append(readline.get_history_item(i))
+ self._py_history.append(readline.get_history_item(i))
readline.clear_history()
@@ -3508,7 +3472,7 @@ class Cmd(cmd.Cmd):
if not self.persistent_history_file:
return
- self.history.truncate(self.persistent_history_length)
+ self.history.truncate(self._persistent_history_length)
try:
with open(self.persistent_history_file, 'wb') as fobj:
pickle.dump(self.history, fobj)
@@ -3740,7 +3704,7 @@ class Cmd(cmd.Cmd):
# _relative_load has been deprecated
do__relative_load = do__relative_run_script
- def run_transcript_tests(self, transcript_paths: List[str]) -> None:
+ def _run_transcript_tests(self, transcript_paths: List[str]) -> None:
"""Runs transcript tests for provided file(s).
This is called when either -t is provided on the command line or the transcript_files argument is provided
@@ -3748,7 +3712,10 @@ class Cmd(cmd.Cmd):
:param transcript_paths: list of transcript test file paths
"""
+ import time
import unittest
+ import cmd2
+ from colorama import Style
from .transcript import Cmd2TestCase
class TestMyAppCase(Cmd2TestCase):
@@ -3761,15 +3728,28 @@ class Cmd(cmd.Cmd):
self.exit_code = -1
return
+ verinfo = ".".join(map(str, sys.version_info[:3]))
+ num_transcripts = len(transcripts_expanded)
+ plural = '' if len(transcripts_expanded) == 1 else 's'
+ self.poutput(Style.BRIGHT + utils.center_text('cmd2 transcript test', pad='=') + Style.RESET_ALL)
+ self.poutput('platform {} -- Python {}, cmd2-{}, readline-{}'.format(sys.platform, verinfo, cmd2.__version__,
+ rl_type))
+ self.poutput('cwd: {}'.format(os.getcwd()))
+ self.poutput('cmd2 app: {}'.format(sys.argv[0]))
+ self.poutput(Style.BRIGHT + 'collected {} transcript{}\n'.format(num_transcripts, plural) + Style.RESET_ALL)
+
self.__class__.testfiles = transcripts_expanded
sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main()
testcase = TestMyAppCase()
stream = utils.StdSim(sys.stderr)
runner = unittest.TextTestRunner(stream=stream)
+ start_time = time.time()
test_results = runner.run(testcase)
+ execution_time = time.time() - start_time
if test_results.wasSuccessful():
- self.decolorized_write(sys.stderr, stream.read())
- self.poutput('Tests passed', color=Fore.LIGHTGREEN_EX)
+ self._decolorized_write(sys.stderr, stream.read())
+ finish_msg = '{0} transcript{1} passed in {2:.3f} seconds'.format(num_transcripts, plural, execution_time)
+ self.poutput(Style.BRIGHT + utils.center_text(finish_msg, pad='=') + Style.RESET_ALL, color=Fore.GREEN)
else:
# Strip off the initial traceback which isn't particularly useful for end users
error_str = stream.read()
@@ -3809,7 +3789,7 @@ class Cmd(cmd.Cmd):
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
+ current_prompt = self.continuation_prompt if self._at_continuation_prompt else self.prompt
# Only update terminal if there are changes
update_terminal = False
@@ -3823,7 +3803,7 @@ class Cmd(cmd.Cmd):
self.prompt = new_prompt
# If we aren't at a continuation prompt, then it's OK to update it
- if not self.at_continuation_prompt:
+ if not self._at_continuation_prompt:
rl_set_prompt(self.prompt)
update_terminal = True
@@ -3948,7 +3928,7 @@ class Cmd(cmd.Cmd):
# Restore the command and help functions to their original values
dc = self.disabled_commands[command]
- setattr(self, self.cmd_func_name(command), dc.command_function)
+ setattr(self, self._cmd_func_name(command), dc.command_function)
if dc.help_function is None:
delattr(self, help_func_name)
@@ -3998,7 +3978,7 @@ class Cmd(cmd.Cmd):
# Overwrite the command and help functions to print the message
new_func = functools.partial(self._report_disabled_command_usage,
message_to_print=message_to_print.replace(COMMAND_NAME, command))
- setattr(self, self.cmd_func_name(command), new_func)
+ setattr(self, self._cmd_func_name(command), new_func)
setattr(self, help_func_name, new_func)
def disable_category(self, category: str, message_to_print: str) -> None:
@@ -4027,7 +4007,7 @@ class Cmd(cmd.Cmd):
:param message_to_print: the message reporting that the command is disabled
:param kwargs: not used
"""
- self.decolorized_write(sys.stderr, "{}\n".format(message_to_print))
+ self._decolorized_write(sys.stderr, "{}\n".format(message_to_print))
def cmdloop(self, intro: Optional[str] = None) -> int:
"""This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2.
@@ -4060,7 +4040,7 @@ class Cmd(cmd.Cmd):
# If transcript-based regression testing was requested, then do that instead of the main loop
if self._transcript_files is not None:
- self.run_transcript_tests([os.path.expanduser(tf) for tf in self._transcript_files])
+ self._run_transcript_tests([os.path.expanduser(tf) for tf in self._transcript_files])
else:
# If an intro was supplied in the method call, allow it to override the default
if intro is not None:
diff --git a/cmd2/constants.py b/cmd2/constants.py
index dede0381..06d6c6c4 100644
--- a/cmd2/constants.py
+++ b/cmd2/constants.py
@@ -24,3 +24,5 @@ LINE_FEED = '\n'
COLORS_NEVER = 'Never'
COLORS_TERMINAL = 'Terminal'
COLORS_ALWAYS = 'Always'
+
+DEFAULT_SHORTCUTS = {'?': 'help', '!': 'shell', '@': 'run_script', '@@': '_relative_run_script'}
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index 8febd270..f705128c 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -324,7 +324,7 @@ class StatementParser:
This string is suitable for inclusion in an error message of your
choice:
- valid, errmsg = statement_parser.is_valid_command('>')
+ valid, errmsg = _statement_parser.is_valid_command('>')
if not valid:
errmsg = "Alias {}".format(errmsg)
"""
diff --git a/cmd2/pyscript_bridge.py b/cmd2/pyscript_bridge.py
index 01d283ea..ac3dfd40 100644
--- a/cmd2/pyscript_bridge.py
+++ b/cmd2/pyscript_bridge.py
@@ -23,7 +23,7 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr
Any combination of these fields can be used when developing a scripting API for a given command.
By default stdout, stderr, and stop will be captured for you. If there is additional command specific data,
- then write that to cmd2's _last_result member. That becomes the data member of this tuple.
+ then write that to cmd2's last_result member. That becomes the data member of this tuple.
In some cases, the data member may contain everything needed for a command and storing stdout
and stderr might just be a duplication of data that wastes memory. In that case, the StdSim can
@@ -88,7 +88,7 @@ class PyscriptBridge(object):
# This will be used to capture sys.stderr
copy_stderr = StdSim(sys.stderr, echo)
- self._cmd2_app._last_result = None
+ self._cmd2_app.last_result = None
stop = False
try:
@@ -105,5 +105,5 @@ class PyscriptBridge(object):
result = CommandResult(stdout=copy_cmd_stdout.getvalue(),
stderr=copy_stderr.getvalue() if copy_stderr.getvalue() else None,
stop=stop,
- data=self._cmd2_app._last_result)
+ data=self._cmd2_app.last_result)
return result
diff --git a/cmd2/transcript.py b/cmd2/transcript.py
index 5a115496..316592ce 100644
--- a/cmd2/transcript.py
+++ b/cmd2/transcript.py
@@ -6,8 +6,8 @@ If the user wants to run a transcript (see docs/transcript.rst),
we need a mechanism to run each command in the transcript as
a unit test, comparing the expected output to the actual output.
-This file contains the classess necessary to make that work. These
-classes are used in cmd2.py::run_transcript_tests()
+This file contains the class necessary to make that work. This
+class is used in cmd2.py::run_transcript_tests()
"""
import re
import unittest
@@ -27,27 +27,32 @@ class Cmd2TestCase(unittest.TestCase):
"""
cmdapp = None
- def fetchTranscripts(self):
- self.transcripts = {}
- for fname in self.cmdapp.testfiles:
- tfile = open(fname)
- self.transcripts[fname] = iter(tfile.readlines())
- tfile.close()
-
def setUp(self):
if self.cmdapp:
- self.fetchTranscripts()
+ self._fetchTranscripts()
# Trap stdout
self._orig_stdout = self.cmdapp.stdout
self.cmdapp.stdout = utils.StdSim(self.cmdapp.stdout)
+ def tearDown(self):
+ if self.cmdapp:
+ # Restore stdout
+ self.cmdapp.stdout = self._orig_stdout
+
def runTest(self): # was testall
if self.cmdapp:
its = sorted(self.transcripts.items())
for (fname, transcript) in its:
self._test_transcript(fname, transcript)
+ def _fetchTranscripts(self):
+ self.transcripts = {}
+ for fname in self.cmdapp.testfiles:
+ tfile = open(fname)
+ self.transcripts[fname] = iter(tfile.readlines())
+ tfile.close()
+
def _test_transcript(self, fname: str, transcript):
line_num = 0
finished = False
@@ -205,8 +210,3 @@ class Cmd2TestCase(unittest.TestCase):
# slash is not escaped, this is what we are looking for
break
return regex, pos, start
-
- def tearDown(self):
- if self.cmdapp:
- # Restore stdout
- self.cmdapp.stdout = self._orig_stdout
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 3500ba7a..3e28641d 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -5,6 +5,7 @@ import collections
import glob
import os
import re
+import shutil
import subprocess
import sys
import threading
@@ -348,6 +349,50 @@ def files_from_glob_patterns(patterns: List[str], access=os.F_OK) -> List[str]:
return files
+def get_exes_in_path(starts_with: str) -> List[str]:
+ """Returns names of executables in a user's path
+
+ :param starts_with: what the exes should start with. leave blank for all exes in path.
+ :return: a list of matching exe names
+ """
+ # Purposely don't match any executable containing wildcards
+ wildcards = ['*', '?']
+ for wildcard in wildcards:
+ if wildcard in starts_with:
+ return []
+
+ # Get a list of every directory in the PATH environment variable and ignore symbolic links
+ paths = [p for p in os.getenv('PATH').split(os.path.pathsep) if not os.path.islink(p)]
+
+ # Use a set to store exe names since there can be duplicates
+ exes_set = set()
+
+ # Find every executable file in the user's path that matches the pattern
+ for path in paths:
+ full_path = os.path.join(path, starts_with)
+ matches = files_from_glob_pattern(full_path + '*', access=os.X_OK)
+
+ for match in matches:
+ exes_set.add(os.path.basename(match))
+
+ 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: (optional) 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.
diff --git a/docs/argument_processing.rst b/docs/argument_processing.rst
index 4bd917cf..599e4cf0 100644
--- a/docs/argument_processing.rst
+++ b/docs/argument_processing.rst
@@ -267,7 +267,7 @@ Here's what it looks like::
if unknown:
self.perror("dir does not take any positional arguments:", traceback_war=False)
self.do_help('dir')
- self._last_result = CommandResult('', 'Bad arguments')
+ self.last_result = CommandResult('', 'Bad arguments')
return
# Get the contents as a list
diff --git a/docs/settingchanges.rst b/docs/settingchanges.rst
index aa6d9084..0e4feac1 100644
--- a/docs/settingchanges.rst
+++ b/docs/settingchanges.rst
@@ -33,7 +33,7 @@ To define more shortcuts, update the dict ``App.shortcuts`` with the
class App(Cmd2):
def __init__(self):
- shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'*': 'sneeze', '~': 'squirm'})
cmd2.Cmd.__init__(self, shortcuts=shortcuts)
diff --git a/examples/arg_print.py b/examples/arg_print.py
index 48bcbd13..3f7f3815 100755
--- a/examples/arg_print.py
+++ b/examples/arg_print.py
@@ -19,7 +19,7 @@ class ArgumentAndOptionPrinter(cmd2.Cmd):
def __init__(self):
# Create command shortcuts which are typically 1 character abbreviations which can be used in place of a command
- shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'$': 'aprint', '%': 'oprint'})
super().__init__(shortcuts=shortcuts)
diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py
index 1e7901b9..49a50670 100755
--- a/examples/cmd_as_argument.py
+++ b/examples/cmd_as_argument.py
@@ -28,7 +28,7 @@ class CmdLineApp(cmd2.Cmd):
MUMBLE_LAST = ['right?']
def __init__(self):
- shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
super().__init__(allow_cli_args=False, use_ipython=True, multiline_commands=['orate'], shortcuts=shortcuts)
diff --git a/examples/colors.py b/examples/colors.py
index fdc0e0bd..f8a9dfdb 100755
--- a/examples/colors.py
+++ b/examples/colors.py
@@ -63,7 +63,7 @@ class CmdLineApp(cmd2.Cmd):
MUMBLE_LAST = ['right?']
def __init__(self):
- shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
super().__init__(use_ipython=True, multiline_commands=['orate'], shortcuts=shortcuts)
diff --git a/examples/decorator_example.py b/examples/decorator_example.py
index bb0d58c0..4f68653e 100755
--- a/examples/decorator_example.py
+++ b/examples/decorator_example.py
@@ -19,7 +19,7 @@ import cmd2
class CmdLineApp(cmd2.Cmd):
""" Example cmd2 application. """
def __init__(self, ip_addr=None, port=None, transcript_files=None):
- shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
super().__init__(use_ipython=False, transcript_files=transcript_files, multiline_commands=['orate'],
diff --git a/examples/example.py b/examples/example.py
index a1ec893c..24be5d5d 100755
--- a/examples/example.py
+++ b/examples/example.py
@@ -26,7 +26,7 @@ class CmdLineApp(cmd2.Cmd):
MUMBLE_LAST = ['right?']
def __init__(self):
- shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
super().__init__(use_ipython=False, multiline_commands=['orate'], shortcuts=shortcuts)
diff --git a/examples/hooks.py b/examples/hooks.py
index 42224403..39a7a0d5 100755
--- a/examples/hooks.py
+++ b/examples/hooks.py
@@ -66,7 +66,7 @@ class CmdLineApp(cmd2.Cmd):
command_pattern = re.compile(r'^([^\s\d]+)(\d+)')
match = command_pattern.search(command)
if match:
- data.statement = self.statement_parser.parse("{} {} {}".format(
+ data.statement = self._statement_parser.parse("{} {} {}".format(
match.group(1),
match.group(2),
'' if data.statement.args is None else data.statement.args
@@ -76,7 +76,7 @@ class CmdLineApp(cmd2.Cmd):
def downcase_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
"""A hook to make uppercase commands lowercase."""
command = data.statement.command.lower()
- data.statement = self.statement_parser.parse("{} {}".format(
+ data.statement = self._statement_parser.parse("{} {}".format(
command,
'' if data.statement.args is None else data.statement.args
))
@@ -90,7 +90,7 @@ class CmdLineApp(cmd2.Cmd):
possible_cmds = [cmd for cmd in self.get_all_commands() if cmd.startswith(data.statement.command)]
if len(possible_cmds) == 1:
raw = data.statement.raw.replace(data.statement.command, possible_cmds[0], 1)
- data.statement = self.statement_parser.parse(raw)
+ data.statement = self._statement_parser.parse(raw)
return data
@cmd2.with_argument_list
diff --git a/examples/pirate.py b/examples/pirate.py
index 9abbe4e6..699ee80c 100755
--- a/examples/pirate.py
+++ b/examples/pirate.py
@@ -29,7 +29,7 @@ class Pirate(cmd2.Cmd):
"""A piratical example cmd2 application involving looting and drinking."""
def __init__(self):
"""Initialize the base class as well as this one"""
- shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'~': 'sing'})
super().__init__(multiline_commands=['sing'], terminators=[MULTILINE_TERMINATOR, '...'], shortcuts=shortcuts)
diff --git a/examples/plumbum_colors.py b/examples/plumbum_colors.py
index 774dc7e4..2c57c22b 100755
--- a/examples/plumbum_colors.py
+++ b/examples/plumbum_colors.py
@@ -66,7 +66,7 @@ class CmdLineApp(cmd2.Cmd):
MUMBLE_LAST = ['right?']
def __init__(self):
- shortcuts = dict(self.DEFAULT_SHORTCUTS)
+ shortcuts = dict(cmd2.DEFAULT_SHORTCUTS)
shortcuts.update({'&': 'speak'})
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
super().__init__(use_ipython=True, multiline_commands=['orate'], shortcuts=shortcuts)
diff --git a/examples/python_scripting.py b/examples/python_scripting.py
index 3e8f64ef..c45648bc 100755
--- a/examples/python_scripting.py
+++ b/examples/python_scripting.py
@@ -58,7 +58,7 @@ class CmdLineApp(cmd2.Cmd):
if not arglist or len(arglist) != 1:
self.perror("cd requires exactly 1 argument:", traceback_war=False)
self.do_help('cd')
- self._last_result = cmd2.CommandResult('', 'Bad arguments')
+ self.last_result = cmd2.CommandResult('', 'Bad arguments')
return
# Convert relative paths to absolute paths
@@ -84,7 +84,7 @@ class CmdLineApp(cmd2.Cmd):
if err:
self.perror(err, traceback_war=False)
- self._last_result = cmd2.CommandResult(out, err, data)
+ self.last_result = cmd2.CommandResult(out, err, data)
# Enable tab completion for cd command
def complete_cd(self, text, line, begidx, endidx):
@@ -100,7 +100,7 @@ class CmdLineApp(cmd2.Cmd):
if unknown:
self.perror("dir does not take any positional arguments:", traceback_war=False)
self.do_help('dir')
- self._last_result = cmd2.CommandResult('', 'Bad arguments')
+ self.last_result = cmd2.CommandResult('', 'Bad arguments')
return
# Get the contents as a list
@@ -113,7 +113,7 @@ class CmdLineApp(cmd2.Cmd):
self.stdout.write(fmt.format(f))
self.stdout.write('\n')
- self._last_result = cmd2.CommandResult(data=contents)
+ self.last_result = cmd2.CommandResult(data=contents)
if __name__ == '__main__':
diff --git a/examples/scripts/conditional.py b/examples/scripts/conditional.py
index 724bb3ee..2e307cb4 100644
--- a/examples/scripts/conditional.py
+++ b/examples/scripts/conditional.py
@@ -27,10 +27,10 @@ original_dir = os.getcwd()
app('cd {}'.format(directory))
# Conditionally do something based on the results of the last command
-if self._last_result:
+if self.last_result:
print('\nContents of directory {!r}:'.format(directory))
app('dir -l')
- print('{}\n'.format(self._last_result.data))
+ print('{}\n'.format(self.last_result.data))
# Change back to where we were
print('Changing back to original directory: {!r}'.format(original_dir))
diff --git a/tests/conftest.py b/tests/conftest.py
index d20d060a..b049dfff 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -4,7 +4,7 @@ Cmd2 unit/functional testing
"""
import sys
from contextlib import redirect_stdout, redirect_stderr
-from typing import Optional
+from typing import List, Optional, Union
from unittest import mock
from pytest import fixture
@@ -25,31 +25,23 @@ except ImportError:
except ImportError:
pass
-# Help text for base cmd2.Cmd application
-BASE_HELP = """Documented commands (type help <topic>):
-========================================
-alias help load py quit run_script shell
-edit history macro pyscript run_pyscript set shortcuts
-""" # noqa: W291
-
-BASE_HELP_VERBOSE = """
-Documented commands (type help <topic>):
-================================================================================
-alias Manage aliases
-edit Edit a file in a text editor
-help List available commands or provide detailed help for a specific command
-history View, run, edit, save, or clear previously entered commands
-load Run commands in script file that is encoded as either ASCII or UTF-8 text
-macro Manage macros
-py Invoke Python command or shell
-pyscript Run a Python script file inside the console
-quit Exit this application
-run_pyscript Run a Python script file inside the console
-run_script Run commands in script file that is encoded as either ASCII or UTF-8 text
-set Set a settable parameter or show current settings of parameters
-shell Execute a command as if at the OS prompt
-shortcuts List available shortcuts
-"""
+
+def verify_help_text(cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]]) -> None:
+ """This function verifies that all expected commands are present in the help text.
+
+ :param cmd2_app: instance of cmd2.Cmd
+ :param help_output: output of help, either as a string or list of strings
+ """
+ if isinstance(help_output, str):
+ help_text = help_output
+ else:
+ help_text = ''.join(help_output)
+ commands = cmd2_app.get_visible_commands()
+ for command in commands:
+ assert command in help_text
+
+ # TODO: Consider adding checks for categories and for verbose history
+
# Help text for the history command
HELP_HISTORY = """Usage: history [-h] [-r | -e | -o FILE | -t TRANSCRIPT | -c] [-s] [-x] [-v]
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 77542d76..9a5b2b47 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -22,8 +22,7 @@ except ImportError:
import cmd2
from cmd2 import clipboard, constants, utils
-from .conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \
- HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG
+from .conftest import run_cmd, normalize, verify_help_text, HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG
def CreateOutsimApp():
c = cmd2.Cmd()
@@ -53,13 +52,11 @@ def test_empty_statement(base_app):
def test_base_help(base_app):
out, err = run_cmd(base_app, 'help')
- expected = normalize(BASE_HELP)
- assert out == expected
+ verify_help_text(base_app, out)
def test_base_help_verbose(base_app):
out, err = run_cmd(base_app, 'help -v')
- expected = normalize(BASE_HELP_VERBOSE)
- assert out == expected
+ verify_help_text(base_app, out)
# Make sure :param type lines are filtered out of help summary
help_doc = base_app.do_help.__func__.__doc__
@@ -67,7 +64,8 @@ def test_base_help_verbose(base_app):
base_app.do_help.__func__.__doc__ = help_doc
out, err = run_cmd(base_app, 'help --verbose')
- assert out == expected
+ verify_help_text(base_app, out)
+ assert ':param' not in ''.join(out)
def test_base_argparse_help(base_app):
# Verify that "set -h" gives the same output as "help set" and that it starts in a way that makes sense
@@ -112,7 +110,7 @@ def test_base_show_readonly(base_app):
expected = normalize(SHOW_TXT + '\nRead only settings:' + """
Commands may be terminated with: {}
Output redirection and pipes allowed: {}
-""".format(base_app.statement_parser.terminators, base_app.allow_redirection))
+""".format(base_app._statement_parser.terminators, base_app.allow_redirection))
assert out == expected
@@ -427,18 +425,17 @@ def test_output_redirection(base_app):
try:
# Verify that writing to a file works
run_cmd(base_app, 'help > {}'.format(filename))
- expected = normalize(BASE_HELP)
with open(filename) as f:
- content = normalize(f.read())
- assert content == expected
+ content = f.read()
+ verify_help_text(base_app, content)
# Verify that appending to a file also works
run_cmd(base_app, 'help history >> {}'.format(filename))
- expected = normalize(BASE_HELP + '\n' + HELP_HISTORY)
with open(filename) as f:
- content = normalize(f.read())
- assert content == expected
- except:
+ appended_content = f.read()
+ assert appended_content.startswith(content)
+ assert len(appended_content) > len(content)
+ except Exception:
raise
finally:
os.remove(filename)
@@ -448,19 +445,18 @@ def test_output_redirection_to_nonexistent_directory(base_app):
# Verify that writing to a file in a non-existent directory doesn't work
run_cmd(base_app, 'help > {}'.format(filename))
- expected = normalize(BASE_HELP)
with pytest.raises(FileNotFoundError):
with open(filename) as f:
- content = normalize(f.read())
- assert content == expected
+ content = f.read()
+ verify_help_text(base_app, content)
# Verify that appending to a file also works
run_cmd(base_app, 'help history >> {}'.format(filename))
- expected = normalize(BASE_HELP + '\n' + HELP_HISTORY)
with pytest.raises(FileNotFoundError):
with open(filename) as f:
- content = normalize(f.read())
- assert content == expected
+ appended_content = f.read()
+ verify_help_text(base_app, appended_content)
+ assert len(appended_content) > len(content)
def test_output_redirection_to_too_long_filename(base_app):
filename = '~/sdkfhksdjfhkjdshfkjsdhfkjsdhfkjdshfkjdshfkjshdfkhdsfkjhewfuihewiufhweiufhiweufhiuewhiuewhfiuwehfia' \
@@ -471,19 +467,18 @@ def test_output_redirection_to_too_long_filename(base_app):
# Verify that writing to a file in a non-existent directory doesn't work
run_cmd(base_app, 'help > {}'.format(filename))
- expected = normalize(BASE_HELP)
with pytest.raises(OSError):
with open(filename) as f:
- content = normalize(f.read())
- assert content == expected
+ content = f.read()
+ verify_help_text(base_app, content)
# Verify that appending to a file also works
run_cmd(base_app, 'help history >> {}'.format(filename))
- expected = normalize(BASE_HELP + '\n' + HELP_HISTORY)
with pytest.raises(OSError):
with open(filename) as f:
- content = normalize(f.read())
- assert content == expected
+ appended_content = f.read()
+ verify_help_text(base_app, content)
+ assert len(appended_content) > len(content)
def test_feedback_to_output_true(base_app):
@@ -524,14 +519,13 @@ def test_feedback_to_output_false(base_app):
def test_disallow_redirection(base_app):
# Set allow_redirection to False
- base_app.statement_parser.allow_redirection = False
+ base_app._statement_parser.allow_redirection = False
filename = 'test_allow_redirect.txt'
# Verify output wasn't redirected
out, err = run_cmd(base_app, 'help > {}'.format(filename))
- expected = normalize(BASE_HELP)
- assert out == expected
+ verify_help_text(base_app, out)
# Verify that no file got created
assert not os.path.exists(filename)
@@ -574,13 +568,14 @@ def test_pipe_to_shell_error(base_app):
def test_send_to_paste_buffer(base_app):
# Test writing to the PasteBuffer/Clipboard
run_cmd(base_app, 'help >')
- expected = normalize(BASE_HELP)
- assert normalize(cmd2.cmd2.get_paste_buffer()) == expected
+ paste_contents = cmd2.cmd2.get_paste_buffer()
+ verify_help_text(base_app, paste_contents)
# Test appending to the PasteBuffer/Clipboard
run_cmd(base_app, 'help history >>')
- expected = normalize(BASE_HELP + '\n' + HELP_HISTORY)
- assert normalize(cmd2.cmd2.get_paste_buffer()) == expected
+ appended_contents = cmd2.cmd2.get_paste_buffer()
+ assert appended_contents.startswith(paste_contents)
+ assert len(appended_contents) > len(paste_contents)
def test_base_timing(base_app):
@@ -901,17 +896,7 @@ def test_custom_command_help(help_app):
def test_custom_help_menu(help_app):
out, err = run_cmd(help_app, 'help')
- expected = normalize("""
-Documented commands (type help <topic>):
-========================================
-alias help load py quit run_script shell squat
-edit history macro pyscript run_pyscript set shortcuts
-
-Undocumented commands:
-======================
-undoc
-""")
- assert out == expected
+ verify_help_text(help_app, out)
def test_help_undocumented(help_app):
out, err = run_cmd(help_app, 'help undoc')
@@ -962,62 +947,11 @@ def helpcat_app():
def test_help_cat_base(helpcat_app):
out, err = run_cmd(helpcat_app, 'help')
- expected = normalize("""Documented commands (type help <topic>):
-
-Custom Category
-===============
-edit squat
-
-Some Category
-=============
-cat_nodoc diddly
-
-Other
-=====
-alias history macro pyscript run_pyscript set shortcuts
-help load py quit run_script shell
-
-Undocumented commands:
-======================
-undoc
-""")
- assert out == expected
+ verify_help_text(helpcat_app, out)
def test_help_cat_verbose(helpcat_app):
out, err = run_cmd(helpcat_app, 'help --verbose')
- expected = normalize("""Documented commands (type help <topic>):
-
-Custom Category
-================================================================================
-edit This overrides the edit command and does nothing.
-squat This command does diddly squat...
-
-Some Category
-================================================================================
-cat_nodoc
-diddly This command does diddly
-
-Other
-================================================================================
-alias Manage aliases
-help List available commands or provide detailed help for a specific command
-history View, run, edit, save, or clear previously entered commands
-load Run commands in script file that is encoded as either ASCII or UTF-8 text
-macro Manage macros
-py Invoke Python command or shell
-pyscript Run a Python script file inside the console
-quit Exit this application
-run_pyscript Run a Python script file inside the console
-run_script Run commands in script file that is encoded as either ASCII or UTF-8 text
-set Set a settable parameter or show current settings of parameters
-shell Execute a command as if at the OS prompt
-shortcuts List available shortcuts
-
-Undocumented commands:
-======================
-undoc
-""")
- assert out == expected
+ verify_help_text(helpcat_app, out)
class SelectApp(cmd2.Cmd):
@@ -1291,7 +1225,7 @@ def test_multiline_input_line_to_statement(multiline_app):
def test_clipboard_failure(base_app, capsys):
# Force cmd2 clipboard to be disabled
- base_app.can_clip = False
+ base_app._can_clip = False
# Redirect command output to the clipboard when a clipboard isn't present
base_app.onecmd_plus_hooks('help > ')
@@ -1307,16 +1241,16 @@ class CommandResultApp(cmd2.Cmd):
super().__init__(*args, **kwargs)
def do_affirmative(self, arg):
- self._last_result = cmd2.CommandResult(arg, data=True)
+ self.last_result = cmd2.CommandResult(arg, data=True)
def do_negative(self, arg):
- self._last_result = cmd2.CommandResult(arg, data=False)
+ self.last_result = cmd2.CommandResult(arg, data=False)
def do_affirmative_no_data(self, arg):
- self._last_result = cmd2.CommandResult(arg)
+ self.last_result = cmd2.CommandResult(arg)
def do_negative_no_data(self, arg):
- self._last_result = cmd2.CommandResult('', arg)
+ self.last_result = cmd2.CommandResult('', arg)
@pytest.fixture
def commandresult_app():
@@ -1326,22 +1260,22 @@ def commandresult_app():
def test_commandresult_truthy(commandresult_app):
arg = 'foo'
run_cmd(commandresult_app, 'affirmative {}'.format(arg))
- assert commandresult_app._last_result
- assert commandresult_app._last_result == cmd2.CommandResult(arg, data=True)
+ assert commandresult_app.last_result
+ assert commandresult_app.last_result == cmd2.CommandResult(arg, data=True)
run_cmd(commandresult_app, 'affirmative_no_data {}'.format(arg))
- assert commandresult_app._last_result
- assert commandresult_app._last_result == cmd2.CommandResult(arg)
+ assert commandresult_app.last_result
+ assert commandresult_app.last_result == cmd2.CommandResult(arg)
def test_commandresult_falsy(commandresult_app):
arg = 'bar'
run_cmd(commandresult_app, 'negative {}'.format(arg))
- assert not commandresult_app._last_result
- assert commandresult_app._last_result == cmd2.CommandResult(arg, data=False)
+ assert not commandresult_app.last_result
+ assert commandresult_app.last_result == cmd2.CommandResult(arg, data=False)
run_cmd(commandresult_app, 'negative_no_data {}'.format(arg))
- assert not commandresult_app._last_result
- assert commandresult_app._last_result == cmd2.CommandResult('', arg)
+ assert not commandresult_app.last_result
+ assert commandresult_app.last_result == cmd2.CommandResult('', arg)
def test_is_text_file_bad_input(base_app):
@@ -1468,7 +1402,7 @@ def test_raw_input(base_app):
m = mock.Mock(name='input', return_value=fake_input)
builtins.input = m
- line = base_app.pseudo_raw_input('(cmd2)')
+ line = base_app._pseudo_raw_input('(cmd2)')
assert line == fake_input
def test_stdin_input():
@@ -1480,7 +1414,7 @@ def test_stdin_input():
m = mock.Mock(name='readline', return_value=fake_input)
app.stdin.readline = m
- line = app.pseudo_raw_input('(cmd2)')
+ line = app._pseudo_raw_input('(cmd2)')
assert line == fake_input
def test_empty_stdin_input():
@@ -1492,7 +1426,7 @@ def test_empty_stdin_input():
m = mock.Mock(name='readline', return_value=fake_input)
app.stdin.readline = m
- line = app.pseudo_raw_input('(cmd2)')
+ line = app._pseudo_raw_input('(cmd2)')
assert line == 'eof'
def test_poutput_string(outsim_app):
@@ -1560,17 +1494,17 @@ def test_get_alias_names(base_app):
run_cmd(base_app, 'alias create fake run_pyscript')
run_cmd(base_app, 'alias create ls !ls -hal')
assert len(base_app.aliases) == 2
- assert sorted(base_app.get_alias_names()) == ['fake', 'ls']
+ assert sorted(base_app._get_alias_names()) == ['fake', 'ls']
def test_get_macro_names(base_app):
assert len(base_app.macros) == 0
run_cmd(base_app, 'macro create foo !echo foo')
run_cmd(base_app, 'macro create bar !echo bar')
assert len(base_app.macros) == 2
- assert sorted(base_app.get_macro_names()) == ['bar', 'foo']
+ assert sorted(base_app._get_macro_names()) == ['bar', 'foo']
def test_get_settable_names(base_app):
- assert sorted(base_app.get_settable_names()) == sorted(base_app.settable.keys())
+ assert sorted(base_app._get_settable_names()) == sorted(base_app.settable.keys())
def test_alias_no_subcommand(base_app):
out, err = run_cmd(base_app, 'alias')
@@ -1656,12 +1590,10 @@ def test_multiple_aliases(base_app):
run_cmd(base_app, 'alias create {} help'.format(alias1))
run_cmd(base_app, 'alias create {} help -v'.format(alias2))
out, err = run_cmd(base_app, alias1)
- expected = normalize(BASE_HELP)
- assert out == expected
+ verify_help_text(base_app, out)
out, err = run_cmd(base_app, alias2)
- expected = normalize(BASE_HELP_VERBOSE)
- assert out == expected
+ verify_help_text(base_app, out)
def test_macro_no_subcommand(base_app):
out, err = run_cmd(base_app, 'macro')
@@ -1716,8 +1648,7 @@ def test_macro_create_with_args(base_app):
# Run the macro
out, err = run_cmd(base_app, 'fake help -v')
- expected = normalize(BASE_HELP_VERBOSE)
- assert out == expected
+ verify_help_text(base_app, out)
def test_macro_create_with_escaped_args(base_app):
# Create the macro
@@ -1810,12 +1741,11 @@ def test_multiple_macros(base_app):
run_cmd(base_app, 'macro create {} help'.format(macro1))
run_cmd(base_app, 'macro create {} help -v'.format(macro2))
out, err = run_cmd(base_app, macro1)
- expected = normalize(BASE_HELP)
- assert out == expected
+ verify_help_text(base_app, out)
- out, err = run_cmd(base_app, macro2)
- expected = normalize(BASE_HELP_VERBOSE)
- assert out == expected
+ out2, err2 = run_cmd(base_app, macro2)
+ verify_help_text(base_app, out2)
+ assert len(out2) > len(out)
def test_nonexistent_macro(base_app):
from cmd2.parsing import StatementParser
@@ -1840,7 +1770,7 @@ def test_ppaged_strips_color_when_redirecting(outsim_app):
msg = 'testing...'
end = '\n'
outsim_app.colors = cmd2.constants.COLORS_TERMINAL
- outsim_app.redirecting = True
+ outsim_app._redirecting = True
outsim_app.ppaged(Fore.RED + msg)
out = outsim_app.stdout.getvalue()
assert out == msg + end
@@ -1849,7 +1779,7 @@ def test_ppaged_strips_color_when_redirecting_if_always(outsim_app):
msg = 'testing...'
end = '\n'
outsim_app.colors = cmd2.constants.COLORS_ALWAYS
- outsim_app.redirecting = True
+ outsim_app._redirecting = True
outsim_app.ppaged(Fore.RED + msg)
out = outsim_app.stdout.getvalue()
assert out == Fore.RED + msg + end
@@ -1878,7 +1808,7 @@ def test_onecmd_raw_str_continue(outsim_app):
stop = outsim_app.onecmd(line)
out = outsim_app.stdout.getvalue()
assert not stop
- assert normalize(out) == normalize(BASE_HELP)
+ verify_help_text(outsim_app, out)
def test_onecmd_raw_str_quit(outsim_app):
line = "quit"
diff --git a/tests/test_completion.py b/tests/test_completion.py
index 91519b0a..9157ce84 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -375,7 +375,7 @@ def test_default_to_shell_completion(cmd2_app, request):
command = 'egrep'
# Make sure the command is on the testing system
- assert command in cmd2_app.get_exes_in_path(command)
+ assert command in utils.get_exes_in_path(command)
line = '{} {}'.format(command, text)
endidx = len(line)
@@ -695,7 +695,7 @@ def test_tokens_for_completion_quoted_redirect(cmd2_app):
endidx = len(line)
begidx = endidx - len(text)
- cmd2_app.statement_parser.redirection = True
+ cmd2_app._statement_parser.redirection = True
expected_tokens = ['command', '>file']
expected_raw_tokens = ['command', '">file']
@@ -709,7 +709,7 @@ def test_tokens_for_completion_redirect_off(cmd2_app):
endidx = len(line)
begidx = endidx - len(text)
- cmd2_app.statement_parser.allow_redirection = False
+ cmd2_app._statement_parser.allow_redirection = False
expected_tokens = ['command', '>file']
expected_raw_tokens = ['command', '>file']
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index 909a6a5c..1d930c26 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -14,7 +14,7 @@ from unittest import mock
import pytest
import cmd2
-from .conftest import run_cmd, BASE_HELP_VERBOSE
+from .conftest import run_cmd, verify_help_text
from cmd2 import transcript
from cmd2.utils import StdSim
@@ -211,9 +211,8 @@ def test_run_script_record_transcript(base_app, request):
with open(transcript_fname) as f:
xscript = f.read()
- expected = '(Cmd) help -v\n' + BASE_HELP_VERBOSE + '\n'
-
- assert xscript == expected
+ assert xscript.startswith('(Cmd) help -v\n')
+ verify_help_text(base_app, xscript)
def test_generate_transcript_stop(capsys):
diff --git a/tests/test_utils.py b/tests/test_utils.py
index b43eb10c..44421b93 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -276,3 +276,26 @@ def test_context_flag_bool(context_flag):
def test_context_flag_exit_err(context_flag):
with pytest.raises(ValueError):
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
+
diff --git a/tests/transcripts/from_cmdloop.txt b/tests/transcripts/from_cmdloop.txt
index 76056fc6..aede6659 100644
--- a/tests/transcripts/from_cmdloop.txt
+++ b/tests/transcripts/from_cmdloop.txt
@@ -1,14 +1,6 @@
# responses with trailing spaces have been matched with a regex
# so you can see where they are.
-(Cmd) help
-
-Documented commands (type help <topic>):
-========================================
-alias history mumble py run_pyscript set speak/ */
-edit load nothing pyscript run_script shell/ */
-help macro orate quit say shortcuts/ */
-
(Cmd) help say
usage: speak [-h] [-p] [-s] [-r REPEAT]/ */
@@ -36,13 +28,12 @@ OODNIGHT, GRACIEGAY
OODNIGHT, GRACIEGAY
OODNIGHT, GRACIEGAY
(Cmd) history
- 1 help
- 2 help say
- 3 say goodnight, Gracie
- 4 say -ps --repeat=5 goodnight, Gracie
- 5 set maxrepeats 5
- 6 say -ps --repeat=5 goodnight, Gracie
-(Cmd) history -r 4
+ 1 help say
+ 2 say goodnight, Gracie
+ 3 say -ps --repeat=5 goodnight, Gracie
+ 4 set maxrepeats 5
+ 5 say -ps --repeat=5 goodnight, Gracie
+(Cmd) history -r 3
OODNIGHT, GRACIEGAY
OODNIGHT, GRACIEGAY
OODNIGHT, GRACIEGAY