diff options
author | Ryan Petrello <lists@ryanpetrello.com> | 2012-03-14 12:27:54 -0700 |
---|---|---|
committer | Ryan Petrello <lists@ryanpetrello.com> | 2012-03-15 14:55:49 -0700 |
commit | 74100f273337b282c9b6940bcb204342c5985c90 (patch) | |
tree | 6bc2142aa4939cd0938758e33b8f9ab545a3c732 /pecan/commands | |
parent | 40913103ffc83a44958e770085b1eca94256ce6e (diff) | |
download | pecan-74100f273337b282c9b6940bcb204342c5985c90.tar.gz |
Implementing a simple command/plugin library to replace PasteScript, including:
$ pecan serve
$ pecan shell
Diffstat (limited to 'pecan/commands')
-rw-r--r-- | pecan/commands/__init__.py | 12 | ||||
-rw-r--r-- | pecan/commands/base.py | 127 | ||||
-rw-r--r-- | pecan/commands/create.py | 42 | ||||
-rw-r--r-- | pecan/commands/runner.py | 131 | ||||
-rw-r--r-- | pecan/commands/serve.py | 78 | ||||
-rw-r--r-- | pecan/commands/shell.py | 19 |
6 files changed, 141 insertions, 268 deletions
diff --git a/pecan/commands/__init__.py b/pecan/commands/__init__.py index 3148810..dade8a0 100644 --- a/pecan/commands/__init__.py +++ b/pecan/commands/__init__.py @@ -1,8 +1,4 @@ -""" -PasteScript commands for Pecan. -""" - -from runner import CommandRunner # noqa -from create import CreateCommand # noqa -from shell import ShellCommand # noqa -from serve import ServeCommand # noqa +from base import CommandRunner, BaseCommand +from serve import ServeCommand +from shell import ShellCommand +from create import CreateCommand diff --git a/pecan/commands/base.py b/pecan/commands/base.py index f30704c..57c4115 100644 --- a/pecan/commands/base.py +++ b/pecan/commands/base.py @@ -1,49 +1,108 @@ -""" -PasteScript base command for Pecan. -""" +import pkg_resources +import os.path +import argparse +import logging +import sys +from warnings import warn from pecan import load_app -from paste.script import command as paste_command -import os.path +log = logging.getLogger(__name__) + + +class CommandManager(object): + """ Used to discover `pecan.command` entry points. """ + def __init__(self): + self.commands_ = {} + self.load_commands() -class Command(paste_command.Command): - """ - Base class for Pecan commands. + def load_commands(self): + for ep in pkg_resources.iter_entry_points('pecan.command'): + log.debug('%s loading plugin %s', self.__class__.__name__, ep) + try: + cmd = ep.load() + assert hasattr(cmd, 'run') + except Exception, e: + warn("Unable to load plugin %s: %s" % (ep, e), RuntimeWarning) + continue + self.add({ep.name: cmd}) - This provides some standard functionality for interacting with Pecan - applications and handles some of the basic PasteScript command cruft. + def add(self, cmd): + self.commands_.update(cmd) - See ``paste.script.command.Command`` for more information. - """ + @property + def commands(self): + return self.commands_ - # command information - group_name = 'Pecan' - summary = '' - # command parser - parser = paste_command.Command.standard_parser() +class CommandRunner(object): + """ Dispatches `pecan` command execution requests. """ + + def __init__(self): + self.manager = CommandManager() + self.parser = argparse.ArgumentParser( + version='Pecan %s' % self.version, + add_help=True + ) + self.parse_commands() + + def parse_commands(self): + subparsers = self.parser.add_subparsers( + dest='command_name', + metavar='command' + ) + for name, cmd in self.commands.items(): + sub = subparsers.add_parser( + name, + help=cmd.summary + ) + for arg in getattr(cmd, 'arguments', tuple()): + arg = arg.copy() + sub.add_argument(arg.pop('command'), **arg) def run(self, args): + ns = self.parser.parse_args(args) + self.commands[ns.command_name]().run(ns) + + @classmethod + def handle_command_line(cls): + runner = CommandRunner() + exit_code = runner.run(sys.argv[1:]) + sys.exit(exit_code) + + @property + def version(self): try: - return paste_command.Command.run(self, args) - except paste_command.BadCommand, ex: - ex.args[0] = self.parser.error(ex.args[0]) - raise + dist = pkg_resources.get_distribution('Pecan') + if os.path.dirname(os.path.dirname(__file__)) == dist.location: + return dist.version + else: + return '(development)' + except: + return '(development)' - def load_app(self): - return load_app(self.validate_file(self.args)) + @property + def commands(self): + return self.manager.commands_ - def logging_file_config(self, config_file): - if os.path.splitext(config_file)[1].lower() == '.ini': - paste_command.Command.logging_file_config(self, config_file) - def validate_file(self, argv): - if not argv or not os.path.isfile(argv[0]): - raise paste_command.BadCommand( - 'This command needs a valid config file.' - ) - return argv[0] +class BaseCommand(object): + """ Base class for Pecan commands. """ + + class __metaclass__(type): + @property + def summary(cls): + return cls.__doc__.strip().splitlines()[0].rstrip('.') - def command(self): - pass + arguments = ({ + 'command': 'config_file', + 'help': 'a Pecan configuration file' + },) + + def run(self, args): + self.args = args + + def load_app(self): + if not os.path.isfile(self.args.config_file): + raise RuntimeError('`%s` is not a file.' % self.args.config_file) + return load_app(self.args.config_file) diff --git a/pecan/commands/create.py b/pecan/commands/create.py index 5ade3c2..ade6301 100644 --- a/pecan/commands/create.py +++ b/pecan/commands/create.py @@ -1,16 +1,14 @@ """ -PasteScript create command for Pecan. +Create command for Pecan """ -from paste.script.create_distro import CreateDistroCommand - -from base import Command +from pecan.commands import BaseCommand from pecan.templates import DEFAULT_TEMPLATE import copy import sys -class CreateCommand(CreateDistroCommand, Command): +class CreateCommand(BaseCommand): """ Creates the file layout for a new Pecan distribution. @@ -19,26 +17,14 @@ class CreateCommand(CreateDistroCommand, Command): egg plugin for user convenience. """ - # command information - summary = __doc__.strip().splitlines()[0].rstrip('.') - description = None - - def command(self): - if not self.options.list_templates: - if not self.options.templates: - self.options.templates = [DEFAULT_TEMPLATE] - try: - return CreateDistroCommand.command(self) - except LookupError, ex: - sys.stderr.write('%s\n\n' % ex) - CreateDistroCommand.list_templates(self) - return 2 - - def all_entry_points(self): - entry_points = [] - for entry in CreateDistroCommand.all_entry_points(self): - if entry.name.startswith('pecan-'): - entry = copy.copy(entry) - entry_points.append(entry) - entry.name = entry.name[6:] - return entry_points + arguments = ({ + 'command': 'template_name', + 'help': 'a registered Pecan template', + 'nargs': '?', + 'default': DEFAULT_TEMPLATE + },) + + def run(self, args): + super(CreateCommand, self).run(args) + print "NOT IMPLEMENTED" + print args.template_name diff --git a/pecan/commands/runner.py b/pecan/commands/runner.py deleted file mode 100644 index 77b18f2..0000000 --- a/pecan/commands/runner.py +++ /dev/null @@ -1,131 +0,0 @@ -""" -PasteScript command runner. -""" -from paste.script import command as paste_command - -import optparse -import os -import pkg_resources -import sys -import warnings - - -class CommandRunner(object): - """ - Dispatches command execution requests. - - This is a custom PasteScript command runner that is specific to Pecan - commands. For a command to show up, its name must begin with "pecan-". - It is also recommended that its group name be set to "Pecan" so that it - shows up under that group when using ``paster`` directly. - """ - - def __init__(self): - - # set up the parser - self.parser = optparse.OptionParser( - add_help_option=False, - version='Pecan %s' % self.get_version(), - usage='%prog [options] COMMAND [command_options]' - ) - self.parser.disable_interspersed_args() - self.parser.add_option('-h', '--help', - action='store_true', - dest='show_help', - help='show detailed help message') - - # suppress BaseException.message warnings for BadCommand - if sys.version_info < (2, 7): - warnings.filterwarnings( - 'ignore', - 'BaseException\.message has been deprecated as of Python 2\.6', - DeprecationWarning, - paste_command.__name__.replace('.', '\\.')) - - # register Pecan as a system plugin when using the custom runner - paste_command.system_plugins.append('Pecan') - - def get_command_template(self, command_names): - if not command_names: - max_length = 10 - else: - max_length = max([len(name) for name in command_names]) - return ' %%-%ds %%s\n' % max_length - - def get_commands(self): - commands = {} - for name, command in paste_command.get_commands().iteritems(): - if name.startswith('pecan-'): - commands[name[6:]] = command.load() - return commands - - def get_version(self): - try: - dist = pkg_resources.get_distribution('Pecan') - if os.path.dirname(os.path.dirname(__file__)) == dist.location: - return dist.version - else: - return '(development)' - except: - return '(development)' - - def print_usage(self, file=sys.stdout): - self.parser.print_help(file=file) - file.write('\n') - command_groups = {} - commands = self.get_commands() - if not commands: - file.write('No commands registered.\n') - return - command_template = self.get_command_template(commands.keys()) - for name, command in commands.iteritems(): - group_name = command.group_name - if group_name.lower() == 'pecan': - group_name = '' - command_groups.setdefault(group_name, {})[name] = command - command_groups = sorted(command_groups.items()) - for i, (group, commands) in enumerate(command_groups): - file.write('%s:\n' % (group or 'Commands')) - for name, command in sorted(commands.items()): - file.write(command_template % (name, command.summary)) - if i + 1 < len(command_groups): - file.write('\n') - - def print_known_commands(self, file=sys.stderr): - commands = self.get_commands() - command_names = sorted(commands.keys()) - if not command_names: - file.write('No commands registered.\n') - return - file.write('Known commands:\n') - command_template = self.get_command_template(command_names) - for name in command_names: - file.write(command_template % (name, commands[name].summary)) - - def run(self, args): - options, args = self.parser.parse_args(args) - if not args: - self.print_usage() - return 0 - command_name = args.pop(0) - commands = self.get_commands() - if command_name not in commands: - sys.stderr.write('Command %s not known\n\n' % command_name) - self.print_known_commands() - return 1 - else: - command = commands[command_name](command_name) - if options.show_help: - return command.run(['-h']) - else: - return command.run(args) - - @classmethod - def handle_command_line(cls): - try: - runner = CommandRunner() - exit_code = runner.run(sys.argv[1:]) - except paste_command.BadCommand, ex: - sys.stderr.write('%s\n' % ex) - exit_code = ex.exit_code - sys.exit(exit_code) diff --git a/pecan/commands/serve.py b/pecan/commands/serve.py index 95cbabf..3d38ab9 100644 --- a/pecan/commands/serve.py +++ b/pecan/commands/serve.py @@ -1,64 +1,36 @@ """ -PasteScript serve command for Pecan. +Serve command for Pecan. """ -from paste.script.serve import ServeCommand as _ServeCommand +from pecan.commands import BaseCommand -from base import Command -import re - -class ServeCommand(_ServeCommand, Command): +class ServeCommand(BaseCommand): """ Serves a Pecan web application. This command serves a Pecan web application using the provided configuration file for the server and application. - - If start/stop/restart is given, then --daemon is implied, and it will - start (normal operation), stop (--stop-daemon), or do both. """ - # command information - usage = 'CONFIG_FILE [start|stop|restart|status]' - summary = __doc__.strip().splitlines()[0].rstrip('.') - description = '\n'.join( - map(lambda s: s.rstrip(), __doc__.strip().splitlines()[2:]) - ) - - # command options/arguments - max_args = 2 - - # command parser - parser = _ServeCommand.parser - parser.remove_option('-n') - parser.remove_option('-s') - parser.remove_option('--server-name') - - # configure scheme regex - _scheme_re = re.compile(r'.*') - - def command(self): - - # set defaults for removed options - setattr(self.options, 'app_name', None) - setattr(self.options, 'server', None) - setattr(self.options, 'server_name', None) - - # run the base command - _ServeCommand.command(self) - - def loadserver(self, server_spec, name, relative_to, **kw): - return (lambda app: WSGIRefServer(app.config.server.host, app.config.server.port, app)) - - def loadapp(self, app_spec, name, relative_to, **kw): - return self.load_app() - - -def WSGIRefServer(host, port, app, **options): - """ - A very simple approach for a WSGI server. - """ - from wsgiref.simple_server import make_server - port = int(port) - srv = make_server(host, port, app, **options) - srv.serve_forever() + def run(self, args): + super(ServeCommand, self).run(args) + app = self.load_app() + self.serve(app, app.config) + + def serve(self, app, conf): + """ + A very simple approach for a WSGI server. + """ + from wsgiref.simple_server import make_server + host, port = conf.server.host, int(conf.server.port) + srv = make_server(host, port, app) + if host == '0.0.0.0': + print 'serving on 0.0.0.0:%s, view at http://127.0.0.1:%s' % \ + (port, port) + else: + print "serving on http://%s:%s" % (host, port) + try: + srv.serve_forever() + except KeyboardInterrupt: + # allow CTRL+C to shutdown + pass diff --git a/pecan/commands/shell.py b/pecan/commands/shell.py index 574a490..78039e1 100644 --- a/pecan/commands/shell.py +++ b/pecan/commands/shell.py @@ -1,27 +1,18 @@ """ -PasteScript shell command for Pecan. +Shell command for Pecan. """ +from pecan.commands import BaseCommand from webtest import TestApp - -from base import Command - import sys -class ShellCommand(Command): +class ShellCommand(BaseCommand): """ Open an interactive shell with the Pecan app loaded. """ - # command information - usage = 'CONFIG_NAME' - summary = __doc__.strip().splitlines()[0].rstrip('.') - - # command options/arguments - min_args = 1 - max_args = 1 - - def command(self): + def run(self, args): + super(ShellCommand, self).run(args) # load the application app = self.load_app() |