diff options
author | Todd Leonhardt <todd.leonhardt@gmail.com> | 2019-06-23 21:11:01 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-06-23 21:11:01 -0400 |
commit | bef07746e33da9def33d814913891384a545a95c (patch) | |
tree | 86b162f79663f70cbe88e64deb4cecb93106ba68 | |
parent | c12ba0ff11b3a8fd083c641cb9149aff6494bbf9 (diff) | |
parent | eb1936e568a2ca4817ab0cd640220a5bc355e226 (diff) | |
download | cmd2-git-bef07746e33da9def33d814913891384a545a95c.tar.gz |
Merge pull request #703 from python-cmd2/public_api
Minimize public API of cmd2.Cmd class
-rw-r--r-- | CHANGELOG.md | 13 | ||||
-rwxr-xr-x | README.md | 2 | ||||
-rw-r--r-- | cmd2/__init__.py | 1 | ||||
-rw-r--r-- | cmd2/cmd2.py | 300 | ||||
-rw-r--r-- | cmd2/constants.py | 2 | ||||
-rw-r--r-- | cmd2/parsing.py | 2 | ||||
-rw-r--r-- | cmd2/pyscript_bridge.py | 6 | ||||
-rw-r--r-- | cmd2/transcript.py | 30 | ||||
-rw-r--r-- | cmd2/utils.py | 45 | ||||
-rw-r--r-- | docs/argument_processing.rst | 2 | ||||
-rw-r--r-- | docs/settingchanges.rst | 2 | ||||
-rwxr-xr-x | examples/arg_print.py | 2 | ||||
-rwxr-xr-x | examples/cmd_as_argument.py | 2 | ||||
-rwxr-xr-x | examples/colors.py | 2 | ||||
-rwxr-xr-x | examples/decorator_example.py | 2 | ||||
-rwxr-xr-x | examples/example.py | 2 | ||||
-rwxr-xr-x | examples/hooks.py | 6 | ||||
-rwxr-xr-x | examples/pirate.py | 2 | ||||
-rwxr-xr-x | examples/plumbum_colors.py | 2 | ||||
-rwxr-xr-x | examples/python_scripting.py | 8 | ||||
-rw-r--r-- | examples/scripts/conditional.py | 4 | ||||
-rw-r--r-- | tests/conftest.py | 44 | ||||
-rw-r--r-- | tests/test_cmd2.py | 192 | ||||
-rw-r--r-- | tests/test_completion.py | 6 | ||||
-rw-r--r-- | tests/test_transcript.py | 7 | ||||
-rw-r--r-- | tests/test_utils.py | 23 | ||||
-rw-r--r-- | tests/transcripts/from_cmdloop.txt | 21 |
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 @@ -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 |