From 12d2079e810cb83cc36076319e7ab0fd289454de Mon Sep 17 00:00:00 2001 From: Martin Baeuml Date: Fri, 10 Jun 2011 16:00:23 +0200 Subject: [PATCH] add command line interface --- sloth/bin/sloth-cli | 11 ++ sloth/core/cli.py | 424 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 435 insertions(+) create mode 100755 sloth/bin/sloth-cli create mode 100644 sloth/core/cli.py diff --git a/sloth/bin/sloth-cli b/sloth/bin/sloth-cli new file mode 100755 index 0000000..8e64a04 --- /dev/null +++ b/sloth/bin/sloth-cli @@ -0,0 +1,11 @@ +#!/usr/bin/env python +import sys +from sloth.core.cli import CommandLineUtility + +def execute_from_command_line(argv=None): + utility = CommandLineUtility(argv) + utility.execute() + +if __name__ == "__main__": + execute_from_command_line(sys.argv) + diff --git a/sloth/core/cli.py b/sloth/core/cli.py new file mode 100644 index 0000000..fb6f8ec --- /dev/null +++ b/sloth/core/cli.py @@ -0,0 +1,424 @@ +""" +Labeltool command line interface + +This is based on django's ManagementUtility. +""" + +import os +import sys +from optparse import make_option, OptionParser +import sloth + +from sloth.annotations.container import * +import shutil + +# command dictionary str -> Command +_commands = {} + +def get_commands(): + """ + Returns a dictionary mapping command names to their callback applications. + + This works by looking for a management.commands package in django.core, and + in each installed application -- if a commands package exists, all commands + in that package are registered. + + Core commands are always included. If a settings module has been + specified, user-defined commands will also be included, the + startproject command will be disabled, and the startapp command + will be modified to use the directory in which the settings module appears. + + The dictionary is in the format {command_name: app_name}. Key-value + pairs from this dictionary can then be used in calls to + load_command_class(app_name, command_name) + + If a specific version of a command must be loaded (e.g., with the + startapp command), the instantiated module can be placed in the + dictionary in place of the application name. + + The dictionary is cached on the first call and reused on subsequent + calls. + """ + global _commands + return _commands + + +class CommandError(Exception): + """ + Exception class indicating a problem while executing a management + command. + + If this exception is raised during the execution of a management + command, it will be caught and turned into a nicely-printed error + message to the appropriate output stream (i.e., stderr); as a + result, raising this exception (with a sensible description of the + error) is the preferred way to indicate that something has gone + wrong in the execution of a command. + """ + pass + + +class BaseCommand(object): + """ + The base class from which all management commands ultimately + derive. + + Use this class if you want access to all of the mechanisms which + parse the command-line arguments and work out what code to call in + response; if you don't need to change any of that behavior, + consider using one of the subclasses defined in this file. + + If you are interested in overriding/customizing various aspects of + the command-parsing and -execution behavior, the normal flow works + as follows: + + 1. ``sloth-cli` loads the command class + and calls its ``run_from_argv()`` method. + + 2. The ``run_from_argv()`` method calls ``create_parser()`` to get + an ``OptionParser`` for the arguments, parses them, performs + any environment changes requested by options like + ``pythonpath``, and then calls the ``execute()`` method, + passing the parsed arguments. + + 3. The ``execute()`` method attempts to carry out the command by + calling the ``handle()`` method with the parsed arguments; any + output produced by ``handle()`` will be printed to standard + output. + + 4. If ``handle()`` raised a ``CommandError``, ``execute()`` will + instead print an error message to ``stderr``. + + Thus, the ``handle()`` method is typically the starting point for + subclasses; built-in commands and command types either place + all of their logic in ``handle()``, or perform some additional + parsing work in ``handle()`` and then delegate from it to more + specialized methods as needed. + + Several attributes affect behavior at various steps along the way: + + ``args`` + A string listing the arguments accepted by the command, + suitable for use in help messages; e.g., a command which takes + a list of application names might set this to ''. + + ``help`` + A short description of the command, which will be printed in + help messages. + + ``option_list`` + This is the list of ``optparse`` options which will be fed + into the command's ``OptionParser`` for parsing arguments. + + """ + # Metadata about this command. + option_list = ( + make_option('-v', '--verbosity', action='store', dest='verbosity', default='1', + type='choice', choices=['0', '1', '2', '3'], + help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'), + make_option('--config', + help='The Python path to a configuration file, e.g. "myconfig". If this isn\'t provided, sloth\'s default configuration will be used.'), + make_option('--pythonpath', + help='A directory to add to the Python path, e.g. "/home/user/labeling".'), + make_option('--traceback', action='store_true', + help='Print traceback on exception'), + ) + help = '' + args = '' + + # Configuration shortcuts that alter various logic. + can_import_settings = True + + def __init__(self): + #self.style = color_style() + pass + + def usage(self, subcommand): + """ + Return a brief description of how to use this command, by + default from the attribute ``self.help``. + """ + usage = '%%prog %s [options] %s' % (subcommand, self.args) + if self.help: + return '%s\n\n%s' % (usage, self.help) + else: + return usage + + def get_version(self): + return sloth.VERSION + + def create_parser(self, prog_name, subcommand): + """ + Create and return the ``OptionParser`` which will be used to + parse the arguments to this command. + """ + return OptionParser(prog=prog_name, + usage=self.usage(subcommand), + version=self.get_version(), + option_list=self.option_list) + + def print_help(self, prog_name, subcommand): + """ + Print the help message for this command, derived from + ``self.usage()``. + """ + parser = self.create_parser(prog_name, subcommand) + parser.print_help() + + def run_from_argv(self, argv): + """ + Set up any environment changes requested (e.g. python path + and sloth configuration), then run this command. + """ + parser = self.create_parser(argv[0], argv[1]) + options, args = parser.parse_args(argv[2:]) + #handle_default_options(options) + self.execute(*args, **options.__dict__) + + def execute(self, *args, **options): + """ + Try to execute this command, performing model validation if + needed (as controlled by the attribute + ``self.requires_model_validation``). If the command raises a + ``CommandError``, intercept it and print it sensibly to + stderr. + """ + try: + self.stdout = options.get('stdout', sys.stdout) + self.stderr = options.get('stderr', sys.stderr) + output = self.handle(*args, **options) + if output: + self.stdout.write(output) + except CommandError, e: + self.stderr.write('Error: %s\n' % e) + sys.exit(1) + + def handle(self, *args, **options): + """ + The actual logic of the command. Subclasses must implement + this method. + """ + raise NotImplementedError() + + +class NoArgsCommand(BaseCommand): + """ + A command which takes no arguments on the command line. + + Rather than implementing ``handle()``, subclasses must implement + ``handle_noargs()``; ``handle()`` itself is overridden to ensure + no arguments are passed to the command. + + Attempting to pass arguments will raise ``CommandError``. + """ + args = '' + + def handle(self, *args, **options): + if args: + raise CommandError("Command doesn't accept any arguments") + return self.handle_noargs(**options) + + def handle_noargs(self, **options): + """ + Perform this command's actions. + + """ + raise NotImplementedError() + + +class ConvertCommand(BaseCommand): + """ + Converts a label file from one file format to another. + """ + args = ' ' + help = __doc__.strip() + + def handle(self, *args, **options): + if len(args) != 2: + raise CommandError("Expect exactly 2 arguments.") + + print "Converting labels from %s to %s" % args + +class CreateConfigCommand(BaseCommand): + """ + Creates a configuration file with default values. + """ + args = '' + help = __doc__.strip() + option_list = BaseCommand.option_list + ( + make_option('-f', '--force', action='store_true', default=False, + help='Overwrite the file if it exists.'), + ) + + def handle(self, *args, **options): + if len(args) != 1: + raise CommandError("Expect exactly 1 argument.") + + template_dir = os.path.join(sloth.__path__[0], 'conf', 'template') + config_template = os.path.join(template_dir, 'config_template.py') + target = args[0] + + if os.path.exists(target) and not options['force']: + sys.stderr.write("Error: %s exists. Use -f to overwrite.\n" % target) + return + + try: + shutil.copy(config_template, target) + _make_writeable(target) + except OSError, e: + sys.stderr.write("Notice: Couldn't set permission bits on %s.\n" % target) + + +_commands['convert'] = ConvertCommand() +_commands['createconfig'] = CreateConfigCommand() + + +class LaxOptionParser(OptionParser): + """ + An option parser that doesn't raise any errors on unknown options. + + This is needed because the --settings and --pythonpath options affect + the commands (and thus the options) that are available to the user. + """ + def error(self, msg): + pass + + def print_help(self): + """Output nothing. + + The lax options are included in the normal option parser, so under + normal usage, we don't need to print the lax options. + """ + pass + + def print_lax_help(self): + """Output the basic options available to every command. + + This just redirects to the default print_help() behaviour. + """ + OptionParser.print_help(self) + + def _process_args(self, largs, rargs, values): + """ + Overrides OptionParser._process_args to exclusively handle default + options and ignore args and other options. + + This overrides the behavior of the super class, which stop parsing + at the first unrecognized option. + """ + while rargs: + arg = rargs[0] + try: + if arg[0:2] == "--" and len(arg) > 2: + # process a single long option (possibly with value(s)) + # the superclass code pops the arg off rargs + self._process_long_opt(rargs, values) + elif arg[:1] == "-" and len(arg) > 1: + # process a cluster of short options (possibly with + # value(s) for the last one only) + # the superclass code pops the arg off rargs + self._process_short_opts(rargs, values) + else: + # it's either a non-default option or an arg + # either way, add it to the args list so we can keep + # dealing with options + del rargs[0] + raise Exception + except: + largs.append(arg) + + +class CommandLineUtility(object): + """ + A CommandLineUtility has a number of commands, which can be manipulated + by editing the self.commands dictionary. + """ + def __init__(self, argv=None): + self.argv = argv or sys.argv[:] + self.prog_name = os.path.basename(self.argv[0]) + + def main_help_text(self): + """ + Returns the script's main help text, as a string. + """ + usage = ['',"Type '%s help ' for help on a specific subcommand." % self.prog_name,''] + usage.append('Available subcommands:') + commands = get_commands().keys() + commands.sort() + for cmd in commands: + usage.append(' %s' % cmd) + return '\n'.join(usage) + + def fetch_command(self, subcommand): + """ + Tries to fetch the given subcommand, printing a message with the + appropriate command called from the command line if it can't be found. + """ + try: + app_name = get_commands()[subcommand] + except KeyError: + sys.stderr.write("Unknown command: %r\nType '%s help' for usage.\n" % \ + (subcommand, self.prog_name)) + sys.exit(1) + if isinstance(app_name, BaseCommand): + # If the command is already loaded, use it directly. + klass = app_name + else: + klass = load_command_class(app_name, subcommand) + return klass + + def execute(self): + """ + Given the command-line arguments, this figures out which subcommand is + being run, creates a parser appropriate to that command, and runs it. + """ + # Preprocess options to extract --settings and --pythonpath. + # These options could affect the commands that are available, so they + # must be processed early. + parser = LaxOptionParser(usage="%prog subcommand [options] [args]", + version=sloth.VERSION, + option_list=BaseCommand.option_list) + try: + options, args = parser.parse_args(self.argv) + #handle_default_options(options) + except: + pass # Ignore any option errors at this point. + + try: + subcommand = self.argv[1] + except IndexError: + subcommand = 'help' # Display help if no arguments were given. + + if subcommand == 'help': + if len(args) > 2: + self.fetch_command(args[2]).print_help(self.prog_name, args[2]) + else: + parser.print_lax_help() + sys.stdout.write(self.main_help_text() + '\n') + sys.exit(1) + # Special-cases: We want 'django-admin.py --version' and + # 'django-admin.py --help' to work, for backwards compatibility. + elif self.argv[1:] == ['--version']: + # LaxOptionParser already takes care of printing the version. + pass + elif self.argv[1:] in (['--help'], ['-h']): + parser.print_lax_help() + sys.stdout.write(self.main_help_text() + '\n') + else: + self.fetch_command(subcommand).run_from_argv(self.argv) + + +def _make_writeable(filename): + """ + Make sure that the file is writeable. Useful if our source is + read-only. + """ + import stat + if sys.platform.startswith('java'): + # On Jython there is no os.access() + return + if not os.access(filename, os.W_OK): + st = os.stat(filename) + new_permissions = stat.S_IMODE(st.st_mode) | stat.S_IWUSR + os.chmod(filename, new_permissions)