summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2017-03-12 22:07:06 -0400
committerGitHub <noreply@github.com>2017-03-12 22:07:06 -0400
commite1e563bbc5b84f8daf698d18cf9f6e2cf205bb83 (patch)
treeb43f46cc5ab3f30aed99592e83df2504f3c606bb
parent3a8b3baa72ac4429c6b2684a612a53484015151c (diff)
parent61562f8fbdea07bffebd240de7a85f7ad6af119a (diff)
downloadcmd2-git-e1e563bbc5b84f8daf698d18cf9f6e2cf205bb83.tar.gz
Merge pull request #74 from python-cmd2/more_cleanup
More cleanup
-rwxr-xr-xcmd2.py382
-rw-r--r--tests/test_parsing.py12
2 files changed, 237 insertions, 157 deletions
diff --git a/cmd2.py b/cmd2.py
index 9135fa37..987a4bcd 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -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):