degal/command.py
author Tero Marttila <terom@fixme.fi>
Thu, 02 Jul 2009 21:59:01 +0300
changeset 144 97505a789003
parent 120 55cb7fc9c8fb
permissions -rw-r--r--
reorganize/rename the commands and their options stuff, options like --force-html are now split out from main.py into commands/update.py
"""
    Command-line based command implementation, handling option/argument parsing.
"""

import concurrent

import inspect, itertools, logging, traceback, optparse

def optify (symbol) :
    """
        Turns the given python symbol into the suitable version for use as a command-line argument.

        In other words, this replaces '_' with '-'.
    """

    return symbol.replace('_', '-')

# wrapper around optparse.make_option
Option = optparse.make_option

class Command (object) :
    """
        A Command is simply a function that can be executed from the command line with some options/arguments
    """

    def __init__ (self, name, func, doc=None, options=None) :
        """
            Create a new Command

             name       - the name of the command
             func       - the callable python function
             doc        - descriptive help text
             options    - named options as Option objects
        """
        
        self.name = name
        self.func = func
        self.doc = doc
        self.options = options
    
    def parse_args (self, args) :
        """
            Pre-parse the given arguments list.
        """

        return args

    def apply (self, config, gallery, options, *args, **kwargs) :
        """
            Construct a CommandContext for execution of this command with the given environment.
            the command with the given context.
        """
        
        # apply extra options
        for k, v in kwargs.iteritems() :
            setattr(options, k, v)

        return Environment(self, config, gallery, options, args)
    
    def option_group (self, parser) :
        """
            Returns an optparse.OptionGroup for this command's options.
        """

        group = optparse.OptionGroup(parser, "Command-specific options")

        for opt in self.options :
            group.add_option(opt)
    
        return group

    def cmdopt_callback (self, option, opt_str, value, parser) :
        """
            Used as a optparse.Option callback-action, adds this command's options to the parser, and stores the
            selected command.
        """

        # check
        if hasattr(parser.values, option.dest) :
            raise ArgumentError("More than one command option given: %s + %s" % (getattr(parser.values, option.dest), self.name))

        if self.options :
            # setup command-specific options
            parser.add_option_group(self.build_options(parser))
        
        # store selected command
        setattr(parser.values, option.dest, self)

class CommandList (object) :
    """
        A set of available Commands
    """

    def __init__ (self, commands) :
        """
            Store with given initial commands
        """

        self.list = commands
        self.dict = dict((cmd.name, cmd) for cmd in commands)

    def lookup (self, name) :
        """
            Lookup a command by name
        """

        return self.dict[name]

    def option_group (self, parser, default) :
        """
            Returns an optparse.OptionGroup for these commands, using the given parser.
        """
        
        group = optparse.OptionGroup(parser, "Command Options", "Select what command to execute, may introduce other options")

        for command in self.list :
            group.add_option('--%s' % optify(command.name), action='callback', callback=command.cmdopt_callback, dest='command', help=command.doc)
        
        # store default
        parser.set_defaults(command=default)

        return group

class Environment (object) :
    """
        The environment that a Command will execute in.
        
        This is bound to a Configuration, a Gallery, options values, argument values and other miscellaneous things. An
        environment also provides other services, such as status output, concurrent execution and error handling.
    """

    def __init__ (self, command, config, gallery, options, args) :
        """
            Create the execution environment
        """

        self.command = command
        self.config = config
        self.gallery = gallery
        self.options = options
        self.args = args

        # conccurency
        self.concurrent = concurrent.Manager(thread_count=config.thread_count)

    def execute (self) :
        """
            Run the command in this context
        """

        return self.command.func(self, *self.args)
    
    def run (self) :
        """
            Run the command with error handling
        """

        try :
            # run it
            return self.execute()
        
        except KeyboardInterrupt :
            self.log_error("Interrupted")

        except :
            # dump traceback
            # XXX: skip all crap up to the actual function
            self.handle_error()
    
    # XXX: split off to a .log object
    def log_msg (self, level, msg, *args, **kwargs) :
        """
            Output a log message with the given level

            XXX: unicode
        """
        
        # control level of output
        if level < self.config.log_level :
            return
        
        # format?
        if args or kwargs :
            if args and not kwargs :
                msg = msg % args

            elif kwargs and not args :
                msg = msg % kwargs

            else :
                raise Exception("log_msg called with both args and kwargs")
        
        # output
        # XXX: stdout/err?
        print msg

    def log_debug (self, msg, *args, **kwargs) :
        self.log_msg(logging.DEBUG, msg, *args, **kwargs)

    def log_info (self, msg, *args, **kwargs) :
        self.log_msg(logging.INFO, msg, *args, **kwargs)
    
    def log_warning (self, msg, *args, **kwargs) :
        self.log_msg(logging.WARNING, msg, *args, **kwargs)

    def log_error (self, msg, *args, **kwargs) :
        self.log_msg(logging.ERROR, msg, *args, **kwargs)
    
    def handle_error (self, exc_info=None) :
        """
            Do something to handle an error that occured
        """
        
        if exc_info :
            traceback.print_execption(*exc_info)

        else :
            traceback.print_exc()

def command (options=None) :
    def _decorator (func) :
        """
            A function decorator used to define Commands automatically
        """
        
        # find help string
        doc = inspect.getdoc(func)
        
        return Command(func.__name__, func, doc, options)
    
    return _decorator