terom@65: """ terom@144: Command-line based command implementation, handling option/argument parsing. terom@65: """ terom@65: terom@144: import concurrent terom@144: terom@144: import inspect, itertools, logging, traceback, optparse terom@144: terom@144: def optify (symbol) : terom@144: """ terom@144: Turns the given python symbol into the suitable version for use as a command-line argument. terom@144: terom@144: In other words, this replaces '_' with '-'. terom@144: """ terom@144: terom@144: return symbol.replace('_', '-') terom@144: terom@144: # wrapper around optparse.make_option terom@144: Option = optparse.make_option terom@144: terom@144: class Command (object) : terom@144: """ terom@144: A Command is simply a function that can be executed from the command line with some options/arguments terom@144: """ terom@144: terom@144: def __init__ (self, name, func, doc=None, options=None) : terom@144: """ terom@144: Create a new Command terom@144: terom@144: name - the name of the command terom@144: func - the callable python function terom@144: doc - descriptive help text terom@144: options - named options as Option objects terom@144: """ terom@144: terom@144: self.name = name terom@144: self.func = func terom@144: self.doc = doc terom@144: self.options = options terom@144: terom@144: def parse_args (self, args) : terom@144: """ terom@144: Pre-parse the given arguments list. terom@144: """ terom@144: terom@144: return args terom@144: terom@144: def apply (self, config, gallery, options, *args, **kwargs) : terom@144: """ terom@144: Construct a CommandContext for execution of this command with the given environment. terom@144: the command with the given context. terom@144: """ terom@144: terom@144: # apply extra options terom@144: for k, v in kwargs.iteritems() : terom@144: setattr(options, k, v) terom@144: terom@144: return Environment(self, config, gallery, options, args) terom@144: terom@144: def option_group (self, parser) : terom@144: """ terom@144: Returns an optparse.OptionGroup for this command's options. terom@144: """ terom@144: terom@144: group = optparse.OptionGroup(parser, "Command-specific options") terom@144: terom@144: for opt in self.options : terom@144: group.add_option(opt) terom@144: terom@144: return group terom@144: terom@144: def cmdopt_callback (self, option, opt_str, value, parser) : terom@144: """ terom@144: Used as a optparse.Option callback-action, adds this command's options to the parser, and stores the terom@144: selected command. terom@144: """ terom@144: terom@144: # check terom@144: if hasattr(parser.values, option.dest) : terom@144: raise ArgumentError("More than one command option given: %s + %s" % (getattr(parser.values, option.dest), self.name)) terom@144: terom@144: if self.options : terom@144: # setup command-specific options terom@144: parser.add_option_group(self.build_options(parser)) terom@144: terom@144: # store selected command terom@144: setattr(parser.values, option.dest, self) terom@65: terom@65: class CommandList (object) : terom@65: """ terom@144: A set of available Commands terom@65: """ terom@65: terom@65: def __init__ (self, commands) : terom@65: """ terom@65: Store with given initial commands terom@65: """ terom@65: terom@65: self.list = commands terom@65: self.dict = dict((cmd.name, cmd) for cmd in commands) terom@65: terom@65: def lookup (self, name) : terom@65: """ terom@65: Lookup a command by name terom@65: """ terom@65: terom@65: return self.dict[name] terom@65: terom@144: def option_group (self, parser, default) : terom@144: """ terom@144: Returns an optparse.OptionGroup for these commands, using the given parser. terom@144: """ terom@144: terom@144: group = optparse.OptionGroup(parser, "Command Options", "Select what command to execute, may introduce other options") terom@144: terom@144: for command in self.list : terom@144: group.add_option('--%s' % optify(command.name), action='callback', callback=command.cmdopt_callback, dest='command', help=command.doc) terom@144: terom@144: # store default terom@144: parser.set_defaults(command=default) terom@144: terom@144: return group terom@144: terom@144: class Environment (object) : terom@65: """ terom@144: The environment that a Command will execute in. terom@144: terom@144: This is bound to a Configuration, a Gallery, options values, argument values and other miscellaneous things. An terom@144: environment also provides other services, such as status output, concurrent execution and error handling. terom@65: """ terom@65: terom@144: def __init__ (self, command, config, gallery, options, args) : terom@65: """ terom@65: Create the execution environment terom@65: """ terom@65: terom@65: self.command = command terom@65: self.config = config terom@65: self.gallery = gallery terom@144: self.options = options terom@144: self.args = args terom@65: terom@117: # conccurency terom@117: self.concurrent = concurrent.Manager(thread_count=config.thread_count) terom@117: terom@144: def execute (self) : terom@65: """ terom@65: Run the command in this context terom@65: """ terom@65: terom@144: return self.command.func(self, *self.args) terom@92: terom@144: def run (self) : terom@92: """ terom@92: Run the command with error handling terom@92: """ terom@92: terom@92: try : terom@92: # run it terom@144: return self.execute() terom@92: terom@92: except KeyboardInterrupt : terom@92: self.log_error("Interrupted") terom@92: terom@92: except : terom@114: # dump traceback terom@120: # XXX: skip all crap up to the actual function terom@114: self.handle_error() terom@144: terom@144: # XXX: split off to a .log object terom@65: def log_msg (self, level, msg, *args, **kwargs) : terom@65: """ terom@65: Output a log message with the given level terom@65: terom@65: XXX: unicode terom@65: """ terom@65: terom@65: # control level of output terom@65: if level < self.config.log_level : terom@65: return terom@65: terom@65: # format? terom@65: if args or kwargs : terom@65: if args and not kwargs : terom@65: msg = msg % args terom@65: terom@65: elif kwargs and not args : terom@65: msg = msg % kwargs terom@65: terom@65: else : terom@65: raise Exception("log_msg called with both args and kwargs") terom@65: terom@65: # output terom@65: # XXX: stdout/err? terom@65: print msg terom@65: terom@65: def log_debug (self, msg, *args, **kwargs) : terom@76: self.log_msg(logging.DEBUG, msg, *args, **kwargs) terom@65: terom@65: def log_info (self, msg, *args, **kwargs) : terom@76: self.log_msg(logging.INFO, msg, *args, **kwargs) terom@65: terom@65: def log_warning (self, msg, *args, **kwargs) : terom@76: self.log_msg(logging.WARNING, msg, *args, **kwargs) terom@65: terom@65: def log_error (self, msg, *args, **kwargs) : terom@76: self.log_msg(logging.ERROR, msg, *args, **kwargs) terom@76: terom@76: def handle_error (self, exc_info=None) : terom@76: """ terom@76: Do something to handle an error that occured terom@76: """ terom@76: terom@76: if exc_info : terom@76: traceback.print_execption(*exc_info) terom@76: terom@76: else : terom@76: traceback.print_exc() terom@65: terom@144: def command (options=None) : terom@144: def _decorator (func) : terom@144: """ terom@144: A function decorator used to define Commands automatically terom@144: """ terom@144: terom@144: # find help string terom@144: doc = inspect.getdoc(func) terom@144: terom@144: return Command(func.__name__, func, doc, options) terom@144: terom@144: return _decorator terom@65: