summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2017-03-12 18:58:10 -0400
committerGitHub <noreply@github.com>2017-03-12 18:58:10 -0400
commit3a8b3baa72ac4429c6b2684a612a53484015151c (patch)
tree5f7e0473008eed4a54616251f98841508a39d7c6
parente1dab4eeaa471344bf9d4f72b82d860339dfa710 (diff)
parentc811e3da2f73be8c9cba1f127a8f0e1f7be51aa8 (diff)
downloadcmd2-git-3a8b3baa72ac4429c6b2684a612a53484015151c.tar.gz
Merge pull request #73 from python-cmd2/cleanup
Cleanup
-rwxr-xr-xcmd2.py220
1 files changed, 173 insertions, 47 deletions
diff --git a/cmd2.py b/cmd2.py
index 5ef4fe8e..9135fa37 100755
--- a/cmd2.py
+++ b/cmd2.py
@@ -84,6 +84,9 @@ __version__ = '0.7.1a'
# Pyparsing enablePackrat() can greatly speed up parsing, but problems have been seen in Python 3 in the past
pyparsing.ParserElement.enablePackrat()
+# Override the default whitespace chars in Pyparsing so that newlines are not treated as whitespace
+pyparsing.ParserElement.setDefaultWhitespaceChars(' \t')
+
# The next 3 variables and associated setter funtions effect how arguments are parsed for commands using @options.
# The defaults are "sane" and maximize backward compatibility with cmd and previous versions of cmd2.
@@ -127,7 +130,7 @@ def set_use_arg_list(val):
class OptionParser(optparse.OptionParser):
- """Subclase of optparse.OptionParser which stores a reference to the function/method it is parsing options for.
+ """Subclass of optparse.OptionParser which stores a reference to the do_* method it is parsing options for.
Used mostly for getting access to the do_* method's docstring when printing help.
"""
@@ -138,11 +141,19 @@ class OptionParser(optparse.OptionParser):
self._func = None
def exit(self, status=0, msg=None):
+ """Called at the end of showing help when either -h is used to show help or when bad arguments are provided.
+
+ We override exit so it doesn't automatically exit the applicaiton.
+ """
self.values._exit = True
if msg:
print(msg)
def print_help(self, *args, **kwargs):
+ """Called when optparse encounters either -h or --help or bad arguments. It prints help for options.
+
+ We override it so that before the standard optparse help, it prints the do_* method docstring, if available.
+ """
try:
print(self._func.__doc__)
except AttributeError:
@@ -215,7 +226,7 @@ options_defined = [] # used to distinguish --options from SQL-style --comments
def options(option_list, arg_desc="arg"):
- '''Used as a decorator and passed a list of optparse-style options,
+ """Used as a decorator and passed a list of optparse-style options,
alters a cmd2 method to populate its ``opts`` argument from its
raw text argument.
@@ -229,13 +240,18 @@ def options(option_list, arg_desc="arg"):
def do_something(self, arg, opts):
if opts.quick:
self.fast_button = True
- '''
+ """
if not isinstance(option_list, list):
option_list = [option_list]
for opt in option_list:
options_defined.append(pyparsing.Literal(opt.get_opt_string()))
def option_setup(func):
+ """Decorator function which modifies on of the do_* methods that use the @options decorator.
+
+ :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)
@@ -247,6 +263,14 @@ def options(option_list, arg_desc="arg"):
optionParser._func = func
def new_func(instance, arg):
+ """For @options commands this replaces the actual do_* methods in the instance __dict__.
+
+ First it does all of the option/argument parsing. Then it calls the underlying do_* method.
+
+ :param instance: cmd2.Cmd2 derived class application instance
+ :param arg: str - command-line arguments provided to the comman
+ :return: bool - returns whatever the result of calling the underlying do_* method would be
+ """
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))
@@ -290,23 +314,7 @@ def options(option_list, arg_desc="arg"):
return option_setup
-class PasteBufferError(EnvironmentError):
- if sys.platform[:3] == 'win':
- errmsg = """Redirecting to or from paste buffer requires pywin32
-to be installed on operating system.
-Download from http://sourceforge.net/projects/pywin32/"""
- elif sys.platform[:3] == 'dar':
- # Use built in pbcopy on Mac OSX
- pass
- else:
- errmsg = """Redirecting to or from paste buffer requires xclip
-to be installed on operating system.
-On Debian/Ubuntu, 'sudo apt-get install xclip' will install it."""
-
- def __init__(self):
- Exception.__init__(self, self.errmsg)
-
-
+# Prefix to use on all OSes when the appropriate library or CLI tool isn't installed for getting access to paste buffer
pastebufferr = """Redirecting to or from paste buffer requires %s
to be installed on operating system.
%s"""
@@ -320,6 +328,10 @@ if sys.platform == "win32":
import win32clipboard
def get_paste_buffer():
+ """Get the contents of the clipboard for Windows OSes.
+
+ :return: str - contents of the clipboard
+ """
win32clipboard.OpenClipboard(0)
try:
result = win32clipboard.GetClipboardData()
@@ -329,6 +341,10 @@ if sys.platform == "win32":
return result
def write_to_paste_buffer(txt):
+ """Paste text to the clipboard for Windows OSes.
+
+ :param txt: str - text to paste to the clipboard
+ """
win32clipboard.OpenClipboard(0)
win32clipboard.EmptyClipboard()
win32clipboard.SetClipboardText(txt)
@@ -337,6 +353,8 @@ if sys.platform == "win32":
can_clip = True
except ImportError:
def get_paste_buffer(*args):
+ """For Windows OSes without the appropriate libary installed to get text from clipboard, raise an exception.
+ """
raise OSError(pastebufferr % ('pywin32', 'Download from http://sourceforge.net/projects/pywin32/'))
write_to_paste_buffer = get_paste_buffer
@@ -356,18 +374,27 @@ elif sys.platform == 'darwin':
pass
if can_clip:
def get_paste_buffer():
+ """Get the contents of the clipboard for Mac OS X.
+
+ :return: str - contents of the clipboard
+ """
pbcopyproc = subprocess.Popen('pbcopy -help', shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
stderr=subprocess.PIPE)
return pbcopyproc.stdout.read()
def write_to_paste_buffer(txt):
+ """Paste text to the clipboard for Mac OS X.
+
+ :param txt: str - text to paste to the clipboard
+ """
pbcopyproc = subprocess.Popen('pbcopy', shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
stderr=subprocess.PIPE)
pbcopyproc.communicate(txt.encode())
else:
def get_paste_buffer(*args):
- raise OSError(
- pastebufferr % ('pbcopy', 'On MacOS X - error should not occur - part of the default installation'))
+ """For Mac OS X without the appropriate tool installed to get text from clipboard, raise an exception."""
+ raise OSError(pastebufferr % ('pbcopy',
+ 'On MacOS X - error should not occur - part of the default installation'))
write_to_paste_buffer = get_paste_buffer
else:
@@ -380,11 +407,19 @@ else:
pass # something went wrong with xclip and we cannot use it
if can_clip:
def get_paste_buffer():
+ """Get the contents of the clipboard for Linux OSes.
+
+ :return: str - contents of the clipboard
+ """
xclipproc = subprocess.Popen('xclip -o -sel clip', shell=True, stdout=subprocess.PIPE,
stdin=subprocess.PIPE)
return xclipproc.stdout.read()
def write_to_paste_buffer(txt):
+ """Paste text to the clipboard for Linux OSes.
+
+ :param txt: str - text to paste to the clipboard
+ """
xclipproc = subprocess.Popen('xclip -sel clip', shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
xclipproc.stdin.write(txt.encode())
xclipproc.stdin.close()
@@ -394,12 +429,11 @@ else:
xclipproc.stdin.close()
else:
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"'))
write_to_paste_buffer = get_paste_buffer
-pyparsing.ParserElement.setDefaultWhitespaceChars(' \t')
-
class ParsedString(str):
"""Subclass of str which also stores a pyparsing.ParseResults object containing structured parse results."""
@@ -410,12 +444,17 @@ class ParsedString(str):
parser = None
def full_parsed_statement(self):
+ """Used to reconstruct the full parsed statement when a command isn't recognized."""
new = ParsedString('%s %s' % (self.parsed.command, self.parsed.args))
new.parsed = self.parsed
new.parser = self.parser
return new
def with_args_replaced(self, newargs):
+ """Used for @options commands when USE_ARG_LIST is False.
+
+ It helps figure out what the args are after removing options.
+ """
new = ParsedString(newargs)
new.parsed = self.parsed
new.parser = self.parser
@@ -430,6 +469,10 @@ class StubbornDict(dict):
Create it with the stubbornDict(arg) factory function.
"""
def update(self, arg):
+ """Adds dictionary arg's key-values pairs in to dict
+
+ :param arg: an object convertable to a StubbornDict
+ """
dict.update(self, StubbornDict.to_dict(arg))
append = update
@@ -485,6 +528,11 @@ def stubbornDict(*arg, **kwarg):
def replace_with_file_contents(fname):
+ """Action to perform when successfully matching parse element definition for inputFrom parser.
+
+ :param fname: str - filename
+ :return: str - contents of file "fname" or contents of the clipboard if fname is None or an empty string
+ """
if fname:
try:
result = open(os.path.expanduser(fname[0])).read()
@@ -496,10 +544,12 @@ def replace_with_file_contents(fname):
class EmbeddedConsoleExit(SystemExit):
+ """Custom exception class for use with the py command."""
pass
class EmptyStatement(Exception):
+ """Custom exception class for handling behavior when the user just presses <Enter>."""
pass
@@ -775,11 +825,23 @@ class Cmd(cmd.Cmd):
pyparsing.Optional(fileName) + (pyparsing.stringEnd | '|')
self.inputParser.ignore(self.commentInProgress)
+ # noinspection PyMethodMayBeStatic
def preparse(self, raw):
+ """Hook that runs before parsing the command-line and as the very first hook for a command.
+
+ :param raw: str - raw command line input
+ :return: str - potentially modified raw command line input
+ """
return raw
- def postparse(self, parseResult):
- return parseResult
+ # noinspection PyMethodMayBeStatic
+ def postparse(self, parse_result):
+ """Hook that runs immediately after parsing the command-line but before parsed() returns a ParsedString.
+
+ :param parse_result: pyparsing.ParseResults - parsing results output by the pyparsing parser
+ :return: pyparsing.ParseResults - potentially modified ParseResults object
+ """
+ return parse_result
def parsed(self, raw):
""" This function is where the actual parsing of each line occurs.
@@ -811,6 +873,7 @@ class Cmd(cmd.Cmd):
p.parser = self.parsed
return p
+ # noinspection PyMethodMayBeStatic
def postparsing_precmd(self, statement):
"""This runs after parsing the command-line, but before anything else; even before adding cmd to history.
@@ -828,6 +891,7 @@ class Cmd(cmd.Cmd):
stop = False
return stop, statement
+ # noinspection PyMethodMayBeStatic
def postparsing_postcmd(self, stop):
"""This runs after everything else, including after postcmd().
@@ -840,6 +904,14 @@ class Cmd(cmd.Cmd):
return stop
def func_named(self, arg):
+ """Gets the method name associated with a given command.
+
+ If self.abbrev is False, it is always just looks for do_arg. However, if self.abbrev is True,
+ it allows abbreivated command names and looks for any commands which start with do_arg.
+
+ :param arg: str - command to look up method name which implements it
+ :return: str - method name which implements the given command
+ """
result = None
target = 'do_' + arg
if target in dir(self):
@@ -852,10 +924,10 @@ class Cmd(cmd.Cmd):
return result
def onecmd_plus_hooks(self, line):
- """
+ """Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks.
- :param line:
- :return:
+ :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.
@@ -905,10 +977,20 @@ class Cmd(cmd.Cmd):
return statement
def redirect_output(self, statement):
+ """Handles output redirection for >, >>, and |.
+
+ :param statement: ParsedString - subclass of str which also contains pyparsing ParseResults instance
+ """
if statement.parsed.pipeTo:
self.kept_state = Statekeeper(self, ('stdout',))
self.kept_sys = Statekeeper(sys, ('stdout',))
sys.stdout = self.stdout
+
+ # NOTE: We couldn't get a real pipe working via subprocess for Python 3.x prior to 3.5.
+ # So to allow compatibility with Python 2.7 and 3.3+ we are redirecting output to a temporary file.
+ # And once command is complete we are using the "cat" shell command to pipe to whatever.
+ # TODO: Once support for Python 3.x prior to 3.5 is no longer necessary, replace with a real subprocess pipe
+
# Redirect stdout to a temporary file
_, self._temp_filename = tempfile.mkstemp()
self.stdout = open(self._temp_filename, 'w')
@@ -928,6 +1010,10 @@ class Cmd(cmd.Cmd):
self.stdout.write(get_paste_buffer())
def restore_output(self, statement):
+ """Handles restoring state after output redirection as well as the actual pipe operation if present.
+
+ :param statement: ParsedString - subclass of str which also contains pyparsing ParseResults instance
+ """
if self.kept_state:
try:
if statement.parsed.output:
@@ -952,16 +1038,12 @@ class Cmd(cmd.Cmd):
self._temp_filename = None
def onecmd(self, line):
- """Interpret the argument as though it had been typed in response
- to the prompt.
-
- This may be overridden, but should not normally need to be;
- see the precmd() and postcmd() methods for useful execution hooks.
- The return value is a flag indicating whether interpretation of
- commands by the interpreter should stop.
-
- This (`cmd2`) version of `onecmd` already override's `cmd`'s `onecmd`.
-
+ """ This executes the actual do_* method for a command.
+
+ If the command provided doesn't exist, then it executes _default() instead.
+
+ :param line: ParsedString - subclass of string inclding the pyparsing ParseResults
+ :return: bool - a flag indicating whether the interpretatoin of commands should stop
"""
statement = self.parsed(line)
self.lastcmd = statement.parsed.raw
@@ -976,12 +1058,22 @@ class Cmd(cmd.Cmd):
return stop
def _default(self, statement):
+ """Executed when the command given isn't a recognized command implemented by a do_* method.
+
+ :param statement: ParsedString - subclass of string inclding the pyparsing ParseResults
+ :return:
+ """
arg = statement.full_parsed_statement()
if self.default_to_shell:
result = os.system(arg)
+ # If os.system() succeeded, then don't print warning about unknown command
if not result:
- return self.postparsing_postcmd(None)
- return self.postparsing_postcmd(self.default(arg))
+ return False
+
+ # Print out a message stating this is an unknown command
+ self.default(arg)
+
+ return False
def pseudo_raw_input(self, prompt):
"""copied from cmd's cmdloop; like raw_input, but accounts for changed stdin, stdout"""
@@ -1142,6 +1234,7 @@ class Cmd(cmd.Cmd):
except (ValueError, AttributeError, NotSettableError):
self.do_show(arg)
+ # noinspection PyMethodMayBeStatic
def do_pause(self, text):
"""Displays the specified text then waits for the user to press <Enter>.
@@ -1158,6 +1251,7 @@ class Cmd(cmd.Cmd):
Usage: pause [text]"""
self.stdout.write("{}\n".format(help_str))
+ # noinspection PyMethodMayBeStatic
def do_shell(self, command):
"""Execute a command as if at the OS prompt.
@@ -1192,6 +1286,10 @@ class Cmd(cmd.Cmd):
# Support the run command even if called prior to invoking an interactive interpreter
def run(arg):
+ """Run a Python script file in the interactive console.
+
+ :param arg: str - filename of *.py script file to run
+ """
try:
with open(arg) as f:
interp.runcode(f.read())
@@ -1199,6 +1297,11 @@ class Cmd(cmd.Cmd):
self.perror(e)
def onecmd_plus_hooks(arg):
+ """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
+ :return: bool - True if cmdloop() should exit once leaving the interactive Python console, False otherwise.
+ """
return self.onecmd_plus_hooks(arg + '\n')
self.pystate['run'] = run
@@ -1212,6 +1315,7 @@ class Cmd(cmd.Cmd):
interp.runcode(arg)
else:
def quit():
+ """Function callable from the interactive Python console to exit that environment and return to cmd2."""
raise EmbeddedConsoleExit
self.pystate['quit'] = quit
@@ -1234,6 +1338,7 @@ class Cmd(cmd.Cmd):
# Only include the do_ipy() method if IPython is available on the system
if ipython_available:
+ # noinspection PyMethodMayBeStatic
def do_ipy(self, arg):
"""Enters an interactive IPython shell.
@@ -1272,6 +1377,13 @@ class Cmd(cmd.Cmd):
self.stdout.write(hi.pr())
def last_matching(self, arg):
+ """Return the last item from the history list that matches arg. Or if arg not provided, retern last item.
+
+ If not match is found, return None.
+
+ :param arg: str - text to search for in history
+ :return: str - last match, last item, or None, depending on arg.
+ """
try:
if arg:
return self.history.get(arg)[-1]
@@ -1423,6 +1535,18 @@ Edited files are run on close if the `autorun_on_edit` settable parameter is Tru
self.stdout.write("{}\n".format(help_str))
def read_file_or_url(self, fname):
+ """Open a file or URL for reading by the do_load() method.
+
+ This method methodically proceeds in the following path until it succeeds (or fails in the end):
+ 1) Try to open the file
+ 2) Try to open the URL if it looks like one
+ 3) Try to expand the ~ to create an absolute path for the filename
+ 4) Try to add the default extension to the expanded path
+ 5) Raise an error
+
+ :param fname: str - filename or URL
+ :return: stream or a file-like object pointing to the file or URL (or raise an exception if it couldn't open)
+ """
# TODO: not working on localhost
if os.path.isfile(fname):
result = open(fname, 'r')
@@ -1597,15 +1721,17 @@ class HistoryItem(str):
class History(list):
""" A list of HistoryItems that knows how to respond to user requests. """
- def zero_based_index(self, onebased):
+
+ # noinspection PyMethodMayBeStatic
+ def _zero_based_index(self, onebased):
result = onebased
if result > 0:
result -= 1
return result
- def to_index(self, raw):
+ def _to_index(self, raw):
if raw:
- result = self.zero_based_index(int(raw))
+ result = self._zero_based_index(int(raw))
else:
result = None
return result
@@ -1637,9 +1763,9 @@ class History(list):
if not results:
raise IndexError
if not results.group('separator'):
- return [self[self.to_index(results.group('start'))]]
- start = self.to_index(results.group('start'))
- end = self.to_index(results.group('end'))
+ return [self[self._to_index(results.group('start'))]]
+ start = self._to_index(results.group('start'))
+ end = self._to_index(results.group('end'))
reverse = False
if end is not None:
if end < start: