first pass at interactive app

This commit is contained in:
Doug Hellmann 2012-04-28 18:26:31 -04:00
parent f63bb59626
commit b17d091258
5 changed files with 129 additions and 13 deletions

View File

@ -25,5 +25,7 @@ To do
to manage transactions
- switch setup/teardown functions in app to use some sort of context
manager?
- interactive shell mode
- add options to csv formatter to control output (delimiter, etc.)
- option to spit out bash completion data
- move command execution into a separate class to be used by App and
InteractiveApp?

View File

@ -8,6 +8,7 @@ import os
import sys
from .help import HelpAction, HelpCommand
from .interactive import InteractiveApp
LOG = logging.getLogger(__name__)
@ -39,6 +40,7 @@ class App(object):
self.stdout = stdout or sys.stdout
self.stderr = stderr or sys.stderr
self.parser = self.build_option_parser(description, version)
self.interactive_mode = False
def build_option_parser(self, description, version):
"""Return an argparse option parser for this application.
@ -77,7 +79,7 @@ class App(object):
help="show this help message and exit",
)
parser.add_argument(
'--debug',
'--debug',
default=False,
action='store_true',
help='show tracebacks on errors',
@ -112,6 +114,28 @@ class App(object):
root_logger.addHandler(console)
return
def run(self, argv):
"""Equivalent to the main program for the application.
"""
self.options, remainder = self.parser.parse_known_args(argv)
self.configure_logging()
self.initialize_app()
result = 1
if not remainder:
result = self.interact()
else:
result = self.run_subcommand(remainder)
return result
# FIXME(dhellmann): Consider moving these command handling methods
# to a separate class.
def initialize_app(self):
"""Hook for subclasses to take global initialization action
after the arguments are parsed but before a command is run.
Invoked only once, even in interactive mode.
"""
return
def prepare_to_run_command(self, cmd):
"""Perform any preliminary work needed to run a command.
"""
@ -122,20 +146,22 @@ class App(object):
"""
return
def run(self, argv):
"""Equivalent to the main program for the application.
"""
if not argv:
argv = ['-h']
self.options, remainder = self.parser.parse_known_args(argv)
self.configure_logging()
cmd_factory, cmd_name, sub_argv = self.command_manager.find_command(remainder)
def interact(self):
self.interactive_mode = True
interpreter = InteractiveApp(self, self.command_manager, self.stdin, self.stdout)
interpreter.prompt = '(%s) ' % self.NAME
interpreter.cmdloop()
return 0
def run_subcommand(self, argv):
cmd_factory, cmd_name, sub_argv = self.command_manager.find_command(argv)
cmd = cmd_factory(self, self.options)
err = None
result = 1
try:
self.prepare_to_run_command(cmd)
cmd_parser = cmd.get_parser(' '.join([self.NAME, cmd_name]))
full_name = cmd_name if self.interactive_mode else ' '.join([self.NAME, cmd_name])
cmd_parser = cmd.get_parser(full_name)
parsed_args = cmd_parser.parse_args(sub_argv)
result = cmd.run(parsed_args)
except Exception as err:

View File

@ -38,9 +38,13 @@ class HelpCommand(Command):
def run(self, parsed_args):
if parsed_args.cmd:
cmd_factory, name, search_args = self.app.command_manager.find_command(parsed_args.cmd)
cmd_factory, cmd_name, search_args = self.app.command_manager.find_command(parsed_args.cmd)
cmd = cmd_factory(self.app, search_args)
cmd_parser = cmd.get_parser(' '.join([self.app.NAME, name]))
full_name = (cmd_name
if self.app.interactive_mode
else ' '.join([self.app.NAME, cmd_name])
)
cmd_parser = cmd.get_parser(full_name)
else:
cmd_parser = self.get_parser(' '.join([self.app.NAME, 'help']))
cmd_parser.parse_args(['--help'])

81
cliff/interactive.py Normal file
View File

@ -0,0 +1,81 @@
"""Application base class.
"""
import itertools
import logging
import logging.handlers
import shlex
import cmd2
LOG = logging.getLogger(__name__)
class InteractiveApp(cmd2.Cmd):
use_rawinput = True
doc_header = "Shell commands (type help <topic>):"
app_cmd_header = "Application commands (type help <topic>):"
def __init__(self, parent_app, command_manager, stdin, stdout):
self.parent_app = parent_app
self.command_manager = command_manager
cmd2.Cmd.__init__(self, 'tab', stdin=stdin, stdout=stdout)
def default(self, line):
# Tie in the the default command processor to
# dispatch commands known to the command manager.
# We send the message through our parent app,
# since it already has the logic for executing
# the subcommand.
line_parts = shlex.split(line)
self.parent_app.run_subcommand(line_parts)
def completedefault(self, text, line, begidx, endidx):
"""Tab-completion for commands known to the command manager.
Does not handle options on the commands.
"""
if not text:
completions = sorted(n for n, v in self.command_manager)
else:
completions = sorted(n for n, v in self.command_manager
if n.startswith(text)
)
return completions
def help_help(self):
# Use the command manager to get instructions for "help"
self.default('help help')
def do_help(self, arg):
if arg:
# Check if the arg is a builtin command or something
# coming from the command manager
arg_parts = shlex.split(arg)
method_name = '_'.join(
itertools.chain(['do'],
itertools.takewhile(lambda x: not x.startswith('-'),
arg_parts)
)
)
# Have the command manager version of the help
# command produce the help text since cmd and
# cmd2 do not provide help for "help"
if hasattr(self, method_name):
return cmd2.Cmd.do_help(self, arg)
# Dispatch to the underlying help command,
# which knows how to provide help for extension
# commands.
self.default('help ' + arg)
else:
cmd2.Cmd.do_help(self, arg)
cmd_names = [n for n, v in self.command_manager]
self.print_topics(self.app_cmd_header, cmd_names, 15, 80)
return
def get_names(self):
return [n
for n in cmd2.Cmd.get_names(self)
if not n.startswith('do__')
]

View File

@ -16,6 +16,9 @@ class DemoApp(App):
command_manager=CommandManager('cliff.demo'),
)
def initialize_app(self):
self.log.debug('initialize_app')
def prepare_to_run_command(self, cmd):
self.log.debug('prepare_to_run_command %s', cmd.__class__.__name__)