first pass at interactive app
This commit is contained in:
parent
f63bb59626
commit
b17d091258
|
@ -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?
|
||||
|
|
46
cliff/app.py
46
cliff/app.py
|
@ -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:
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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__')
|
||||
]
|
|
@ -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__)
|
||||
|
||||
|
|
Loading…
Reference in New Issue