diff options
-rw-r--r-- | cmd2/__init__.py | 3 | ||||
-rw-r--r-- | cmd2/argcomplete_bridge.py | 246 | ||||
-rwxr-xr-x | cmd2/cmd2.py | 95 | ||||
-rw-r--r-- | cmd2/utils.py | 15 | ||||
-rwxr-xr-x | examples/alias_startup.py | 6 | ||||
-rwxr-xr-x | examples/arg_print.py | 6 | ||||
-rwxr-xr-x | examples/argparse_example.py | 2 | ||||
-rwxr-xr-x | examples/environment.py | 2 | ||||
-rwxr-xr-x | examples/event_loops.py | 2 | ||||
-rwxr-xr-x | examples/example.py | 2 | ||||
-rwxr-xr-x | examples/help_categories.py | 3 | ||||
-rwxr-xr-x | examples/paged_output.py | 4 | ||||
-rwxr-xr-x | examples/persistent_history.py | 2 | ||||
-rwxr-xr-x | examples/pirate.py | 2 | ||||
-rwxr-xr-x | examples/python_scripting.py | 2 | ||||
-rwxr-xr-x | examples/remove_unused.py | 2 | ||||
-rwxr-xr-x | examples/subcommands.py | 75 | ||||
-rwxr-xr-x | examples/submenus.py | 2 | ||||
-rwxr-xr-x | examples/tab_autocompletion.py | 4 | ||||
-rwxr-xr-x | examples/tab_completion.py | 4 | ||||
-rwxr-xr-x | examples/table_display.py | 2 | ||||
-rw-r--r-- | tests/conftest.py | 2 | ||||
-rw-r--r-- | tests/test_argparse.py | 2 | ||||
-rw-r--r-- | tests/test_cmd2.py | 20 | ||||
-rw-r--r-- | tests/test_completion.py | 2 | ||||
-rw-r--r-- | tests/test_parsing.py | 18 | ||||
-rw-r--r-- | tests/test_submenu.py | 2 | ||||
-rw-r--r-- | tests/test_transcript.py | 8 |
28 files changed, 395 insertions, 140 deletions
diff --git a/cmd2/__init__.py b/cmd2/__init__.py index 8e744e03..bf6c047f 100644 --- a/cmd2/__init__.py +++ b/cmd2/__init__.py @@ -1,5 +1,2 @@ # # -*- coding: utf-8 -*- -# -from .cmd2 import __version__, Cmd, set_posix_shlex, set_strip_quotes, AddSubmenu, CmdResult, categorize -from .cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category diff --git a/cmd2/argcomplete_bridge.py b/cmd2/argcomplete_bridge.py new file mode 100644 index 00000000..583f3345 --- /dev/null +++ b/cmd2/argcomplete_bridge.py @@ -0,0 +1,246 @@ +# coding=utf-8 +"""Hijack the ArgComplete's bash completion handler to return AutoCompleter results""" + +try: + # check if argcomplete is installed + import argcomplete +except ImportError: + # not installed, skip the rest of the file + pass + +else: + # argcomplete is installed + + from contextlib import redirect_stdout + import copy + from io import StringIO + import os + import shlex + import sys + + from . import constants + from . import utils + + + def tokens_for_completion(line, endidx): + """ + Used by tab completion functions to get all tokens through the one being completed + :param line: str - the current input line with leading whitespace removed + :param endidx: int - the ending index of the prefix text + :return: A 4 item tuple where the items are + On Success + tokens: list of unquoted tokens + this is generally the list needed for tab completion functions + raw_tokens: list of tokens with any quotes preserved + this can be used to know if a token was quoted or is missing a closing quote + begidx: beginning of last token + endidx: cursor position + + Both lists are guaranteed to have at least 1 item + The last item in both lists is the token being tab completed + + On Failure + Both items are None + """ + unclosed_quote = '' + quotes_to_try = copy.copy(constants.QUOTES) + + tmp_line = line[:endidx] + tmp_endidx = endidx + + # Parse the line into tokens + while True: + try: + # Use non-POSIX parsing to keep the quotes around the tokens + initial_tokens = shlex.split(tmp_line[:tmp_endidx], posix=False) + + # calculate begidx + if unclosed_quote: + begidx = tmp_line[:tmp_endidx].rfind(initial_tokens[-1]) + 1 + else: + if tmp_endidx > 0 and tmp_line[tmp_endidx - 1] == ' ': + begidx = endidx + else: + begidx = tmp_line[:tmp_endidx].rfind(initial_tokens[-1]) + + # If the cursor is at an empty token outside of a quoted string, + # then that is the token being completed. Add it to the list. + if not unclosed_quote and begidx == tmp_endidx: + initial_tokens.append('') + break + except ValueError: + # ValueError can be caused by missing closing quote + if not quotes_to_try: + # Since we have no more quotes to try, something else + # is causing the parsing error. Return None since + # this means the line is malformed. + return None, None, None, None + + # Add a closing quote and try to parse again + unclosed_quote = quotes_to_try[0] + quotes_to_try = quotes_to_try[1:] + + tmp_line = line[:endidx] + tmp_line += unclosed_quote + tmp_endidx = endidx + 1 + + raw_tokens = initial_tokens + + # Save the unquoted tokens + tokens = [utils.strip_quotes(cur_token) for cur_token in raw_tokens] + + # If the token being completed had an unclosed quote, we need + # to remove the closing quote that was added in order for it + # to match what was on the command line. + if unclosed_quote: + raw_tokens[-1] = raw_tokens[-1][:-1] + + return tokens, raw_tokens, begidx, endidx + + class CompletionFinder(argcomplete.CompletionFinder): + """Hijack the functor from argcomplete to call AutoCompleter""" + + def __call__(self, argument_parser, completer=None, always_complete_options=True, exit_method=os._exit, output_stream=None, + exclude=None, validator=None, print_suppressed=False, append_space=None, + default_completer=argcomplete.FilesCompleter()): + """ + :param argument_parser: The argument parser to autocomplete on + :type argument_parser: :class:`argparse.ArgumentParser` + :param always_complete_options: + Controls the autocompletion of option strings if an option string opening character (normally ``-``) has not + been entered. If ``True`` (default), both short (``-x``) and long (``--x``) option strings will be + suggested. If ``False``, no option strings will be suggested. If ``long``, long options and short options + with no long variant will be suggested. If ``short``, short options and long options with no short variant + will be suggested. + :type always_complete_options: boolean or string + :param exit_method: + Method used to stop the program after printing completions. Defaults to :meth:`os._exit`. If you want to + perform a normal exit that calls exit handlers, use :meth:`sys.exit`. + :type exit_method: callable + :param exclude: List of strings representing options to be omitted from autocompletion + :type exclude: iterable + :param validator: + Function to filter all completions through before returning (called with two string arguments, completion + and prefix; return value is evaluated as a boolean) + :type validator: callable + :param print_suppressed: + Whether or not to autocomplete options that have the ``help=argparse.SUPPRESS`` keyword argument set. + :type print_suppressed: boolean + :param append_space: + Whether to append a space to unique matches. The default is ``True``. + :type append_space: boolean + + .. note:: + If you are not subclassing CompletionFinder to override its behaviors, + use ``argcomplete.autocomplete()`` directly. It has the same signature as this method. + + Produces tab completions for ``argument_parser``. See module docs for more info. + + Argcomplete only executes actions if their class is known not to have side effects. Custom action classes can be + added to argcomplete.safe_actions, if their values are wanted in the ``parsed_args`` completer argument, or + their execution is otherwise desirable. + """ + self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude, + validator=validator, print_suppressed=print_suppressed, append_space=append_space, + default_completer=default_completer) + + if "_ARGCOMPLETE" not in os.environ: + # not an argument completion invocation + return + + try: + argcomplete.debug_stream = os.fdopen(9, "w") + except IOError: + argcomplete.debug_stream = sys.stderr + + if output_stream is None: + try: + output_stream = os.fdopen(8, "wb") + except IOError: + argcomplete.debug("Unable to open fd 8 for writing, quitting") + exit_method(1) + + # print("", stream=debug_stream) + # for v in "COMP_CWORD COMP_LINE COMP_POINT COMP_TYPE COMP_KEY _ARGCOMPLETE_COMP_WORDBREAKS COMP_WORDS".split(): + # print(v, os.environ[v], stream=debug_stream) + + ifs = os.environ.get("_ARGCOMPLETE_IFS", "\013") + if len(ifs) != 1: + argcomplete.debug("Invalid value for IFS, quitting [{v}]".format(v=ifs)) + exit_method(1) + + comp_line = os.environ["COMP_LINE"] + comp_point = int(os.environ["COMP_POINT"]) + + comp_line = argcomplete.ensure_str(comp_line) + + ############################## + # SWAPPED FOR AUTOCOMPLETER + # + # Replaced with our own tokenizer function + ############################## + + # cword_prequote, cword_prefix, cword_suffix, comp_words, last_wordbreak_pos = split_line(comp_line, comp_point) + tokens, _, begidx, endidx = tokens_for_completion(comp_line, comp_point) + + # _ARGCOMPLETE is set by the shell script to tell us where comp_words + # should start, based on what we're completing. + # 1: <script> [args] + # 2: python <script> [args] + # 3: python -m <module> [args] + start = int(os.environ["_ARGCOMPLETE"]) - 1 + ############################## + # SWAPPED FOR AUTOCOMPLETER + # + # Applying the same token dropping to our tokens + ############################## + # comp_words = comp_words[start:] + tokens = tokens[start:] + + # debug("\nLINE: {!r}".format(comp_line), + # "\nPOINT: {!r}".format(comp_point), + # "\nPREQUOTE: {!r}".format(cword_prequote), + # "\nPREFIX: {!r}".format(cword_prefix), + # "\nSUFFIX: {!r}".format(cword_suffix), + # "\nWORDS:", comp_words) + + ############################## + # SWAPPED FOR AUTOCOMPLETER + # + # Replaced with our own completion function and customizing the returned values + ############################## + # completions = self._get_completions(comp_words, cword_prefix, cword_prequote, last_wordbreak_pos) + + # capture stdout from the autocompleter + result = StringIO() + with redirect_stdout(result): + completions = completer.complete_command(tokens, tokens[-1], comp_line, begidx, endidx) + outstr = result.getvalue() + + if completions: + # If any completion has a space in it, then quote all completions + # this improves the user experience so they don't nede to go back and add a quote + if ' ' in ''.join(completions): + completions = ['"{}"'.format(entry) for entry in completions] + + argcomplete.debug("\nReturning completions:", completions) + + output_stream.write(ifs.join(completions).encode(argcomplete.sys_encoding)) + elif outstr: + # if there are no completions, but we got something from stdout, try to print help + + # trick the bash completion into thinking there are 2 completions that are unlikely + # to ever match. + outstr = outstr.replace('\n', ' ').replace('\t', ' ').replace(' ', ' ').strip() + # generate a filler entry that should always sort first + filler = ' {0:><{width}}'.format('', width=len(outstr)/2) + outstr = ifs.join([filler, outstr]) + + output_stream.write(outstr.encode(argcomplete.sys_encoding)) + else: + # if completions is None we assume we don't know how to handle it so let bash + # go forward with normal filesystem completion + output_stream.write(ifs.join([]).encode(argcomplete.sys_encoding)) + output_stream.flush() + argcomplete.debug_stream.flush() + exit_method(0) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 98adc01f..80fa5601 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -71,10 +71,6 @@ elif rl_type == RlType.GNU: import ctypes from .rl_utils import readline_lib - # Save address that rl_basic_quote_characters is pointing to since we need to override and restore it - rl_basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters") - orig_rl_basic_quote_characters_addr = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value - # Newer versions of pyperclip are released as a single file, but older versions had a more complicated structure try: from pyperclip.exceptions import PyperclipException @@ -184,21 +180,6 @@ def _which(editor: str) -> Optional[str]: return editor_path -def strip_quotes(arg: str) -> str: - """ Strip outer quotes from a string. - - Applies to both single and double quotes. - - :param arg: string to strip outer quotes from - :return: same string with potentially outer quotes stripped - """ - quote_chars = '"' + "'" - - if len(arg) > 1 and arg[0] == arg[-1] and arg[0] in quote_chars: - arg = arg[1:-1] - return arg - - def parse_quoted_string(cmdline: str) -> List[str]: """Parse a quoted string into a list of arguments.""" if isinstance(cmdline, list): @@ -211,7 +192,7 @@ def parse_quoted_string(cmdline: str) -> List[str]: if not POSIX_SHLEX and STRIP_QUOTES_FOR_NON_POSIX: temp_arglist = [] for arg in lexed_arglist: - temp_arglist.append(strip_quotes(arg)) + temp_arglist.append(utils.strip_quotes(arg)) lexed_arglist = temp_arglist return lexed_arglist @@ -385,7 +366,7 @@ def replace_with_file_contents(fname: str) -> str: """ try: # Any outer quotes are not part of the filename - unquoted_file = strip_quotes(fname[0]) + unquoted_file = utils.strip_quotes(fname[0]) with open(os.path.expanduser(unquoted_file)) as source_file: result = source_file.read() except IOError: @@ -603,11 +584,22 @@ class AddSubmenu(object): try: # copy over any shared attributes self._copy_in_shared_attrs(_self) + + # Reset the submenu's tab completion parameters + submenu.allow_appended_space = True + submenu.allow_closing_quote = True + submenu.display_matches = [] + return _complete_from_cmd(submenu, text, line, begidx, endidx) finally: # copy back original attributes self._copy_out_shared_attrs(_self, original_attributes) + # Pass the submenu's tab completion parameters back up to the menu that called complete() + _self.allow_appended_space = submenu.allow_appended_space + _self.allow_closing_quote = submenu.allow_closing_quote + _self.display_matches = copy.copy(submenu.display_matches) + original_do_help = cmd_obj.do_help original_complete_help = cmd_obj.complete_help @@ -996,6 +988,11 @@ class Cmd(cmd.Cmd): self.allow_closing_quote = True self.display_matches = [] + if rl_type == RlType.GNU: + readline.set_completion_display_matches_hook(self._display_matches_gnu_readline) + elif rl_type == RlType.PYREADLINE: + readline.rl.mode._display_completions = self._display_matches_pyreadline + def tokens_for_completion(self, line, begidx, endidx): """ Used by tab completion functions to get all tokens through the one being completed @@ -1105,7 +1102,7 @@ class Cmd(cmd.Cmd): raw_tokens = initial_tokens # Save the unquoted tokens - tokens = [strip_quotes(cur_token) for cur_token in raw_tokens] + tokens = [utils.strip_quotes(cur_token) for cur_token in raw_tokens] # If the token being completed had an unclosed quote, we need # to remove the closing quote that was added in order for it @@ -2368,38 +2365,33 @@ class Cmd(cmd.Cmd): """ # An almost perfect copy from Cmd; however, the pseudo_raw_input portion # has been split out so that it can be called separately - if self.use_rawinput and self.completekey: + if self.use_rawinput and self.completekey and rl_type != RlType.NONE: # Set up readline for our tab completion needs if rl_type == RlType.GNU: - readline.set_completion_display_matches_hook(self._display_matches_gnu_readline) - # Set GNU readline's rl_basic_quote_characters to NULL so it won't automatically add a closing quote # We don't need to worry about setting rl_completion_suppress_quote since we never declared # rl_completer_quote_characters. - rl_basic_quote_characters.value = None + basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters") + old_basic_quote_characters = ctypes.cast(basic_quote_characters, ctypes.c_void_p).value + basic_quote_characters.value = None - elif rl_type == RlType.PYREADLINE: - readline.rl.mode._display_completions = self._display_matches_pyreadline + old_completer = readline.get_completer() + old_delims = readline.get_completer_delims() + readline.set_completer(self.complete) - try: - self.old_completer = readline.get_completer() - self.old_delims = readline.get_completer_delims() - readline.set_completer(self.complete) + # Break words on whitespace and quotes when tab completing + completer_delims = " \t\n" + ''.join(constants.QUOTES) - # 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) - if self.allow_redirection: - # If redirection is allowed, then break words on those characters too - completer_delims += ''.join(constants.REDIRECTION_CHARS) + readline.set_completer_delims(completer_delims) - readline.set_completer_delims(completer_delims) + # Enable tab completion + readline.parse_and_bind(self.completekey + ": complete") - # Enable tab completion - readline.parse_and_bind(self.completekey + ": complete") - except NameError: - pass stop = None try: while not stop: @@ -2423,19 +2415,15 @@ class Cmd(cmd.Cmd): # Run the command along with all associated pre and post hooks stop = self.onecmd_plus_hooks(line) finally: - if self.use_rawinput and self.completekey: + if self.use_rawinput and self.completekey and rl_type != RlType.NONE: # Restore what we changed in readline - try: - readline.set_completer(self.old_completer) - readline.set_completer_delims(self.old_delims) - except NameError: - pass + readline.set_completer(old_completer) + readline.set_completer_delims(old_delims) if rl_type == RlType.GNU: readline.set_completion_display_matches_hook(None) - rl_basic_quote_characters.value = orig_rl_basic_quote_characters_addr - + basic_quote_characters.value = old_basic_quote_characters elif rl_type == RlType.PYREADLINE: readline.rl.mode._display_completions = orig_pyreadline_display @@ -2781,7 +2769,7 @@ Usage: Usage: unalias [-a] name [name ...] set_parser = ACArgumentParser(formatter_class=argparse.RawTextHelpFormatter) 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') - set_parser.add_argument('settable', nargs=(0,2), help='[param_name] [value]') + set_parser.add_argument('settable', nargs=(0, 2), help='[param_name] [value]') @with_argparser(set_parser) def do_set(self, args): @@ -2838,7 +2826,7 @@ Usage: Usage: unalias [-a] name [name ...] # an unclosed quote, so we only need to check the first character. first_char = tokens[index][0] if first_char in constants.QUOTES: - tokens[index] = strip_quotes(tokens[index]) + tokens[index] = utils.strip_quotes(tokens[index]) tokens[index] = os.path.expanduser(tokens[index]) @@ -2862,7 +2850,6 @@ Usage: Usage: unalias [-a] name [name ...] index_dict = {1: self.shell_cmd_complete} return self.index_based_complete(text, line, begidx, endidx, index_dict, self.path_complete) - # noinspection PyBroadException def do_py(self, arg): """ @@ -3356,7 +3343,7 @@ class ParserManager: ignore=do_not_parse)('pipeTo')) + \ pyparsing.Optional(output_destination_parser + pyparsing.SkipTo(string_end, ignore=do_not_parse). - setParseAction(lambda x: strip_quotes(x[0].strip()))('outputTo')) + setParseAction(lambda x: utils.strip_quotes(x[0].strip()))('outputTo')) multilineCommand.setParseAction(lambda x: x[0]) oneline_command.setParseAction(lambda x: x[0]) diff --git a/cmd2/utils.py b/cmd2/utils.py index 167879aa..a975c6b8 100644 --- a/cmd2/utils.py +++ b/cmd2/utils.py @@ -5,6 +5,7 @@ import collections from . import constants + def strip_ansi(text: str) -> str: """Strip ANSI escape codes from a string. @@ -14,6 +15,19 @@ def strip_ansi(text: str) -> str: return constants.ANSI_ESCAPE_RE.sub('', text) +def strip_quotes(arg: str) -> str: + """ Strip outer quotes from a string. + + Applies to both single and double quotes. + + :param arg: string to strip outer quotes from + :return: same string with potentially outer quotes stripped + """ + if len(arg) > 1 and arg[0] == arg[-1] and arg[0] in constants.QUOTES: + arg = arg[1:-1] + return arg + + def namedtuple_with_defaults(typename, field_names, default_values=()): """ Convenience function for defining a namedtuple with default values @@ -41,3 +55,4 @@ def namedtuple_with_defaults(typename, field_names, default_values=()): prototype = T(*default_values) T.__new__.__defaults__ = tuple(prototype) return T + diff --git a/examples/alias_startup.py b/examples/alias_startup.py index 30764c27..7ccfa6e5 100755 --- a/examples/alias_startup.py +++ b/examples/alias_startup.py @@ -4,12 +4,8 @@ 1) How to add custom command aliases using the alias command 2) How to load an initialization script at startup """ -import argparse -import cmd2 -import pyparsing - -from cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args +from cmd2 import cmd2 class AliasAndStartup(cmd2.Cmd): diff --git a/examples/arg_print.py b/examples/arg_print.py index 18fa483f..90df053a 100755 --- a/examples/arg_print.py +++ b/examples/arg_print.py @@ -11,10 +11,8 @@ It also serves as an example of how to create command aliases (shortcuts). """ import argparse -import cmd2 -import pyparsing - -from cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args +from cmd2 import cmd2 +from cmd2.cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args class ArgumentAndOptionPrinter(cmd2.Cmd): diff --git a/examples/argparse_example.py b/examples/argparse_example.py index e9b377ba..e8afef5c 100755 --- a/examples/argparse_example.py +++ b/examples/argparse_example.py @@ -14,7 +14,7 @@ verifying that the output produced matches the transcript. import argparse import sys -from cmd2 import Cmd, with_argparser, with_argument_list +from cmd2.cmd2 import Cmd, with_argparser, with_argument_list class CmdLineApp(Cmd): diff --git a/examples/environment.py b/examples/environment.py index c245f55d..af452e4e 100755 --- a/examples/environment.py +++ b/examples/environment.py @@ -4,7 +4,7 @@ A sample application for cmd2 demonstrating customized environment parameters """ -from cmd2 import Cmd +from cmd2.cmd2 import Cmd class EnvironmentApp(Cmd): diff --git a/examples/event_loops.py b/examples/event_loops.py index 53d3ca2b..a76c5d91 100755 --- a/examples/event_loops.py +++ b/examples/event_loops.py @@ -6,7 +6,7 @@ This is an example of how to use cmd2 in a way so that cmd2 doesn't own the inne This opens up the possibility of registering cmd2 input with event loops, like asyncio, without occupying the main loop. """ -import cmd2 +from cmd2 import cmd2 class Cmd2EventBased(cmd2.Cmd): diff --git a/examples/example.py b/examples/example.py index 612d81e5..1fc6bf6d 100755 --- a/examples/example.py +++ b/examples/example.py @@ -14,7 +14,7 @@ the transcript. import random import argparse -from cmd2 import Cmd, with_argparser +from cmd2.cmd2 import Cmd, with_argparser class CmdLineApp(Cmd): diff --git a/examples/help_categories.py b/examples/help_categories.py index cfb5f253..dcfbd31f 100755 --- a/examples/help_categories.py +++ b/examples/help_categories.py @@ -4,9 +4,10 @@ A sample application for tagging categories on commands. """ -from cmd2 import Cmd, categorize, __version__, with_argparser, with_category import argparse +from cmd2.cmd2 import Cmd, categorize, __version__, with_argparser, with_category + class HelpCategories(Cmd): """ Example cmd2 application. """ diff --git a/examples/paged_output.py b/examples/paged_output.py index bb410af6..9396f04e 100755 --- a/examples/paged_output.py +++ b/examples/paged_output.py @@ -3,8 +3,8 @@ """A simple example demonstrating the using paged output via the ppaged() method. """ -import cmd2 -from cmd2 import with_argument_list +from cmd2 import cmd2 +from cmd2.cmd2 import with_argument_list class PagedOutput(cmd2.Cmd): diff --git a/examples/persistent_history.py b/examples/persistent_history.py index 61e26b9c..251dbd67 100755 --- a/examples/persistent_history.py +++ b/examples/persistent_history.py @@ -5,7 +5,7 @@ This will allow end users of your cmd2-based application to use the arrow keys and Ctrl+r in a manner which persists across invocations of your cmd2 application. This can make it much easier for them to use your application. """ -import cmd2 +from cmd2 import cmd2 class Cmd2PersistentHistory(cmd2.Cmd): diff --git a/examples/pirate.py b/examples/pirate.py index 7fe3884b..2daa8631 100755 --- a/examples/pirate.py +++ b/examples/pirate.py @@ -7,7 +7,7 @@ presented as part of her PyCon 2010 talk. It demonstrates many features of cmd2. """ import argparse -from cmd2 import Cmd, with_argparser +from cmd2.cmd2 import Cmd, with_argparser class Pirate(Cmd): diff --git a/examples/python_scripting.py b/examples/python_scripting.py index 7e2cf345..865cf052 100755 --- a/examples/python_scripting.py +++ b/examples/python_scripting.py @@ -17,7 +17,7 @@ This application and the "scripts/conditional.py" script serve as an example for import argparse import os -import cmd2 +from cmd2 import cmd2 class CmdLineApp(cmd2.Cmd): diff --git a/examples/remove_unused.py b/examples/remove_unused.py index 8a567123..dfe0a055 100755 --- a/examples/remove_unused.py +++ b/examples/remove_unused.py @@ -9,7 +9,7 @@ name, they just won't clutter the help menu. Commands can also be removed entirely by using Python's "del". """ -import cmd2 +from cmd2 import cmd2 class RemoveUnusedBuiltinCommands(cmd2.Cmd): diff --git a/examples/subcommands.py b/examples/subcommands.py index eda88072..9bf6c666 100755 --- a/examples/subcommands.py +++ b/examples/subcommands.py @@ -1,5 +1,6 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # coding=utf-8 +# PYTHON_ARGCOMPLETE_OK """A simple example demonstrating how to use Argparse to support subcommands. @@ -8,11 +9,51 @@ and provides separate contextual help. """ import argparse -import cmd2 -from cmd2 import with_argparser - sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] +# create the top-level parser for the base command +base_parser = argparse.ArgumentParser(prog='base') +base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help') + +# create the parser for the "foo" subcommand +parser_foo = base_subparsers.add_parser('foo', help='foo help') +parser_foo.add_argument('-x', type=int, default=1, help='integer') +parser_foo.add_argument('y', type=float, help='float') + +# create the parser for the "bar" subcommand +parser_bar = base_subparsers.add_parser('bar', help='bar help') + +bar_subparsers = parser_bar.add_subparsers(title='layer3', help='help for 3rd layer of commands') +parser_bar.add_argument('z', help='string') + +bar_subparsers.add_parser('apple', help='apple help') +bar_subparsers.add_parser('artichoke', help='artichoke help') +bar_subparsers.add_parser('cranberries', help='cranberries help') + +# create the parser for the "sport" subcommand +parser_sport = base_subparsers.add_parser('sport', help='sport help') +sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport') +setattr(sport_arg, 'arg_choices', sport_item_strs) + +# Handle bash completion if it's installed +try: + # only move forward if we can import CompletionFinder and AutoCompleter + from cmd2.argcomplete_bridge import CompletionFinder + from cmd2.argparse_completer import AutoCompleter + if __name__ == '__main__': + with open('out.txt', 'a') as f: + f.write('Here 1') + f.flush() + completer = CompletionFinder() + completer(base_parser, AutoCompleter(base_parser)) +except ImportError: + pass + + +# Intentionally below the bash completion code to reduce tab completion lag +from cmd2 import cmd2 +from cmd2.cmd2 import with_argparser + class SubcommandsExample(cmd2.Cmd): """ @@ -36,33 +77,9 @@ class SubcommandsExample(cmd2.Cmd): """sport subcommand of base command""" self.poutput('Sport is {}'.format(args.sport)) - # create the top-level parser for the base command - base_parser = argparse.ArgumentParser(prog='base') - base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help') - - # create the parser for the "foo" subcommand - parser_foo = base_subparsers.add_parser('foo', help='foo help') - parser_foo.add_argument('-x', type=int, default=1, help='integer') - parser_foo.add_argument('y', type=float, help='float') + # Set handler functions for the subcommands parser_foo.set_defaults(func=base_foo) - - # create the parser for the "bar" subcommand - parser_bar = base_subparsers.add_parser('bar', help='bar help') parser_bar.set_defaults(func=base_bar) - - bar_subparsers = parser_bar.add_subparsers(title='layer3', help='help for 3rd layer of commands') - parser_bar.add_argument('z', help='string') - - bar_subparsers.add_parser('apple', help='apple help') - bar_subparsers.add_parser('artichoke', help='artichoke help') - bar_subparsers.add_parser('cranberries', help='cranberries help') - - # create the parser for the "sport" subcommand - parser_sport = base_subparsers.add_parser('sport', help='sport help') - sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport') - setattr(sport_arg, 'arg_choices', sport_item_strs) - - # Set both a function and tab completer for the "sport" subcommand parser_sport.set_defaults(func=base_sport) @with_argparser(base_parser) diff --git a/examples/submenus.py b/examples/submenus.py index 44b17f33..27c8cb10 100755 --- a/examples/submenus.py +++ b/examples/submenus.py @@ -11,7 +11,7 @@ of the submenu. Nesting of the submenus is done with the cmd2.AddSubmenu() decor from __future__ import print_function import sys -import cmd2 +from cmd2 import cmd2 from IPython import embed diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py index 6146b64b..f3302533 100755 --- a/examples/tab_autocompletion.py +++ b/examples/tab_autocompletion.py @@ -10,8 +10,8 @@ import argparse import itertools from typing import List -import cmd2 -from cmd2 import with_argparser, with_category, argparse_completer +from cmd2 import cmd2, argparse_completer +from cmd2.cmd2 import with_argparser, with_category actors = ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels', 'Adam Driver', 'Daisy Ridley', 'John Boyega', 'Oscar Isaac', diff --git a/examples/tab_completion.py b/examples/tab_completion.py index 919e9560..30fa283d 100755 --- a/examples/tab_completion.py +++ b/examples/tab_completion.py @@ -4,8 +4,8 @@ """ import argparse -import cmd2 -from cmd2 import with_argparser, with_argument_list +from cmd2 import cmd2 +from cmd2.cmd2 import with_argparser, with_argument_list # List of strings used with flag and index based completion functions food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato'] diff --git a/examples/table_display.py b/examples/table_display.py index 2e6ea804..5d168408 100755 --- a/examples/table_display.py +++ b/examples/table_display.py @@ -12,7 +12,7 @@ WARNING: This example requires the tabulate module. """ import functools -import cmd2 +from cmd2 import cmd2 import tabulate # Format to use with tabulate module when displaying tables diff --git a/tests/conftest.py b/tests/conftest.py index 90d45bd9..562ca4fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ import sys from pytest import fixture from unittest import mock -import cmd2 +from cmd2 import cmd2 # Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit) try: diff --git a/tests/test_argparse.py b/tests/test_argparse.py index 6a9a93a7..e23c5d17 100644 --- a/tests/test_argparse.py +++ b/tests/test_argparse.py @@ -5,7 +5,7 @@ Cmd2 testing for argument parsing import argparse import pytest -import cmd2 +from cmd2 import cmd2 from unittest import mock from .conftest import run_cmd, StdOut diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 48f50bdc..c8955497 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -21,7 +21,7 @@ try: except ImportError: from unittest import mock -import cmd2 +from cmd2 import cmd2 from .conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \ HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, StdOut @@ -109,8 +109,8 @@ def test_base_show_readonly(base_app): Strip Quotes after splitting arguments: {} """.format(base_app.terminators, base_app.allow_cli_args, base_app.allow_redirection, - "POSIX" if cmd2.cmd2.POSIX_SHLEX else "non-POSIX", - "True" if cmd2.cmd2.STRIP_QUOTES_FOR_NON_POSIX and not cmd2.cmd2.POSIX_SHLEX else "False")) + "POSIX" if cmd2.POSIX_SHLEX else "non-POSIX", + "True" if cmd2.STRIP_QUOTES_FOR_NON_POSIX and not cmd2.POSIX_SHLEX else "False")) assert out == expected @@ -647,18 +647,18 @@ def test_pipe_to_shell_error(base_app, capsys): assert err.startswith("EXCEPTION of type '{}' occurred with message:".format(expected_error)) -@pytest.mark.skipif(not cmd2.cmd2.can_clip, +@pytest.mark.skipif(not cmd2.can_clip, reason="Pyperclip could not find a copy/paste mechanism for your system") 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 + assert normalize(cmd2.get_paste_buffer()) == expected # 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 + assert normalize(cmd2.get_paste_buffer()) == expected def test_base_timing(base_app, capsys): @@ -1314,7 +1314,7 @@ optional arguments: reason="cmd2._which function only used on Mac and Linux") def test_which_editor_good(): editor = 'vi' - path = cmd2.cmd2._which(editor) + path = cmd2._which(editor) # Assert that the vi editor was found because it should exist on all Mac and Linux systems assert path @@ -1322,7 +1322,7 @@ def test_which_editor_good(): reason="cmd2._which function only used on Mac and Linux") def test_which_editor_bad(): editor = 'notepad.exe' - path = cmd2.cmd2._which(editor) + path = cmd2._which(editor) # Assert that the editor wasn't found because no notepad.exe on non-Windows systems ;-) assert path is None @@ -1349,7 +1349,7 @@ def multiline_app(): return app def test_multiline_complete_empty_statement_raises_exception(multiline_app): - with pytest.raises(cmd2.cmd2.EmptyStatement): + with pytest.raises(cmd2.EmptyStatement): multiline_app._complete_statement('') def test_multiline_complete_statement_without_terminator(multiline_app): @@ -1367,7 +1367,7 @@ def test_multiline_complete_statement_without_terminator(multiline_app): def test_clipboard_failure(capsys): # Force cmd2 clipboard to be disabled - cmd2.cmd2.disable_clip() + cmd2.disable_clip() app = cmd2.Cmd() # Redirect command output to the clipboard when a clipboard isn't present diff --git a/tests/test_completion.py b/tests/test_completion.py index ef35b635..7026db48 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -12,7 +12,7 @@ import argparse import os import sys -import cmd2 +from cmd2 import cmd2 import pytest from .conftest import complete_tester, StdOut from examples.subcommands import SubcommandsExample diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 2682ec68..b61e2d06 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -8,16 +8,14 @@ problematic because they worked properly for some versions of pyparsing but not Copyright 2017 Todd Leonhardt <todd.leonhardt@gmail.com> Released under MIT license, see LICENSE file """ -import sys - -import cmd2 +from cmd2 import cmd2 import pytest @pytest.fixture def hist(): from cmd2.cmd2 import HistoryItem - h = cmd2.cmd2.History([HistoryItem('first'), HistoryItem('second'), HistoryItem('third'), HistoryItem('fourth')]) + h = cmd2.History([HistoryItem('first'), HistoryItem('second'), HistoryItem('third'), HistoryItem('fourth')]) return h # Case-sensitive parser @@ -25,7 +23,7 @@ def hist(): def parser(): c = cmd2.Cmd() c.multilineCommands = ['multiline'] - c.parser_manager = cmd2.cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators, + c.parser_manager = cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators, multilineCommands=c.multilineCommands, legalChars=c.legalChars, commentGrammars=c.commentGrammars, commentInProgress=c.commentInProgress, blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser, @@ -38,7 +36,7 @@ def parser(): def cs_pm(): c = cmd2.Cmd() c.multilineCommands = ['multiline'] - c.parser_manager = cmd2.cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators, + c.parser_manager = cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators, multilineCommands=c.multilineCommands, legalChars=c.legalChars, commentGrammars=c.commentGrammars, commentInProgress=c.commentInProgress, blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser, @@ -77,7 +75,7 @@ def test_history_get(hist): def test_cast(): - cast = cmd2.cmd2.cast + cast = cmd2.cast # Boolean assert cast(True, True) == True @@ -101,7 +99,7 @@ def test_cast(): def test_cast_problems(capsys): - cast = cmd2.cmd2.cast + cast = cmd2.cast expected = 'Problem setting parameter (now {}) to {}; incorrect type?\n' @@ -327,8 +325,8 @@ def test_parse_input_redirect_from_unicode_filename(input_parser): def test_empty_statement_raises_exception(): app = cmd2.Cmd() - with pytest.raises(cmd2.cmd2.EmptyStatement): + with pytest.raises(cmd2.EmptyStatement): app._complete_statement('') - with pytest.raises(cmd2.cmd2.EmptyStatement): + with pytest.raises(cmd2.EmptyStatement): app._complete_statement(' ') diff --git a/tests/test_submenu.py b/tests/test_submenu.py index fbb9857b..db334daa 100644 --- a/tests/test_submenu.py +++ b/tests/test_submenu.py @@ -4,7 +4,7 @@ Cmd2 testing for argument parsing """ import pytest -import cmd2 +from cmd2 import cmd2 from .conftest import run_cmd, StdOut, normalize diff --git a/tests/test_transcript.py b/tests/test_transcript.py index 8ee5f3f6..4f821c06 100644 --- a/tests/test_transcript.py +++ b/tests/test_transcript.py @@ -14,8 +14,8 @@ import random from unittest import mock import pytest -import cmd2 -from cmd2 import set_posix_shlex, set_strip_quotes +from cmd2 import cmd2 +from cmd2.cmd2 import set_posix_shlex, set_strip_quotes from .conftest import run_cmd, StdOut, normalize class CmdLineApp(cmd2.Cmd): @@ -189,7 +189,7 @@ now: ---> assert out == expected -class TestMyAppCase(cmd2.cmd2.Cmd2TestCase): +class TestMyAppCase(cmd2.Cmd2TestCase): CmdApp = CmdLineApp CmdApp.testfiles = ['tests/transcript.txt'] @@ -293,7 +293,7 @@ def test_transcript(request, capsys, filename, feedback_to_output): def test_parse_transcript_expected(expected, transformed): app = CmdLineApp() - class TestMyAppCase(cmd2.cmd2.Cmd2TestCase): + class TestMyAppCase(cmd2.Cmd2TestCase): cmdapp = app testcase = TestMyAppCase() |