diff options
-rwxr-xr-x | cmd2.py | 382 | ||||
-rw-r--r-- | tests/test_parsing.py | 12 |
2 files changed, 237 insertions, 157 deletions
@@ -178,9 +178,9 @@ def remaining_args(opts_plus_args, arg_list): :return: str - non-option arguments as a single string, with original spacing preserved """ pattern = '\s+'.join(re.escape(a) for a in arg_list) + '\s*$' - matchObj = re.search(pattern, opts_plus_args) + match_obj = re.search(pattern, opts_plus_args) try: - remaining = opts_plus_args[matchObj.start():] + remaining = opts_plus_args[match_obj.start():] except AttributeError: # Don't preserve spacing, but at least we don't crash and we do preserve args and their order remaining = ' '.join(arg_list) @@ -189,10 +189,10 @@ def remaining_args(opts_plus_args, arg_list): def _attr_get_(obj, attr): - '''Returns an attribute's value, or None (no error) if undefined. + """Returns an attribute's value, or None (no error) if undefined. Analagous to .get() for dictionaries. Useful when checking for value of options that may not have been defined on a given - method.''' + method.""" try: return getattr(obj, attr) except AttributeError: @@ -252,15 +252,15 @@ def options(option_list, arg_desc="arg"): :param func: do_* method which uses the @options decorator :return: modified version of the do_* method """ - optionParser = OptionParser() - for opt in option_list: - optionParser.add_option(opt) + option_parser = OptionParser() + for option in option_list: + option_parser.add_option(option) # Allow reasonable help for commands defined with @options and an empty list of options if len(option_list) > 0: - optionParser.set_usage("%s [options] %s" % (func.__name__[3:], arg_desc)) + option_parser.set_usage("%s [options] %s" % (func.__name__[3:], arg_desc)) else: - optionParser.set_usage("%s %s" % (func.__name__[3:], arg_desc)) - optionParser._func = func + option_parser.set_usage("%s %s" % (func.__name__[3:], arg_desc)) + option_parser._func = func def new_func(instance, arg): """For @options commands this replaces the actual do_* methods in the instance __dict__. @@ -273,14 +273,14 @@ def options(option_list, arg_desc="arg"): """ try: # Use shlex to split the command line into a list of arguments based on shell rules - opts, newArgList = optionParser.parse_args(shlex.split(arg, posix=POSIX_SHLEX)) + opts, new_arglist = option_parser.parse_args(shlex.split(arg, posix=POSIX_SHLEX)) # If not using POSIX shlex, make sure to strip off outer quotes for convenience if not POSIX_SHLEX and STRIP_QUOTES_FOR_NON_POSIX: - new_arg_list = [] - for arg in newArgList: - new_arg_list.append(strip_quotes(arg)) - newArgList = new_arg_list + temp_arglist = [] + for arg in new_arglist: + temp_arglist.append(strip_quotes(arg)) + new_arglist = temp_arglist # Also strip off outer quotes on string option values for key, val in opts.__dict__.items(): @@ -289,26 +289,26 @@ def options(option_list, arg_desc="arg"): # Must find the remaining args in the original argument list, but # mustn't include the command itself - # if hasattr(arg, 'parsed') and newArgList[0] == arg.parsed.command: - # newArgList = newArgList[1:] + # if hasattr(arg, 'parsed') and new_arglist[0] == arg.parsed.command: + # new_arglist = new_arglist[1:] if USE_ARG_LIST: - arg = newArgList + arg = new_arglist else: - newArgs = remaining_args(arg, newArgList) + new_args = remaining_args(arg, new_arglist) if isinstance(arg, ParsedString): - arg = arg.with_args_replaced(newArgs) + arg = arg.with_args_replaced(new_args) else: - arg = newArgs + arg = new_args except optparse.OptParseError as e: print(e) - optionParser.print_help() + option_parser.print_help() return if hasattr(opts, '_exit'): return None result = func(instance, arg, opts) return result - new_func.__doc__ = '%s\n%s' % (func.__doc__, optionParser.format_help()) + new_func.__doc__ = '%s\n%s' % (func.__doc__, option_parser.format_help()) return new_func return option_setup @@ -352,6 +352,7 @@ if sys.platform == "win32": can_clip = True except ImportError: + # noinspection PyUnusedLocal def get_paste_buffer(*args): """For Windows OSes without the appropriate libary installed to get text from clipboard, raise an exception. """ @@ -391,6 +392,7 @@ elif sys.platform == 'darwin': stderr=subprocess.PIPE) pbcopyproc.communicate(txt.encode()) else: + # noinspection PyUnusedLocal def get_paste_buffer(*args): """For Mac OS X without the appropriate tool installed to get text from clipboard, raise an exception.""" raise OSError(pastebufferr % ('pbcopy', @@ -401,9 +403,10 @@ else: # Running on Linux try: with open(os.devnull, 'w') as DEVNULL: - subprocess.check_call('xclip -o -sel clip', shell=True, stdin=subprocess.PIPE, stdout=DEVNULL, stderr=DEVNULL) + subprocess.check_call('xclip -o -sel clip', shell=True, stdin=subprocess.PIPE, stdout=DEVNULL, + stderr=DEVNULL) can_clip = True - except Exception: + except (subprocess.CalledProcessError, OSError, IOError): pass # something went wrong with xclip and we cannot use it if can_clip: def get_paste_buffer(): @@ -428,6 +431,7 @@ else: xclipproc.stdin.write(txt.encode()) xclipproc.stdin.close() else: + # noinspection PyUnusedLocal def get_paste_buffer(*args): """For Linux without the appropriate tool installed to get text from clipboard, raise an exception.""" raise OSError(pastebufferr % ('xclip', 'On Debian/Ubuntu, install with "sudo apt-get install xclip"')) @@ -483,17 +487,17 @@ class StubbornDict(dict): def __add__(self, arg): selfcopy = copy.copy(self) - selfcopy.update(stubbornDict(arg)) + selfcopy.update(stubborn_dict(arg)) return selfcopy def __radd__(self, arg): selfcopy = copy.copy(self) - selfcopy.update(stubbornDict(arg)) + selfcopy.update(stubborn_dict(arg)) return selfcopy @classmethod def to_dict(cls, arg): - 'Generates dictionary from string or list of strings' + """Generates dictionary from string or list of strings""" if hasattr(arg, 'splitlines'): arg = arg.splitlines() if hasattr(arg, '__reversed__'): @@ -513,7 +517,7 @@ class StubbornDict(dict): return result -def stubbornDict(*arg, **kwarg): +def stubborn_dict(*arg, **kwarg): """ Factory function which creates instances of the StubornDict class. :param arg: an argument which could be used to construct a built-in dict dictionary @@ -620,7 +624,7 @@ class Cmd(cmd.Cmd): timing = False # Prints elapsed time for each command # To make an attribute settable with the "do_set" command, add it to this ... - settable = stubbornDict(''' + settable = stubborn_dict(''' abbrev Accept abbreviated commands autorun_on_edit Automatically run files after editing case_insensitive upper- and lower-case both OK @@ -650,7 +654,7 @@ class Cmd(cmd.Cmd): if not use_ipython: try: del Cmd.do_ipy - except Exception: + except AttributeError: pass # Call super class constructor. Need to do it in this way for Python 2 and 3 compatibility @@ -732,6 +736,7 @@ class Cmd(cmd.Cmd): return self.colorcodes[color][True] + val + self.colorcodes[color][False] return val + # noinspection PyUnusedLocal def do_cmdenvironment(self, args): """Summary report of interactive parameters.""" self.stdout.write(""" @@ -755,6 +760,7 @@ class Cmd(cmd.Cmd): else: cmd.Cmd.do_help(self, arg) + # noinspection PyUnusedLocal def do_shortcuts(self, args): """Lists single-key shortcuts available.""" result = "\n".join('%s: %s' % (sc[0], sc[1]) for sc in sorted(self.shortcuts)) @@ -762,67 +768,67 @@ class Cmd(cmd.Cmd): def _init_parser(self): """ Initializes everything related to pyparsing. """ - # outputParser = (pyparsing.Literal('>>') | (pyparsing.WordStart() + '>') | pyparsing.Regex('[^=]>'))('output') - outputParser = (pyparsing.Literal(self.redirector * 2) | - (pyparsing.WordStart() + self.redirector) | - pyparsing.Regex('[^=]' + self.redirector))('output') + # output_parser = (pyparsing.Literal('>>') | (pyparsing.WordStart() + '>') | pyparsing.Regex('[^=]>'))('output') + output_parser = (pyparsing.Literal(self.redirector * 2) | + (pyparsing.WordStart() + self.redirector) | + pyparsing.Regex('[^=]' + self.redirector))('output') - terminatorParser = pyparsing.Or( + terminator_parser = pyparsing.Or( [(hasattr(t, 'parseString') and t) or pyparsing.Literal(t) for t in self.terminators])('terminator') - stringEnd = pyparsing.stringEnd ^ '\nEOF' + string_end = pyparsing.stringEnd ^ '\nEOF' self.multilineCommand = pyparsing.Or( [pyparsing.Keyword(c, caseless=self.case_insensitive) for c in self.multilineCommands])('multilineCommand') - oneLineCommand = (~self.multilineCommand + pyparsing.Word(self.legalChars))('command') + oneline_command = (~self.multilineCommand + pyparsing.Word(self.legalChars))('command') pipe = pyparsing.Keyword('|', identChars='|') self.commentGrammars.ignore(pyparsing.quotedString).setParseAction(lambda x: '') - doNotParse = self.commentGrammars | self.commentInProgress | pyparsing.quotedString - afterElements = \ - pyparsing.Optional(pipe + pyparsing.SkipTo(outputParser ^ stringEnd, ignore=doNotParse)('pipeTo')) + \ + do_not_parse = self.commentGrammars | self.commentInProgress | pyparsing.quotedString + after_elements = \ + pyparsing.Optional(pipe + pyparsing.SkipTo(output_parser ^ string_end, ignore=do_not_parse)('pipeTo')) + \ pyparsing.Optional( - outputParser + pyparsing.SkipTo(stringEnd, ignore=doNotParse).setParseAction(lambda x: x[0].strip())( + output_parser + pyparsing.SkipTo(string_end, ignore=do_not_parse).setParseAction(lambda x: x[0].strip())( 'outputTo')) if self.case_insensitive: self.multilineCommand.setParseAction(lambda x: x[0].lower()) - oneLineCommand.setParseAction(lambda x: x[0].lower()) + oneline_command.setParseAction(lambda x: x[0].lower()) if self.blankLinesAllowed: self.blankLineTerminationParser = pyparsing.NoMatch else: self.blankLineTerminator = (pyparsing.lineEnd + pyparsing.lineEnd)('terminator') self.blankLineTerminator.setResultsName('terminator') - self.blankLineTerminationParser = ((self.multilineCommand ^ oneLineCommand) + - pyparsing.SkipTo(self.blankLineTerminator, ignore=doNotParse).setParseAction( + self.blankLineTerminationParser = ((self.multilineCommand ^ oneline_command) + + pyparsing.SkipTo(self.blankLineTerminator, ignore=do_not_parse).setParseAction( lambda x: x[0].strip())('args') + self.blankLineTerminator)('statement') - self.multilineParser = (((self.multilineCommand ^ oneLineCommand) + pyparsing.SkipTo(terminatorParser, - ignore=doNotParse).setParseAction( - lambda x: x[0].strip())('args') + terminatorParser)('statement') + - pyparsing.SkipTo(outputParser ^ pipe ^ stringEnd, ignore=doNotParse).setParseAction( - lambda x: x[0].strip())('suffix') + afterElements) + self.multilineParser = (((self.multilineCommand ^ oneline_command) + pyparsing.SkipTo(terminator_parser, + ignore=do_not_parse).setParseAction( + lambda x: x[0].strip())('args') + terminator_parser)('statement') + + pyparsing.SkipTo(output_parser ^ pipe ^ string_end, ignore=do_not_parse).setParseAction( + lambda x: x[0].strip())('suffix') + after_elements) self.multilineParser.ignore(self.commentInProgress) - self.singleLineParser = ((oneLineCommand + pyparsing.SkipTo(terminatorParser ^ stringEnd ^ pipe ^ outputParser, - ignore=doNotParse).setParseAction( + self.singleLineParser = ((oneline_command + pyparsing.SkipTo(terminator_parser ^ string_end ^ pipe ^ output_parser, + ignore=do_not_parse).setParseAction( lambda x: x[0].strip())('args'))('statement') + - pyparsing.Optional(terminatorParser) + afterElements) + pyparsing.Optional(terminator_parser) + after_elements) # self.multilineParser = self.multilineParser.setResultsName('multilineParser') # self.singleLineParser = self.singleLineParser.setResultsName('singleLineParser') self.blankLineTerminationParser = self.blankLineTerminationParser.setResultsName('statement') self.parser = self.prefixParser + ( - stringEnd | + string_end | self.multilineParser | self.singleLineParser | self.blankLineTerminationParser | - self.multilineCommand + pyparsing.SkipTo(stringEnd, ignore=doNotParse) + self.multilineCommand + pyparsing.SkipTo(string_end, ignore=do_not_parse) ) self.parser.ignore(self.commentGrammars) - inputMark = pyparsing.Literal('<') - inputMark.setParseAction(lambda x: '') - fileName = pyparsing.Word(self.legalChars + '/\\') - inputFrom = fileName('inputFrom') - inputFrom.setParseAction(replace_with_file_contents) + input_mark = pyparsing.Literal('<') + input_mark.setParseAction(lambda x: '') + file_name = pyparsing.Word(self.legalChars + '/\\') + input_from = file_name('inputFrom') + input_from.setParseAction(replace_with_file_contents) # a not-entirely-satisfactory way of distinguishing < as in "import from" from < # as in "lesser than" - self.inputParser = inputMark + pyparsing.Optional(inputFrom) + pyparsing.Optional('>') + \ - pyparsing.Optional(fileName) + (pyparsing.stringEnd | '|') + self.inputParser = input_mark + pyparsing.Optional(input_from) + pyparsing.Optional('>') + \ + pyparsing.Optional(file_name) + (pyparsing.stringEnd | '|') self.inputParser.ignore(self.commentInProgress) # noinspection PyMethodMayBeStatic @@ -929,37 +935,33 @@ class Cmd(cmd.Cmd): :param line: str - line of text read from inp :return: bool - True if cmdloop() should exit, False otherwise """ - # The outermost level of try/finally nesting can be condensed once - # Python 2.4 support can be dropped. stop = 0 try: - statement = '' + statement = self.complete_statement(line) + (stop, statement) = self.postparsing_precmd(statement) + if stop: + return self.postparsing_postcmd(stop) + if statement.parsed.command not in self.excludeFromHistory: + self.history.append(statement.parsed.raw) try: - statement = self.complete_statement(line) - (stop, statement) = self.postparsing_precmd(statement) - if stop: - return self.postparsing_postcmd(stop) - if statement.parsed.command not in self.excludeFromHistory: - self.history.append(statement.parsed.raw) - try: - if self.allow_redirection: - self.redirect_output(statement) - timestart = datetime.datetime.now() - statement = self.precmd(statement) - stop = self.onecmd(statement) - stop = self.postcmd(stop, statement) - if self.timing: - self.pfeedback('Elapsed: %s' % str(datetime.datetime.now() - timestart)) - finally: - if self.allow_redirection: - self.restore_output(statement) - except EmptyStatement: - pass - except ValueError as ex: - # If shlex.split failed on syntax, let user know whats going on - self.perror("Invalid syntax: {}".format(ex), traceback_war=False) - except Exception as ex: - self.perror(ex, type(ex).__name__) + if self.allow_redirection: + self.redirect_output(statement) + timestart = datetime.datetime.now() + statement = self.precmd(statement) + stop = self.onecmd(statement) + stop = self.postcmd(stop, statement) + if self.timing: + self.pfeedback('Elapsed: %s' % str(datetime.datetime.now() - timestart)) + finally: + if self.allow_redirection: + self.restore_output(statement) + except EmptyStatement: + pass + except ValueError as ex: + # If shlex.split failed on syntax, let user know whats going on + self.perror("Invalid syntax: {}".format(ex), traceback_war=False) + except Exception as ex: + self.perror(ex, type(ex).__name__) finally: return self.postparsing_postcmd(stop) @@ -1098,6 +1100,10 @@ class Cmd(cmd.Cmd): """Repeatedly issue a prompt, accept input, parse an initial prefix off the received input, and dispatch to action methods, passing them the remainder of the line as argument. + + This serves the same role as cmd.cmdloop(). + + :return: bool - True implies the entire application should exit. """ # An almost perfect copy from Cmd; however, the pseudo_raw_input portion # has been split out so that it can be called separately @@ -1113,6 +1119,7 @@ class Cmd(cmd.Cmd): stop = None try: while not stop: + # NOTE: cmdqueue appears completely unused, but it is defined in cmd.Cmd, so we are leaving it here if self.cmdqueue: line = self.cmdqueue.pop(0) else: @@ -1130,29 +1137,31 @@ class Cmd(cmd.Cmd): pass return stop + # noinspection PyUnusedLocal def do_eof(self, arg): - """Automatically called at end of loading a script.""" - return self._STOP_SCRIPT_NO_EXIT # End of script; should not exit app + """Automatically called at end of loading a script or when <Ctrl>-D is pressed.""" + # End of script should not exit app, but <Ctrl>-D should. + return self._STOP_SCRIPT_NO_EXIT def do_quit(self, arg): """Exits this application.""" self._should_quit = True return self._STOP_AND_EXIT - def select(self, options, prompt='Your choice? '): + def select(self, opts, prompt='Your choice? '): """Presents a numbered menu to the user. Modelled after the bash shell's SELECT. Returns the item chosen. - Argument ``options`` can be: + Argument ``opts`` can be: | a single string -> will be split into one-word options | a list of strings -> will be offered as options | a list of tuples -> interpreted as (value, text), so that the return value can differ from the text advertised to the user """ - local_opts = options - if isinstance(options, string_types): - local_opts = list(zip(options.split(), options.split())) + local_opts = opts + if isinstance(opts, string_types): + local_opts = list(zip(opts.split(), opts.split())) fulloptions = [] for opt in local_opts: if isinstance(opt, string_types): @@ -1209,29 +1218,29 @@ class Cmd(cmd.Cmd): Call without arguments for a list of settable parameters with their values. """ try: - statement, paramName, val = arg.parsed.raw.split(None, 2) + statement, param_name, val = arg.parsed.raw.split(None, 2) val = val.strip() - paramName = paramName.strip().lower() - if paramName not in self.settable: - hits = [p for p in self.settable if p.startswith(paramName)] + param_name = param_name.strip().lower() + if param_name not in self.settable: + hits = [p for p in self.settable if p.startswith(param_name)] if len(hits) == 1: - paramName = hits[0] + param_name = hits[0] else: - return self.do_show(paramName) - currentVal = getattr(self, paramName) + return self.do_show(param_name) + current_val = getattr(self, param_name) if (val[0] == val[-1]) and val[0] in ("'", '"'): val = val[1:-1] else: - val = cast(currentVal, val) - setattr(self, paramName, val) - self.stdout.write('%s - was: %s\nnow: %s\n' % (paramName, currentVal, val)) - if currentVal != val: + val = cast(current_val, val) + setattr(self, param_name, val) + self.stdout.write('%s - was: %s\nnow: %s\n' % (param_name, current_val, val)) + if current_val != val: try: - onchange_hook = getattr(self, '_onchange_%s' % paramName) - onchange_hook(old=currentVal, new=val) + onchange_hook = getattr(self, '_onchange_%s' % param_name) + onchange_hook(old=current_val, new=val) except AttributeError: pass - except (ValueError, AttributeError, NotSettableError): + except (ValueError, AttributeError): self.do_show(arg) # noinspection PyMethodMayBeStatic @@ -1285,24 +1294,24 @@ class Cmd(cmd.Cmd): arg = arg.parsed.raw[2:].strip() # Support the run command even if called prior to invoking an interactive interpreter - def run(arg): + def run(filename): """Run a Python script file in the interactive console. - :param arg: str - filename of *.py script file to run + :param filename: str - filename of *.py script file to run """ try: - with open(arg) as f: + with open(filename) as f: interp.runcode(f.read()) except IOError as e: self.perror(e) - def onecmd_plus_hooks(arg): + def onecmd_plus_hooks(cmd_plus_args): """Run a cmd2.Cmd command from a Python script or the interactive Python console. - :param arg: str - command line including command and arguments to run + :param cmd_plus_args: str - command line including command and arguments to run :return: bool - True if cmdloop() should exit once leaving the interactive Python console, False otherwise. """ - return self.onecmd_plus_hooks(arg + '\n') + return self.onecmd_plus_hooks(cmd_plus_args + '\n') self.pystate['run'] = run self.pystate['cmd'] = onecmd_plus_hooks @@ -1314,6 +1323,7 @@ class Cmd(cmd.Cmd): if arg.strip(): interp.runcode(arg) else: + # noinspection PyShadowingBuiltins def quit(): """Function callable from the interactive Python console to exit that environment and return to cmd2.""" raise EmbeddedConsoleExit @@ -1338,7 +1348,7 @@ class Cmd(cmd.Cmd): # Only include the do_ipy() method if IPython is available on the system if ipython_available: - # noinspection PyMethodMayBeStatic + # noinspection PyMethodMayBeStatic,PyUnusedLocal def do_ipy(self, arg): """Enters an interactive IPython shell. @@ -1641,7 +1651,7 @@ Script should contain one command per line, just like command would be typed in runme = self.last_matching(arg) self.pfeedback(runme) if runme: - stop = self.onecmd_plus_hooks(runme) + return self.onecmd_plus_hooks(runme) def help_run(self): """Print help for do_run().""" @@ -1653,7 +1663,14 @@ Script should contain one command per line, just like command would be typed in arg is /enclosed in forward-slashes/ -> run most recent by regex""" self.stdout.write("{}\n".format(help_str)) - def runTranscriptTests(self, callargs): + def run_transcript_tests(self, callargs): + """Runs transcript tests for provided file(s). + + This is called when either -t is provided on the command line or the transcript_files argument is provided + during construction of the cmd2.Cmd instance. + + :param callargs: List[str] - list of transcript test file names + """ class TestMyAppCase(Cmd2TestCase): CmdApp = self.__class__ @@ -1665,11 +1682,26 @@ Script should contain one command per line, just like command would be typed in result.printErrors() def run_commands_at_invocation(self, callargs): + """Runs commands provided as arguments on the command line when the application is started. + + :param callargs: List[str] - list of strings where each string is a command plus its arguments + :return: bool - True implies the entire application should exit + """ for initial_command in callargs: if self.onecmd_plus_hooks(initial_command + '\n'): return self._STOP_AND_EXIT def cmdloop(self, intro=None): + """This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2. + + _cmdloop() provides the main loop equivalent to cmd.cmdloop(). This is a wrapper around that which deals with + the following extra feactures provided by cmd2: + - commands at invocation + - transcript testing + - intro banner + + :param intro: str - if provided this overrides self.intro and serves as the intro banner printed once at start + """ callargs = None if self.allow_cli_args: parser = optparse.OptionParser() @@ -1681,7 +1713,7 @@ Script should contain one command per line, just like command would be typed in self._transcript_files = callargs if self._transcript_files is not None: - self.runTranscriptTests(self._transcript_files) + self.run_transcript_tests(self._transcript_files) else: # Always run the preloop first self.preloop() @@ -1708,14 +1740,24 @@ Script should contain one command per line, just like command would be typed in class HistoryItem(str): + """Class used to represent an item in the History list. + + Thing wrapper around str class which adds a custom format for printing. It also keeps track of its index in the + list as well as a lowercase representation of itself for convenience/efficiency. + """ listformat = '-------------------------[%d]\n%s\n' + # noinspection PyUnusedLocal def __init__(self, instr): str.__init__(self) self.lowercase = self.lower() self.idx = None def pr(self): + """Represent a HistoryItem in a pretty fashion suitable for printing. + + :return: str - pretty print string version of a HistoryItem + """ return self.listformat % (self.idx, str(self)) @@ -1757,6 +1799,11 @@ class History(list): spanpattern = re.compile(r'^\s*(?P<start>-?\d+)?\s*(?P<separator>:|(\.{2,}))?\s*(?P<end>-?\d+)?\s*$') def span(self, raw): + """Parses the input string search for a span pattern and if if found, returns a slice from the History list. + + :param raw: str - string potentially containing a span of the forms a..b, a:b, a:, ..b + :return: List[HistoryItem] - slice from the History list + """ if raw.lower() in ('*', '-', 'all'): raw = ':' results = self.spanpattern.search(raw) @@ -1780,15 +1827,20 @@ class History(list): rangePattern = re.compile(r'^\s*(?P<start>[\d]+)?\s*-\s*(?P<end>[\d]+)?\s*$') def append(self, new): + """Append a HistoryItem to end of the History list + + :param new: str - command line to convert to HistoryItem and add to the end of the History list + """ new = HistoryItem(new) list.append(self, new) new.idx = len(self) - def extend(self, new): - for n in new: - self.append(n) - - def get(self, getme=None, fromEnd=False): + def get(self, getme=None): + """Get an item or items from the History list using 1-based indexing. + + :param getme: int or str - item(s) to get - either an integer index or string to search for + :return: List[str] - list of HistoryItems matching the retrieval criteria + """ if not getme: return self try: @@ -1800,10 +1852,10 @@ class History(list): except IndexError: return [] except ValueError: - rangeResult = self.rangePattern.search(getme) - if rangeResult: - start = rangeResult.group('start') or None - end = rangeResult.group('start') or None + range_result = self.rangePattern.search(getme) + if range_result: + start = range_result.group('start') or None + end = range_result.group('start') or None if start: start = int(start) - 1 if end: @@ -1817,19 +1869,30 @@ class History(list): finder = re.compile(getme[1:-1], re.DOTALL | re.MULTILINE | re.IGNORECASE) def isin(hi): + """Listcomp filter function for doing a regular expression search of History. + + :param hi: HistoryItem + :return: bool - True if search matches + """ return finder.search(hi) else: def isin(hi): + """Listcomp filter function for doing a case-insensitive string search of History. + + :param hi: HistoryItem + :return: bool - True if search matches + """ return getme.lower() in hi.lowercase return [itm for itm in self if isin(itm)] -class NotSettableError(Exception): - pass - - def cast(current, new): - """Tries to force a new value into the same type as the current.""" + """Tries to force a new value into the same type as the current when trying to set the value for a parameter. + + :param current: current value for the parameter, type varies + :param new: str - new value + :return: new value with same type as current, or the current value if there was an error casting + """ typ = type(current) if typ == bool: try: @@ -1838,7 +1901,7 @@ def cast(current, new): pass try: new = new.lower() - except: + except AttributeError: pass if (new == 'on') or (new[0] in ('y', 't')): return True @@ -1847,24 +1910,32 @@ def cast(current, new): else: try: return typ(new) - except: + except (ValueError, TypeError): pass print("Problem setting parameter (now %s) to %s; incorrect type?" % (current, new)) return current class Statekeeper(object): + """Class used to save and restore state during load and py commands as well as when redirecting output or pipes.""" def __init__(self, obj, attribs): + """Use the instance attributes as a generic key-value store to copy instance attributes from outer object. + + :param obj: instance of cmd2.Cmd derived class (your application instance) + :param attribs: Tuple[str] - tuple of strings listing attributes of obj to save a copy of + """ self.obj = obj self.attribs = attribs if self.obj: - self.save() + self._save() - def save(self): + def _save(self): + """Create copies of attributes from self.obj inside this Statekeeper instance.""" for attrib in self.attribs: setattr(self, attrib, getattr(self.obj, attrib)) def restore(self): + """Overwrite attributes in self.obj with the saved values stored in this Statekeeper instance.""" if self.obj: for attrib in self.attribs: setattr(self.obj, attrib, getattr(self, attrib)) @@ -1891,22 +1962,31 @@ class OutputTrap(Borg): sys.stdout = self def write(self, txt): + """Add text to the internal contents. + + :param txt: str + """ self.contents += txt def read(self): + """Read from the internal contents and then clear them out. + + :return: str - text from the internal contents + """ result = self.contents self.contents = '' return result - def tearDown(self): + def tear_down(self): + """Restores normal output.""" sys.stdout = self.old_stdout self.contents = '' class Cmd2TestCase(unittest.TestCase): - '''Subclass this, setting CmdApp, to make a unittest.TestCase class + """Subclass this, setting CmdApp, to make a unittest.TestCase class that will execute the commands in a transcript file and expect the results shown. - See example.py''' + See example.py""" CmdApp = None def fetchTranscripts(self): @@ -1939,10 +2019,10 @@ class Cmd2TestCase(unittest.TestCase): anyWhitespace = re.compile(r'\s', re.DOTALL | re.MULTILINE) def _test_transcript(self, fname, transcript): - lineNum = 0 + line_num = 0 finished = False line = next(transcript) - lineNum += 1 + line_num += 1 while not finished: # Scroll forward to where actual commands begin while not line.startswith(self.cmdapp.prompt): @@ -1951,7 +2031,7 @@ class Cmd2TestCase(unittest.TestCase): except StopIteration: finished = True break - lineNum += 1 + line_num += 1 command = [line[len(self.cmdapp.prompt):]] line = next(transcript) # Read the entirety of a multi-line command @@ -1961,10 +2041,10 @@ class Cmd2TestCase(unittest.TestCase): line = next(transcript) except StopIteration: raise (StopIteration, - 'Transcript broke off while reading command beginning at line {} with\n{}'.format(lineNum, + 'Transcript broke off while reading command beginning at line {} with\n{}'.format(line_num, command[0]) ) - lineNum += 1 + line_num += 1 command = ''.join(command) # Send the command into the application and capture the resulting output stop = self.cmdapp.onecmd_plus_hooks(command) @@ -1973,7 +2053,7 @@ class Cmd2TestCase(unittest.TestCase): # Read the expected result from transcript if line.startswith(self.cmdapp.prompt): message = '\nFile %s, line %d\nCommand was:\n%r\nExpected: (nothing)\nGot:\n%r\n' % \ - (fname, lineNum, command, result) + (fname, line_num, command, result) self.assert_(not (result.strip()), message) continue expected = [] @@ -1984,11 +2064,11 @@ class Cmd2TestCase(unittest.TestCase): except StopIteration: finished = True break - lineNum += 1 + line_num += 1 expected = ''.join(expected) # Compare actual result to expected message = '\nFile %s, line %d\nCommand was:\n%s\nExpected:\n%s\nGot:\n%s\n' % \ - (fname, lineNum, command, expected, result) + (fname, line_num, command, expected, result) expected = self.expectationParser.transformString(expected) # checking whitespace is a pain - let's skip it expected = self.anyWhitespace.sub('', expected) @@ -1997,7 +2077,7 @@ class Cmd2TestCase(unittest.TestCase): def tearDown(self): if self.CmdApp: - self.outputTrap.tearDown() + self.outputTrap.tear_down() #noinspection PyClassHasNoInit diff --git a/tests/test_parsing.py b/tests/test_parsing.py index f073bc01..d6b64093 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -51,13 +51,13 @@ def test_stubborn_dict_class(): ('shoe', 'schuh'), ('small', 'klein')] def test_stubborn_dict_factory(): - assert sorted(cmd2.stubbornDict('cow a bovine\nhorse an equine').items()) == [('cow', 'a bovine'), - ('horse', 'an equine')] - assert sorted(cmd2.stubbornDict(['badger', 'porcupine a poky creature']).items()) == [('badger', ''), - ('porcupine', + assert sorted(cmd2.stubborn_dict('cow a bovine\nhorse an equine').items()) == [('cow', 'a bovine'), + ('horse', 'an equine')] + assert sorted(cmd2.stubborn_dict(['badger', 'porcupine a poky creature']).items()) == [('badger', ''), + ('porcupine', 'a poky creature')] - assert sorted(cmd2.stubbornDict(turtle='has shell', frog='jumpy').items()) == [('frog', 'jumpy'), - ('turtle', 'has shell')] + assert sorted(cmd2.stubborn_dict(turtle='has shell', frog='jumpy').items()) == [('frog', 'jumpy'), + ('turtle', 'has shell')] def test_history_span(hist): |