Hello community, here is the log from the commit of package python-cmd2 for openSUSE:Factory checked in at 2019-09-23 12:09:41 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-cmd2 (Old) and /work/SRC/openSUSE:Factory/.python-cmd2.new.7948 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-cmd2" Mon Sep 23 12:09:41 2019 rev:27 rq:730677 version:0.9.16 Changes: -------- --- /work/SRC/openSUSE:Factory/python-cmd2/python-cmd2.changes 2019-08-07 13:56:46.420855928 +0200 +++ /work/SRC/openSUSE:Factory/.python-cmd2.new.7948/python-cmd2.changes 2019-09-23 12:09:44.109882338 +0200 @@ -1,0 +2,8 @@ +Fri Sep 13 10:57:37 UTC 2019 - Tomáš Chvátal <tchva...@suse.com> + +- Update to 0.9.16: + * Fixed inconsistent parsing/tab completion behavio + * Create directory for the persistent history file if it does not already exist + * Aliases and macros can no longer have the same name as a command + +------------------------------------------------------------------- Old: ---- cmd2-0.9.15.tar.gz New: ---- cmd2-0.9.16.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-cmd2.spec ++++++ --- /var/tmp/diff_new_pack.L8fhY9/_old 2019-09-23 12:09:45.257882148 +0200 +++ /var/tmp/diff_new_pack.L8fhY9/_new 2019-09-23 12:09:45.261882148 +0200 @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 Name: python-cmd2 -Version: 0.9.15 +Version: 0.9.16 Release: 0 Summary: Extra features for standard library's cmd module License: MIT @@ -35,11 +35,11 @@ Requires: python-colorama >= 0.3.7 Requires: python-pyperclip >= 1.6 Requires: python-wcwidth >= 0.1.7 -%if %python3_version_nodots < 35 +BuildArch: noarch +%if %{python3_version_nodots} < 35 Requires: python-contextlib2 Requires: python-typing %endif -BuildArch: noarch # SECTION Test requirements BuildRequires: %{python_module attrs >= 16.3.0} BuildRequires: %{python_module colorama >= 0.3.7} ++++++ cmd2-0.9.15.tar.gz -> cmd2-0.9.16.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cmd2-0.9.15/CHANGELOG.md new/cmd2-0.9.16/CHANGELOG.md --- old/cmd2-0.9.15/CHANGELOG.md 2019-07-25 03:46:19.000000000 +0200 +++ new/cmd2-0.9.16/CHANGELOG.md 2019-08-08 03:55:37.000000000 +0200 @@ -1,3 +1,16 @@ +## 0.9.16 (August 7, 2019) +* Bug Fixes + * Fixed inconsistent parsing/tab completion behavior based on the value of `allow_redirection`. This flag is + only meant to be a security setting that prevents redirection of stdout and should not alter parsing logic. +* Enhancements + * Raise `TypeError` if trying to set choices/completions on argparse action that accepts no arguments + * Create directory for the persistent history file if it does not already exist + * Added `set_choices_function()`, `set_choices_method()`, `set_completer_function()`, and `set_completer_method()` + to support cases where this functionality needs to be added to an argparse action outside of the normal + `parser.add_argument()` call. +* Breaking Changes + * Aliases and macros can no longer have the same name as a command + ## 0.9.15 (July 24, 2019) * Bug Fixes * Fixed exception caused by tab completing after an invalid subcommand was entered diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cmd2-0.9.15/PKG-INFO new/cmd2-0.9.16/PKG-INFO --- old/cmd2-0.9.15/PKG-INFO 2019-07-25 04:11:23.000000000 +0200 +++ new/cmd2-0.9.16/PKG-INFO 2019-08-08 04:00:03.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: cmd2 -Version: 0.9.15 +Version: 0.9.16 Summary: cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python Home-page: https://github.com/python-cmd2/cmd2 Author: Catherine Devlin @@ -196,7 +196,7 @@ Any command accepts multi-line input when its name is listed the `multiline_commands` optional argument to `cmd2.Cmd.__init`. The program will keep expecting input until a line ends with any of the characters listed in the - `terminators` optional argument to `cmd2.Cmd.__init__()` . The default terminators are `;` and `/n` (empty newline). + `terminators` optional argument to `cmd2.Cmd.__init__()` . The default terminators are `;` and `\n` (empty newline). - Special-character shortcut commands (beyond cmd's "@" and "!") @@ -216,11 +216,10 @@ Tutorials --------- - A few tutorials on using cmd2 exist: - - * Florida PyCon 2017 talk: [slides](https://docs.google.com/presentation/d/1LRmpfBt3V-pYQfgQHdczf16F3hcXmhK83tl77R6IJtE), [video](https://www.youtube.com/watch?v=6m0RdpITaeY) - * PyCon 2010 talk by Catherine Devlin, the original author: [video](http://pyvideo.org/pycon-us-2010/pycon-2010--easy-command-line-applications-with-c.html) - * A nice brief step-by-step tutorial: [blog](https://kushaldas.in/posts/developing-command-line-interpreters-using-python-cmd2.html) + * PyOhio 2019 presentation: + * [video](https://www.youtube.com/watch?v=pebeWrTqIIw) + * [slides](https://github.com/python-cmd2/talks/blob/master/PyOhio_2019/cmd2-PyOhio_2019.pdf) + * [example code](https://github.com/python-cmd2/talks/tree/master/PyOhio_2019/examples) Example Application @@ -400,5 +399,5 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.5 Description-Content-Type: text/markdown -Provides-Extra: test Provides-Extra: dev +Provides-Extra: test diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cmd2-0.9.15/README.md new/cmd2-0.9.16/README.md --- old/cmd2-0.9.15/README.md 2019-07-20 01:17:59.000000000 +0200 +++ new/cmd2-0.9.16/README.md 2019-08-03 19:14:02.000000000 +0200 @@ -188,7 +188,7 @@ Any command accepts multi-line input when its name is listed the `multiline_commands` optional argument to `cmd2.Cmd.__init`. The program will keep expecting input until a line ends with any of the characters listed in the - `terminators` optional argument to `cmd2.Cmd.__init__()` . The default terminators are `;` and `/n` (empty newline). + `terminators` optional argument to `cmd2.Cmd.__init__()` . The default terminators are `;` and `\n` (empty newline). - Special-character shortcut commands (beyond cmd's "@" and "!") @@ -208,11 +208,10 @@ Tutorials --------- -A few tutorials on using cmd2 exist: - -* Florida PyCon 2017 talk: [slides](https://docs.google.com/presentation/d/1LRmpfBt3V-pYQfgQHdczf16F3hcXmhK83tl77R6IJtE), [video](https://www.youtube.com/watch?v=6m0RdpITaeY) -* PyCon 2010 talk by Catherine Devlin, the original author: [video](http://pyvideo.org/pycon-us-2010/pycon-2010--easy-command-line-applications-with-c.html) -* A nice brief step-by-step tutorial: [blog](https://kushaldas.in/posts/developing-command-line-interpreters-using-python-cmd2.html) +* PyOhio 2019 presentation: + * [video](https://www.youtube.com/watch?v=pebeWrTqIIw) + * [slides](https://github.com/python-cmd2/talks/blob/master/PyOhio_2019/cmd2-PyOhio_2019.pdf) + * [example code](https://github.com/python-cmd2/talks/tree/master/PyOhio_2019/examples) Example Application diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cmd2-0.9.15/cmd2/argparse_custom.py new/cmd2-0.9.16/cmd2/argparse_custom.py --- old/cmd2-0.9.15/cmd2/argparse_custom.py 2019-07-23 01:40:19.000000000 +0200 +++ new/cmd2-0.9.16/cmd2/argparse_custom.py 2019-08-06 04:42:05.000000000 +0200 @@ -94,6 +94,14 @@ as dynamic. Therefore it is up to the developer to validate if the user has typed an acceptable value for these arguments. + The following functions exist in cases where you may want to manually add choice providing function/methods to + an existing argparse action. For instance, in __init__() of a custom action class. + + set_choices_function(action, func) + set_choices_method(action, method) + set_completer_function(action, func) + set_completer_method(action, method) + CompletionItem Class: This class was added to help in cases where uninformative data is being tab completed. For instance, tab completing ID numbers isn't very helpful to a user without context. Returning a list of CompletionItems @@ -223,6 +231,9 @@ self.description = desc +############################################################################################################ +# Class and functions related to ChoicesCallable +############################################################################################################ class ChoicesCallable: """ Enables using a callable as the choices provider for an argparse argument. @@ -241,6 +252,48 @@ self.to_call = to_call +def _set_choices_callable(action: argparse.Action, choices_callable: ChoicesCallable) -> None: + """ + Set the choices_callable attribute of an argparse Action + :param action: action being edited + :param choices_callable: the ChoicesCallable instance to use + :raises: TypeError if used on incompatible action type + """ + # Verify consistent use of parameters + if action.choices is not None: + err_msg = ("None of the following parameters can be used alongside a choices parameter:\n" + "choices_function, choices_method, completer_function, completer_method") + raise (TypeError(err_msg)) + elif action.nargs == 0: + err_msg = ("None of the following parameters can be used on an action that takes no arguments:\n" + "choices_function, choices_method, completer_function, completer_method") + raise (TypeError(err_msg)) + + setattr(action, ATTR_CHOICES_CALLABLE, choices_callable) + + +def set_choices_function(action: argparse.Action, choices_function: Callable[[], Iterable[Any]]) -> None: + """Set choices_function on an argparse action""" + _set_choices_callable(action, ChoicesCallable(is_method=False, is_completer=False, to_call=choices_function)) + + +def set_choices_method(action: argparse.Action, choices_method: Callable[[Any], Iterable[Any]]) -> None: + """Set choices_method on an argparse action""" + _set_choices_callable(action, ChoicesCallable(is_method=True, is_completer=False, to_call=choices_method)) + + +def set_completer_function(action: argparse.Action, + completer_function: Callable[[str, str, int, int], List[str]]) -> None: + """Set completer_function on an argparse action""" + _set_choices_callable(action, ChoicesCallable(is_method=False, is_completer=True, to_call=completer_function)) + + +def set_completer_method(action: argparse.Action, + completer_method: Callable[[Any, str, str, int, int], List[str]]) -> None: + """Set completer_method on an argparse action""" + _set_choices_callable(action, ChoicesCallable(is_method=True, is_completer=True, to_call=completer_method)) + + ############################################################################################################ # Patch _ActionsContainer.add_argument with our wrapper to support more arguments ############################################################################################################ @@ -291,7 +344,17 @@ See the header of this file for more information :return: the created argument action + :raises ValueError on incorrect parameter usage """ + # Verify consistent use of arguments + choices_callables = [choices_function, choices_method, completer_function, completer_method] + num_params_set = len(choices_callables) - choices_callables.count(None) + + if num_params_set > 1: + err_msg = ("Only one of the following parameters may be used at a time:\n" + "choices_function, choices_method, completer_function, completer_method") + raise (ValueError(err_msg)) + # Pre-process special ranged nargs nargs_range = None @@ -345,30 +408,17 @@ # Create the argument using the original add_argument function new_arg = orig_actions_container_add_argument(self, *args, **kwargs) - # Verify consistent use of arguments - choice_params = [new_arg.choices, choices_function, choices_method, completer_function, completer_method] - num_set = len(choice_params) - choice_params.count(None) - - if num_set > 1: - err_msg = ("Only one of the following may be used in an argparser argument at a time:\n" - "choices, choices_function, choices_method, completer_function, completer_method") - raise (ValueError(err_msg)) - # Set the custom attributes setattr(new_arg, ATTR_NARGS_RANGE, nargs_range) if choices_function: - setattr(new_arg, ATTR_CHOICES_CALLABLE, - ChoicesCallable(is_method=False, is_completer=False, to_call=choices_function)) + set_choices_function(new_arg, choices_function) elif choices_method: - setattr(new_arg, ATTR_CHOICES_CALLABLE, - ChoicesCallable(is_method=True, is_completer=False, to_call=choices_method)) + set_choices_method(new_arg, choices_method) elif completer_function: - setattr(new_arg, ATTR_CHOICES_CALLABLE, - ChoicesCallable(is_method=False, is_completer=True, to_call=completer_function)) + set_completer_function(new_arg, completer_function) elif completer_method: - setattr(new_arg, ATTR_CHOICES_CALLABLE, - ChoicesCallable(is_method=True, is_completer=True, to_call=completer_method)) + set_completer_method(new_arg, completer_method) setattr(new_arg, ATTR_SUPPRESS_TAB_HINT, suppress_tab_hint) setattr(new_arg, ATTR_DESCRIPTIVE_COMPLETION_HEADER, descriptive_header) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cmd2-0.9.15/cmd2/cmd2.py new/cmd2-0.9.16/cmd2/cmd2.py --- old/cmd2-0.9.15/cmd2/cmd2.py 2019-07-25 00:50:26.000000000 +0200 +++ new/cmd2-0.9.16/cmd2/cmd2.py 2019-08-08 02:38:15.000000000 +0200 @@ -353,9 +353,15 @@ commands to be run or, if -t is specified, transcript files to run. This should be set to False if your application parses its own arguments. :param transcript_files: allow running transcript tests when allow_cli_args is False - :param allow_redirection: should output redirection and pipes be allowed + :param allow_redirection: should output redirection and pipes be allowed. this is only a security setting + and does not alter parsing behavior. :param multiline_commands: list of commands allowed to accept multi-line input - :param shortcuts: dictionary containing shortcuts for commands + :param terminators: list of characters that terminate a command. These are mainly intended for terminating + multiline commands, but will also terminate single-line commands. If not supplied, then + defaults to semicolon. If your app only contains single-line commands and you want + terminators to be treated as literals by the parser, then set this to an empty list. + :param shortcuts: dictionary containing shortcuts for commands. If not supplied, then defaults to + constants.DEFAULT_SHORTCUTS. """ # If use_ipython is False, make sure the do_ipy() method doesn't exit if not use_ipython: @@ -371,11 +377,14 @@ # Call super class constructor super().__init__(completekey=completekey, stdin=stdin, stdout=stdout) - # Attributes which should NOT be dynamically settable at runtime + # Attributes which should NOT be dynamically settable via the set command at runtime + # To prevent a user from altering these with the py/ipy commands, remove locals_in_py from the + # settable dictionary during your applications's __init__ method. self.default_to_shell = False # Attempt to run unrecognized commands as shell commands self.quit_on_sigint = False # Quit the loop on interrupt instead of just resetting prompt + self.allow_redirection = allow_redirection # Security setting to prevent redirection of stdout - # Attributes which ARE dynamically settable at runtime + # Attributes which ARE dynamically settable via the set command at runtime self.continuation_prompt = '> ' self.debug = False self.echo = False @@ -435,11 +444,16 @@ # True if running inside a Python script or interactive console, False otherwise self._in_py = False - self.statement_parser = StatementParser(allow_redirection=allow_redirection, - terminators=terminators, + self.statement_parser = StatementParser(terminators=terminators, multiline_commands=multiline_commands, shortcuts=shortcuts) + # Verify commands don't have invalid names (like starting with a shortcut) + for cur_cmd in self.get_all_commands(): + valid, errmsg = self.statement_parser.is_valid_command(cur_cmd) + if not valid: + raise ValueError("Invalid command name {!r}: {}".format(cur_cmd, errmsg)) + # 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 @@ -611,16 +625,6 @@ """Read-only property to access the aliases stored in the StatementParser.""" 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 - - @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 - def poutput(self, msg: Any, *, end: str = '\n') -> None: """Print message to self.stdout and appends a newline by default @@ -686,7 +690,7 @@ final_msg = ansi.style_error(final_msg) if not self.debug: - warning = "\nTo enable full traceback, run the following command: 'set debug true'" + warning = "\nTo enable full traceback, run the following command: 'set debug true'" final_msg += ansi.style_warning(warning) # Set apply_style to False since style has already been applied @@ -826,61 +830,8 @@ # Return empty lists since this means the line is malformed. return [], [] - if self.allow_redirection: - - # Since redirection is enabled, we need to treat redirection characters (|, <, >) - # as word breaks when they are in unquoted strings. Go through each token - # and further split them on these characters. Each run of redirect characters - # is treated as a single token. - raw_tokens = [] - - for cur_initial_token in initial_tokens: - - # Save tokens up to 1 character in length or quoted tokens. No need to parse these. - if len(cur_initial_token) <= 1 or cur_initial_token[0] in constants.QUOTES: - raw_tokens.append(cur_initial_token) - continue - - # Iterate over each character in this token - cur_index = 0 - cur_char = cur_initial_token[cur_index] - - # Keep track of the token we are building - cur_raw_token = '' - - while True: - if cur_char not in constants.REDIRECTION_CHARS: - - # Keep appending to cur_raw_token until we hit a redirect char - while cur_char not in constants.REDIRECTION_CHARS: - cur_raw_token += cur_char - cur_index += 1 - if cur_index < len(cur_initial_token): - cur_char = cur_initial_token[cur_index] - else: - break - - else: - redirect_char = cur_char - - # Keep appending to cur_raw_token until we hit something other than redirect_char - while cur_char == redirect_char: - cur_raw_token += cur_char - cur_index += 1 - if cur_index < len(cur_initial_token): - cur_char = cur_initial_token[cur_index] - else: - break - - # Save the current token - raw_tokens.append(cur_raw_token) - cur_raw_token = '' - - # Check if we've viewed all characters - if cur_index >= len(cur_initial_token): - break - else: - raw_tokens = initial_tokens + # Further split tokens on punctuation characters + raw_tokens = self.statement_parser.split_on_punctuation(initial_tokens) # Save the unquoted tokens tokens = [utils.strip_quotes(cur_token) for cur_token in raw_tokens] @@ -1223,72 +1174,70 @@ this will be called if we aren't completing for redirection :return: a list of possible tab completions """ - if self.allow_redirection: - - # Get all tokens through the one being completed. We want the raw tokens - # so we can tell if redirection strings are quoted and ignore them. - _, raw_tokens = self.tokens_for_completion(line, begidx, endidx) - if not raw_tokens: - return [] + # Get all tokens through the one being completed. We want the raw tokens + # so we can tell if redirection strings are quoted and ignore them. + _, raw_tokens = self.tokens_for_completion(line, begidx, endidx) + if not raw_tokens: + return [] - # Must at least have the command - if len(raw_tokens) > 1: + # Must at least have the command + if len(raw_tokens) > 1: - # True when command line contains any redirection tokens - has_redirection = False + # True when command line contains any redirection tokens + has_redirection = False - # Keep track of state while examining tokens - in_pipe = False - in_file_redir = False - do_shell_completion = False - do_path_completion = False - prior_token = None - - for cur_token in raw_tokens: - # Process redirection tokens - if cur_token in constants.REDIRECTION_TOKENS: - has_redirection = True - - # Check if we are at a pipe - if cur_token == constants.REDIRECTION_PIPE: - # Do not complete bad syntax (e.g cmd | |) - if prior_token == constants.REDIRECTION_PIPE: - return [] + # Keep track of state while examining tokens + in_pipe = False + in_file_redir = False + do_shell_completion = False + do_path_completion = False + prior_token = None + + for cur_token in raw_tokens: + # Process redirection tokens + if cur_token in constants.REDIRECTION_TOKENS: + has_redirection = True + + # Check if we are at a pipe + if cur_token == constants.REDIRECTION_PIPE: + # Do not complete bad syntax (e.g cmd | |) + if prior_token == constants.REDIRECTION_PIPE: + return [] - in_pipe = True - in_file_redir = False + in_pipe = True + in_file_redir = False - # Otherwise this is a file redirection token - else: - if prior_token in constants.REDIRECTION_TOKENS or in_file_redir: - # Do not complete bad syntax (e.g cmd | >) (e.g cmd > blah >) - return [] + # Otherwise this is a file redirection token + else: + if prior_token in constants.REDIRECTION_TOKENS or in_file_redir: + # Do not complete bad syntax (e.g cmd | >) (e.g cmd > blah >) + return [] - in_pipe = False - in_file_redir = True + in_pipe = False + in_file_redir = True - # Not a redirection token - else: - do_shell_completion = False - do_path_completion = False + # Not a redirection token + else: + do_shell_completion = False + do_path_completion = False - if prior_token == constants.REDIRECTION_PIPE: - do_shell_completion = True - elif in_pipe or prior_token in (constants.REDIRECTION_OUTPUT, constants.REDIRECTION_APPEND): - do_path_completion = True + if prior_token == constants.REDIRECTION_PIPE: + do_shell_completion = True + elif in_pipe or prior_token in (constants.REDIRECTION_OUTPUT, constants.REDIRECTION_APPEND): + do_path_completion = True - prior_token = cur_token + prior_token = cur_token - if do_shell_completion: - return self.shell_cmd_complete(text, line, begidx, endidx) + if do_shell_completion: + return self.shell_cmd_complete(text, line, begidx, endidx) - elif do_path_completion: - return self.path_complete(text, line, begidx, endidx) + elif do_path_completion: + return self.path_complete(text, line, begidx, endidx) - # If there were redirection strings anywhere on the command line, then we - # are no longer tab completing for the current command - elif has_redirection: - return [] + # If there were redirection strings anywhere on the command line, then we + # are no longer tab completing for the current command + elif has_redirection: + return [] # Call the command's completer function return compfunc(text, line, begidx, endidx) @@ -1729,13 +1678,10 @@ statement = self.statement_parser.parse_command_only(line) return statement.command, statement.args, statement.command_and_args - def onecmd_plus_hooks(self, line: str, *, expand: bool = True, add_to_history: bool = True, - py_bridge_call: bool = False) -> bool: + def onecmd_plus_hooks(self, line: str, *, add_to_history: bool = True, py_bridge_call: bool = False) -> bool: """Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks. :param line: command line to run - :param expand: If True, then aliases, macros, and shortcuts will be expanded. - Set this to False if the command token should not be altered. Defaults to True. :param add_to_history: If True, then add this command to history. Defaults to True. :param py_bridge_call: This should only ever be set to True by PyBridge to signify the beginning of an app() call from Python. It is used to enable/disable the storage of the @@ -1746,7 +1692,7 @@ stop = False try: - statement = self._input_line_to_statement(line, expand=expand) + statement = self._input_line_to_statement(line) except EmptyStatement: return self._run_cmdfinalization_hooks(stop, None) except ValueError as ex: @@ -1861,15 +1807,12 @@ except Exception as ex: self.pexcept(ex) - def runcmds_plus_hooks(self, cmds: List[Union[HistoryItem, str]], *, - expand: bool = True, add_to_history: bool = True) -> bool: + def runcmds_plus_hooks(self, cmds: List[Union[HistoryItem, str]], *, add_to_history: bool = True) -> bool: """ Used when commands are being run in an automated fashion like text scripts or history replays. The prompt and command line for each command will be printed if echo is True. :param cmds: commands to run - :param expand: If True, then aliases, macros, and shortcuts will be expanded. - Set this to False if the command token should not be altered. Defaults to True. :param add_to_history: If True, then add these commands to history. Defaults to True. :return: True if running of commands should stop """ @@ -1880,12 +1823,12 @@ if self.echo: self.poutput('{}{}'.format(self.prompt, line)) - if self.onecmd_plus_hooks(line, expand=expand, add_to_history=add_to_history): + if self.onecmd_plus_hooks(line, add_to_history=add_to_history): return True return False - def _complete_statement(self, line: str, *, expand: bool = True) -> Statement: + def _complete_statement(self, line: str) -> Statement: """Keep accepting lines of input until the command is complete. There is some pretty hacky code here to handle some quirks of @@ -1894,13 +1837,11 @@ backwards compatibility with the standard library version of cmd. :param line: the line being parsed - :param expand: If True, then aliases and shortcuts will be expanded. - Set this to False if the command token should not be altered. Defaults to True. :return: the completed Statement """ while True: try: - statement = self.statement_parser.parse(line, expand=expand) + statement = self.statement_parser.parse(line) if statement.multiline_command and statement.terminator: # we have a completed multiline command, we are done break @@ -1911,7 +1852,7 @@ 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, expand=expand) + statement = self.statement_parser.parse_command_only(line) if not statement.multiline_command: # not a multiline command, so raise the exception raise @@ -1948,13 +1889,11 @@ raise EmptyStatement() return statement - def _input_line_to_statement(self, line: str, *, expand: bool = True) -> Statement: + def _input_line_to_statement(self, line: str) -> Statement: """ Parse the user's input line and convert it to a Statement, ensuring that all macros are also resolved :param line: the line being parsed - :param expand: If True, then aliases, macros, and shortcuts will be expanded. - Set this to False if the command token should not be altered. Defaults to True. :return: parsed command line as a Statement """ used_macros = [] @@ -1963,14 +1902,14 @@ # Continue until all macros are resolved while True: # Make sure all input has been read and convert it to a Statement - statement = self._complete_statement(line, expand=expand) + statement = self._complete_statement(line) # Save the fully entered line if this is the first loop iteration if orig_line is None: orig_line = statement.raw # Check if this command matches a macro and wasn't already processed to avoid an infinite loop - if expand and statement.command in self.macros.keys() and statement.command not in used_macros: + if statement.command in self.macros.keys() and statement.command not in used_macros: used_macros.append(statement.command) line = self._resolve_macro(statement) if line is None: @@ -2184,22 +2123,19 @@ return target if callable(getattr(self, target, None)) else '' # noinspection PyMethodOverriding - def onecmd(self, statement: Union[Statement, str], *, - expand: bool = True, add_to_history: bool = True) -> bool: + def onecmd(self, statement: Union[Statement, str], *, add_to_history: bool = True) -> bool: """ This executes the actual do_* method for a command. If the command provided doesn't exist, then it executes default() instead. :param statement: intended to be a Statement instance parsed command from the input stream, alternative acceptance of a str is present only for backward compatibility with cmd - :param expand: If True, then aliases, macros, and shortcuts will be expanded. - Set this to False if the command token should not be altered. Defaults to True. :param add_to_history: If True, then add this command to history. Defaults to True. :return: a flag indicating whether the interpretation of commands should stop """ # For backwards compatibility with cmd, allow a str to be passed in if not isinstance(statement, Statement): - statement = self._input_line_to_statement(statement, expand=expand) + statement = self._input_line_to_statement(statement) func = self.cmd_func(statement.command) if func: @@ -2308,12 +2244,11 @@ readline_settings.completer = readline.get_completer() readline.set_completer(self.complete) - # Break words on whitespace and quotes when tab completing - completer_delims = " \t\n" + ''.join(constants.QUOTES) - - if self.allow_redirection: - # If redirection is allowed, then break words on those characters too - completer_delims += ''.join(constants.REDIRECTION_CHARS) + # Set the readline word delimiters for completion + completer_delims = " \t\n" + completer_delims += ''.join(constants.QUOTES) + completer_delims += ''.join(constants.REDIRECTION_CHARS) + completer_delims += ''.join(self.statement_parser.terminators) readline_settings.delims = readline.get_completer_delims() readline.set_completer_delims(completer_delims) @@ -2389,6 +2324,10 @@ self.perror("Invalid alias name: {}".format(errmsg)) return + if args.name in self.get_all_commands(): + self.perror("Alias cannot have the same name as a command") + return + if args.name in self.macros: self.perror("Alias cannot have the same name as a macro") return @@ -2451,8 +2390,8 @@ alias_create_description = "Create or overwrite an alias" alias_create_epilog = ("Notes:\n" - " If you want to use redirection, pipes, or terminators like ';' in the value\n" - " of the alias, then quote them.\n" + " If you want to use redirection, pipes, or terminators in the value of the\n" + " alias, then quote them.\n" "\n" " Since aliases are resolved during parsing, tab completion will function as\n" " it would for the actual command the alias resolves to.\n" @@ -2518,8 +2457,8 @@ self.perror("Invalid macro name: {}".format(errmsg)) return - if args.name in self.statement_parser.multiline_commands: - self.perror("Macro cannot have the same name as a multiline command") + if args.name in self.get_all_commands(): + self.perror("Macro cannot have the same name as a command") return if args.name in self.aliases: @@ -2657,8 +2596,8 @@ "\n" " macro create backup !cp \"{1}\" \"{1}.orig\"\n" "\n" - " If you want to use redirection, pipes, or terminators like ';' in the value\n" - " of the macro, then quote them.\n" + " If you want to use redirection, pipes, or terminators in the value of the\n" + " macro, then quote them.\n" "\n" " macro create show_results print_results -type {1} \"|\" less\n" "\n" @@ -2957,6 +2896,28 @@ | a list of tuples -> interpreted as (value, text), so that the return value can differ from the text advertised to the user """ + + completion_disabled = False + orig_completer = None + + def disable_completion(): + """Turn off completion during the select input line""" + nonlocal orig_completer + nonlocal completion_disabled + + if rl_type != RlType.NONE and not completion_disabled: + orig_completer = readline.get_completer() + readline.set_completer(lambda *args, **kwargs: None) + completion_disabled = True + + def enable_completion(): + """Restore tab completion when select is done reading input""" + nonlocal completion_disabled + + if rl_type != RlType.NONE and completion_disabled: + readline.set_completer(orig_completer) + completion_disabled = False + local_opts = opts if isinstance(opts, str): local_opts = list(zip(opts.split(), opts.split())) @@ -2971,15 +2932,28 @@ fulloptions.append((opt[0], opt[0])) for (idx, (_, text)) in enumerate(fulloptions): self.poutput(' %2d. %s' % (idx + 1, text)) + while True: safe_prompt = rl_make_safe_prompt(prompt) - response = input(safe_prompt) + + try: + with self.sigint_protection: + disable_completion() + response = input(safe_prompt) + except EOFError: + response = '' + self.poutput('\n', end='') + finally: + with self.sigint_protection: + enable_completion() + + if not response: + continue if rl_type != RlType.NONE: hlen = readline.get_current_history_length() - if hlen >= 1 and response != '': + if hlen >= 1: readline.remove_history_item(hlen - 1) - try: choice = int(response) if choice < 1: @@ -2989,6 +2963,7 @@ except (ValueError, IndexError): self.poutput("{!r} isn't a valid choice. Pick a number between 1 and {}:".format( response, len(fulloptions))) + return result def _get_read_only_settings(self) -> str: @@ -3603,15 +3578,25 @@ hist_file = os.path.abspath(os.path.expanduser(hist_file)) - # first we try and unpickle the history file - history = History() # on Windows, trying to open a directory throws a permission # error, not a `IsADirectoryError`. So we'll check it ourselves. if os.path.isdir(hist_file): - msg = "persistent history file '{}' is a directory" + msg = "Persistent history file '{}' is a directory" self.perror(msg.format(hist_file)) return + # Create the directory for the history file if it doesn't already exist + hist_file_dir = os.path.dirname(hist_file) + try: + os.makedirs(hist_file_dir, exist_ok=True) + except OSError as ex: + msg = "Error creating persistent history file directory '{}': {}".format(hist_file_dir, ex) + self.pexcept(msg) + return + + # first we try and unpickle the history file + history = History() + try: with open(hist_file, 'rb') as fobj: history = pickle.load(fobj) @@ -3619,7 +3604,7 @@ # If any non-operating system error occurs when attempting to unpickle, just use an empty history pass except OSError as ex: - msg = "can not read persistent history file '{}': {}" + msg = "Can not read persistent history file '{}': {}" self.pexcept(msg.format(hist_file, ex)) return @@ -3655,7 +3640,7 @@ with open(self.persistent_history_file, 'wb') as fobj: pickle.dump(self.history, fobj) except OSError as ex: - msg = "can not write persistent history file '{}': {}" + msg = "Can not write persistent history file '{}': {}" self.pexcept(msg.format(self.persistent_history_file, ex)) def _generate_transcript(self, history: List[Union[HistoryItem, str]], transcript_file: str) -> None: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cmd2-0.9.15/cmd2/parsing.py new/cmd2-0.9.16/cmd2/parsing.py --- old/cmd2-0.9.15/cmd2/parsing.py 2019-07-25 00:50:26.000000000 +0200 +++ new/cmd2-0.9.16/cmd2/parsing.py 2019-08-07 15:12:28.000000000 +0200 @@ -185,7 +185,7 @@ """Combine command and args with a space separating them. Quoted arguments remain quoted. Output redirection and piping are - excluded, as are any multiline command terminators. + excluded, as are any command terminators. """ if self.command and self.args: rtn = '{} {}'.format(self.command, self.args) @@ -245,7 +245,6 @@ the expansion. """ def __init__(self, - allow_redirection: bool = True, terminators: Optional[Iterable[str]] = None, multiline_commands: Optional[Iterable[str]] = None, aliases: Optional[Dict[str, str]] = None, @@ -257,13 +256,11 @@ * multiline commands * shortcuts - :param allow_redirection: should redirection and pipes be allowed? - :param terminators: iterable containing strings which should terminate multiline commands + :param terminators: iterable containing strings which should terminate commands :param multiline_commands: iterable containing the names of commands that accept multiline input :param aliases: dictionary containing aliases :param shortcuts: dictionary containing shortcuts """ - self.allow_redirection = allow_redirection if terminators is None: self.terminators = (constants.MULTILINE_TERMINATOR,) else: @@ -359,20 +356,17 @@ errmsg = '' return valid, errmsg - def tokenize(self, line: str, *, expand: bool = True) -> List[str]: + def tokenize(self, line: str) -> List[str]: """ Lex a string into a list of tokens. Shortcuts and aliases are expanded and comments are removed :param line: the command line being lexed - :param expand: If True, then aliases and shortcuts will be expanded. - Set this to False if the command token should not be altered. Defaults to True. :return: A list of tokens :raises ValueError if there are unclosed quotation marks. """ # expand shortcuts and aliases - if expand: - line = self._expand(line) + line = self._expand(line) # check if this line is a comment if line.lstrip().startswith(constants.COMMENT_CHAR): @@ -382,18 +376,16 @@ tokens = shlex_split(line) # custom lexing - tokens = self._split_on_punctuation(tokens) + tokens = self.split_on_punctuation(tokens) return tokens - def parse(self, line: str, *, expand: bool = True) -> Statement: + def parse(self, line: str) -> Statement: """ Tokenize the input and parse it into a Statement object, stripping comments, expanding aliases and shortcuts, and extracting output redirection directives. :param line: the command line being parsed - :param expand: If True, then aliases and shortcuts will be expanded. - Set this to False if the command token should not be altered. Defaults to True. :return: the created Statement :raises ValueError if there are unclosed quotation marks """ @@ -410,7 +402,7 @@ arg_list = [] # lex the input into a list of tokens - tokens = self.tokenize(line, expand=expand) + tokens = self.tokenize(line) # of the valid terminators, find the first one to occur in the input terminator_pos = len(tokens) + 1 @@ -532,7 +524,7 @@ output_to=output_to) return statement - def parse_command_only(self, rawinput: str, *, expand: bool = True) -> Statement: + def parse_command_only(self, rawinput: str) -> Statement: """Partially parse input into a Statement object. The command is identified, and shortcuts and aliases are expanded. @@ -557,15 +549,12 @@ whitespace. :param rawinput: the command line as entered by the user - :param expand: If True, then aliases and shortcuts will be expanded. - Set this to False if the command token should not be altered. Defaults to True. :return: the created Statement """ line = rawinput # expand shortcuts and aliases - if expand: - line = self._expand(rawinput) + line = self._expand(rawinput) command = '' args = '' @@ -619,7 +608,7 @@ """ # Check if to_parse needs to be converted to a Statement if not isinstance(to_parse, Statement): - to_parse = self.parse(command_name + ' ' + to_parse, expand=False) + to_parse = self.parse(command_name + ' ' + to_parse) if preserve_quotes: return to_parse, to_parse.arg_list @@ -678,7 +667,7 @@ return command, args - def _split_on_punctuation(self, tokens: List[str]) -> List[str]: + def split_on_punctuation(self, tokens: List[str]) -> List[str]: """Further splits tokens from a command line using punctuation characters Punctuation characters are treated as word breaks when they are in @@ -690,8 +679,7 @@ """ punctuation = [] punctuation.extend(self.terminators) - if self.allow_redirection: - punctuation.extend(constants.REDIRECTION_CHARS) + punctuation.extend(constants.REDIRECTION_CHARS) punctuated_tokens = [] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cmd2-0.9.15/cmd2.egg-info/PKG-INFO new/cmd2-0.9.16/cmd2.egg-info/PKG-INFO --- old/cmd2-0.9.15/cmd2.egg-info/PKG-INFO 2019-07-25 04:11:22.000000000 +0200 +++ new/cmd2-0.9.16/cmd2.egg-info/PKG-INFO 2019-08-08 04:00:02.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: cmd2 -Version: 0.9.15 +Version: 0.9.16 Summary: cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python Home-page: https://github.com/python-cmd2/cmd2 Author: Catherine Devlin @@ -196,7 +196,7 @@ Any command accepts multi-line input when its name is listed the `multiline_commands` optional argument to `cmd2.Cmd.__init`. The program will keep expecting input until a line ends with any of the characters listed in the - `terminators` optional argument to `cmd2.Cmd.__init__()` . The default terminators are `;` and `/n` (empty newline). + `terminators` optional argument to `cmd2.Cmd.__init__()` . The default terminators are `;` and `\n` (empty newline). - Special-character shortcut commands (beyond cmd's "@" and "!") @@ -216,11 +216,10 @@ Tutorials --------- - A few tutorials on using cmd2 exist: - - * Florida PyCon 2017 talk: [slides](https://docs.google.com/presentation/d/1LRmpfBt3V-pYQfgQHdczf16F3hcXmhK83tl77R6IJtE), [video](https://www.youtube.com/watch?v=6m0RdpITaeY) - * PyCon 2010 talk by Catherine Devlin, the original author: [video](http://pyvideo.org/pycon-us-2010/pycon-2010--easy-command-line-applications-with-c.html) - * A nice brief step-by-step tutorial: [blog](https://kushaldas.in/posts/developing-command-line-interpreters-using-python-cmd2.html) + * PyOhio 2019 presentation: + * [video](https://www.youtube.com/watch?v=pebeWrTqIIw) + * [slides](https://github.com/python-cmd2/talks/blob/master/PyOhio_2019/cmd2-PyOhio_2019.pdf) + * [example code](https://github.com/python-cmd2/talks/tree/master/PyOhio_2019/examples) Example Application @@ -400,5 +399,5 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Python: >=3.5 Description-Content-Type: text/markdown -Provides-Extra: test Provides-Extra: dev +Provides-Extra: test diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cmd2-0.9.15/docs/features/shortcuts_aliases_macros.rst new/cmd2-0.9.16/docs/features/shortcuts_aliases_macros.rst --- old/cmd2-0.9.15/docs/features/shortcuts_aliases_macros.rst 2019-07-19 03:01:42.000000000 +0200 +++ new/cmd2-0.9.16/docs/features/shortcuts_aliases_macros.rst 2019-08-08 02:38:15.000000000 +0200 @@ -38,6 +38,7 @@ updating the ``shortcuts`` attribute This warning applies in general to many other attributes which are not settable at runtime. +Note: Command, alias, and macro names cannot start with a shortcut Aliases ------- @@ -65,6 +66,7 @@ For more details run: ``help alias delete`` +Note: Aliases cannot have the same name as a command or macro Macros ------ @@ -93,3 +95,5 @@ For more details on listing macros run: ``help macro list`` For more details on deleting macros run: ``help macro delete`` + +Note: Macros cannot have the same name as a command or alias \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cmd2-0.9.15/examples/async_printing.py new/cmd2-0.9.16/examples/async_printing.py --- old/cmd2-0.9.15/examples/async_printing.py 2019-07-19 03:01:42.000000000 +0200 +++ new/cmd2-0.9.16/examples/async_printing.py 2019-08-02 02:50:24.000000000 +0200 @@ -139,12 +139,26 @@ return alert_str def _generate_colored_prompt(self) -> str: - """Randomly generates a colored prompt + """ + Randomly generates a colored prompt :return: the new prompt """ - fg_color = random.choice(list(ansi.FG_COLORS.keys())) - bg_color = random.choice(list(ansi.BG_COLORS.keys())) - return ansi.style(self.visible_prompt.rstrip(), fg=fg_color, bg=bg_color) + ' ' + rand_num = random.randint(1, 20) + + status_color = 'reset' + + if rand_num == 1: + status_color = 'bright_red' + elif rand_num == 2: + status_color = 'bright_yellow' + elif rand_num == 3: + status_color = 'cyan' + elif rand_num == 4: + status_color = 'bright_green' + elif rand_num == 5: + status_color = 'bright_blue' + + return ansi.style(self.visible_prompt, fg=status_color) def _alerter_thread_func(self) -> None: """ Prints alerts and updates the prompt any time the prompt is showing """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cmd2-0.9.15/tests/test_argparse_custom.py new/cmd2-0.9.16/tests/test_argparse_custom.py --- old/cmd2-0.9.15/tests/test_argparse_custom.py 2019-07-19 03:01:42.000000000 +0200 +++ new/cmd2-0.9.16/tests/test_argparse_custom.py 2019-08-06 04:42:05.000000000 +0200 @@ -39,25 +39,49 @@ pass -@pytest.mark.parametrize('args, is_valid', [ - ({'choices': []}, True), +@pytest.mark.parametrize('kwargs, is_valid', [ ({'choices_function': fake_func}, True), ({'choices_method': fake_func}, True), ({'completer_function': fake_func}, True), ({'completer_method': fake_func}, True), - ({'choices': [], 'choices_function': fake_func}, False), - ({'choices': [], 'choices_method': fake_func}, False), + ({'choices_function': fake_func, 'choices_method': fake_func}, False), ({'choices_method': fake_func, 'completer_function': fake_func}, False), - ({'choices_method': fake_func, 'completer_method': fake_func}, False), + ({'completer_function': fake_func, 'completer_method': fake_func}, False), ]) -def test_apcustom_invalid_args(args, is_valid): +def test_apcustom_choices_callable_count(kwargs, is_valid): parser = Cmd2ArgumentParser(prog='test') try: - parser.add_argument('name', **args) + parser.add_argument('name', **kwargs) assert is_valid except ValueError as ex: assert not is_valid - assert 'Only one of the following may be used' in str(ex) + assert 'Only one of the following parameters' in str(ex) + + +@pytest.mark.parametrize('kwargs', [ + ({'choices_function': fake_func}), + ({'choices_method': fake_func}), + ({'completer_function': fake_func}), + ({'completer_method': fake_func}) +]) +def test_apcustom_no_choices_callables_alongside_choices(kwargs): + with pytest.raises(TypeError) as excinfo: + parser = Cmd2ArgumentParser(prog='test') + parser.add_argument('name', choices=['my', 'choices', 'list'], **kwargs) + assert 'None of the following parameters can be used alongside a choices parameter' in str(excinfo.value) + + +@pytest.mark.parametrize('kwargs', [ + ({'choices_function': fake_func}), + ({'choices_method': fake_func}), + ({'completer_function': fake_func}), + ({'completer_method': fake_func}) +]) +def test_apcustom_no_choices_callables_when_nargs_is_0(kwargs): + with pytest.raises(TypeError) as excinfo: + parser = Cmd2ArgumentParser(prog='test') + parser.add_argument('name', action='store_true', **kwargs) + assert 'None of the following parameters can be used on an action that takes no arguments' in str(excinfo.value) def test_apcustom_usage(): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cmd2-0.9.15/tests/test_cmd2.py new/cmd2-0.9.16/tests/test_cmd2.py --- old/cmd2-0.9.15/tests/test_cmd2.py 2019-07-25 00:50:26.000000000 +0200 +++ new/cmd2-0.9.16/tests/test_cmd2.py 2019-08-08 02:38:15.000000000 +0200 @@ -87,6 +87,10 @@ expected = normalize(SHORTCUTS_TXT) assert out == expected +def test_command_starts_with_shortcut(): + with pytest.raises(ValueError) as excinfo: + app = cmd2.Cmd(shortcuts={'help': 'fake'}) + assert "Invalid command name 'help'" in str(excinfo.value) def test_base_show(base_app): # force editor to be 'vim' so test is repeatable across platforms @@ -558,7 +562,7 @@ def test_disallow_redirection(base_app): # Set allow_redirection to False - base_app.statement_parser.allow_redirection = False + base_app.allow_redirection = False filename = 'test_allow_redirect.txt' @@ -639,7 +643,7 @@ expected_text = normalize(""" EXCEPTION of type '{}' occurred with message: 'Please use 'set editor' to specify your text editing program of choice.' -To enable full traceback, run the following command: 'set debug true' +To enable full traceback, run the following command: 'set debug true' """.format(expected_exception)) return expected_text @@ -1098,6 +1102,7 @@ arg = 'Sauce? ' calls = [mock.call(arg), mock.call(arg)] m.assert_has_calls(calls) + assert m.call_count == 2 # And verify the expected output to stdout assert out == expected @@ -1122,6 +1127,7 @@ arg = 'Sauce? ' calls = [mock.call(arg), mock.call(arg)] m.assert_has_calls(calls) + assert m.call_count == 2 # And verify the expected output to stdout assert out == expected @@ -1181,6 +1187,19 @@ # And verify the expected output to stdout assert out == expected +def test_select_eof(select_app): + # Ctrl-D during select causes an EOFError that just reprompts the user + m = mock.MagicMock(name='input', side_effect=[EOFError, 2]) + builtins.input = m + + food = 'fish' + out, err = run_cmd(select_app, "eat {}".format(food)) + + # Make sure our mock was called exactly twice with the expected arguments + arg = 'Sauce? ' + calls = [mock.call(arg), mock.call(arg)] + m.assert_has_calls(calls) + assert m.call_count == 2 class HelpNoDocstringApp(cmd2.Cmd): greet_parser = argparse.ArgumentParser() @@ -1625,6 +1644,10 @@ out, err = run_cmd(base_app, 'alias create {} help'.format(alias_name)) assert "Invalid alias name" in err[0] +def test_alias_create_with_command_name(base_app): + out, err = run_cmd(base_app, 'alias create help stuff') + assert "Alias cannot have the same name as a command" in err[0] + def test_alias_create_with_macro_name(base_app): macro = "my_macro" run_cmd(base_app, 'macro create {} help'.format(macro)) @@ -1713,19 +1736,16 @@ out, err = run_cmd(base_app, 'macro create {} help'.format(macro_name)) assert "Invalid macro name" in err[0] +def test_macro_create_with_command_name(base_app): + out, err = run_cmd(base_app, 'macro create help stuff') + assert "Macro cannot have the same name as a command" in err[0] + def test_macro_create_with_alias_name(base_app): macro = "my_macro" run_cmd(base_app, 'alias create {} help'.format(macro)) out, err = run_cmd(base_app, 'macro create {} help'.format(macro)) assert "Macro cannot have the same name as an alias" in err[0] -def test_macro_create_with_command_name(multiline_app): - out, err = run_cmd(multiline_app, 'macro create help stuff') - assert out == normalize("Macro 'help' created") - - out, err = run_cmd(multiline_app, 'macro create orate stuff') - assert "Macro cannot have the same name as a multiline command" in err[0] - def test_macro_create_with_args(base_app): # Create the macro out, err = run_cmd(base_app, 'macro create fake {1} {2}') @@ -1843,37 +1863,6 @@ assert exception is not None -def test_input_line_to_statement_expand(base_app): - # Enable/Disable expansion of shortcuts - line = '!ls' - statement = base_app._input_line_to_statement(line, expand=True) - assert statement.command == 'shell' - - statement = base_app._input_line_to_statement(line, expand=False) - assert statement.command == '!ls' - - # Enable/Disable expansion of aliases - run_cmd(base_app, 'alias create help macro') - - line = 'help' - statement = base_app._input_line_to_statement(line, expand=True) - assert statement.command == 'macro' - - statement = base_app._input_line_to_statement(line, expand=False) - assert statement.command == 'help' - - run_cmd(base_app, 'alias delete help') - - # Enable/Disable expansion of macros - run_cmd(base_app, 'macro create help alias') - - line = 'help' - statement = base_app._input_line_to_statement(line, expand=True) - assert statement.command == 'alias' - - statement = base_app._input_line_to_statement(line, expand=False) - assert statement.command == 'help' - def test_ppaged(outsim_app): msg = 'testing...' end = '\n' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cmd2-0.9.15/tests/test_completion.py new/cmd2-0.9.16/tests/test_completion.py --- old/cmd2-0.9.15/tests/test_completion.py 2019-07-24 03:41:45.000000000 +0200 +++ new/cmd2-0.9.16/tests/test_completion.py 2019-08-03 17:16:28.000000000 +0200 @@ -689,46 +689,32 @@ assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens -def test_tokens_for_completion_redirect(cmd2_app): - text = '>>file' - line = 'command | < {}'.format(text) +def test_tokens_for_completion_punctuation(cmd2_app): + """Test that redirectors and terminators are word delimiters""" + text = 'file' + line = 'command | < ;>>{}'.format(text) endidx = len(line) begidx = endidx - len(text) - cmd2_app.allow_redirection = True - expected_tokens = ['command', '|', '<', '>>', 'file'] - expected_raw_tokens = ['command', '|', '<', '>>', 'file'] + expected_tokens = ['command', '|', '<', ';', '>>', 'file'] + expected_raw_tokens = ['command', '|', '<', ';', '>>', 'file'] tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx) assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens -def test_tokens_for_completion_quoted_redirect(cmd2_app): +def test_tokens_for_completion_quoted_punctuation(cmd2_app): + """Test that quoted punctuation characters are not word delimiters""" text = '>file' line = 'command "{}'.format(text) endidx = len(line) begidx = endidx - len(text) - cmd2_app.statement_parser.redirection = True expected_tokens = ['command', '>file'] expected_raw_tokens = ['command', '">file'] tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx) assert expected_tokens == tokens - assert expected_raw_tokens == raw_tokens - -def test_tokens_for_completion_redirect_off(cmd2_app): - text = '>file' - line = 'command {}'.format(text) - endidx = len(line) - begidx = endidx - len(text) - - cmd2_app.statement_parser.allow_redirection = False - expected_tokens = ['command', '>file'] - expected_raw_tokens = ['command', '>file'] - - tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx) - assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens def test_add_opening_quote_basic_no_text(cmd2_app): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cmd2-0.9.15/tests/test_history.py new/cmd2-0.9.16/tests/test_history.py --- old/cmd2-0.9.15/tests/test_history.py 2019-07-25 00:50:26.000000000 +0200 +++ new/cmd2-0.9.16/tests/test_history.py 2019-08-02 14:21:44.000000000 +0200 @@ -268,7 +268,6 @@ def parser(): from cmd2.parsing import StatementParser parser = StatementParser( - allow_redirection=True, terminators=[';', '&'], multiline_commands=['multiline'], aliases={'helpalias': 'help', @@ -638,6 +637,36 @@ _, err = capsys.readouterr() assert 'is a directory' in err +def test_history_can_create_directory(mocker): + # Mock out atexit.register so the persistent file doesn't written when this function + # exists because we will be deleting the directory it needs to go to. + mock_register = mocker.patch('atexit.register') + + # Create a temp path for us to use and let it get deleted + with tempfile.TemporaryDirectory() as test_dir: + pass + assert not os.path.isdir(test_dir) + + # Add some subdirectories for the complete history file directory + hist_file_dir = os.path.join(test_dir, 'subdir1', 'subdir2') + hist_file = os.path.join(hist_file_dir, 'hist_file') + + # Make sure cmd2 creates the history file directory + cmd2.Cmd(persistent_history_file=hist_file) + assert os.path.isdir(hist_file_dir) + + # Cleanup + os.rmdir(hist_file_dir) + +def test_history_cannot_create_directory(mocker, capsys): + mock_open = mocker.patch('os.makedirs') + mock_open.side_effect = OSError + + hist_file_path = os.path.join('fake_dir', 'file') + cmd2.Cmd(persistent_history_file=hist_file_path) + _, err = capsys.readouterr() + assert 'Error creating persistent history file directory' in err + def test_history_file_permission_error(mocker, capsys): mock_open = mocker.patch('builtins.open') mock_open.side_effect = PermissionError @@ -645,7 +674,7 @@ cmd2.Cmd(persistent_history_file='/tmp/doesntmatter') out, err = capsys.readouterr() assert not out - assert 'can not read' in err + assert 'Can not read' in err def test_history_file_conversion_no_truncate_on_init(hist_file, capsys): # make sure we don't truncate the plain text history file on init @@ -720,4 +749,4 @@ app._persist_history() out, err = capsys.readouterr() assert not out - assert 'can not write' in err + assert 'Can not write' in err diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cmd2-0.9.15/tests/test_parsing.py new/cmd2-0.9.16/tests/test_parsing.py --- old/cmd2-0.9.15/tests/test_parsing.py 2019-07-19 03:01:42.000000000 +0200 +++ new/cmd2-0.9.16/tests/test_parsing.py 2019-08-07 15:12:28.000000000 +0200 @@ -13,7 +13,6 @@ @pytest.fixture def parser(): parser = StatementParser( - allow_redirection=True, terminators=[';', '&'], multiline_commands=['multiline'], aliases={'helpalias': 'help', @@ -605,19 +604,11 @@ ('l', 'shell', 'ls -al') ]) def test_parse_alias_and_shortcut_expansion(parser, line, command, args): - # Test first with expansion statement = parser.parse(line) assert statement.command == command assert statement == args assert statement.args == statement - # Now allow no expansion - tokens = shlex_split(line) - statement = parser.parse(line, expand=False) - assert statement.command == tokens[0] - assert shlex_split(statement) == tokens[1:] - assert statement.args == statement - def test_parse_alias_on_multiline_command(parser): line = 'anothermultiline has > inside an unfinished command' statement = parser.parse(line)