Implementing a simple command/plugin library to replace PasteScript, including:

$ pecan serve
$ pecan shell
This commit is contained in:
Ryan Petrello
2012-03-14 12:27:54 -07:00
parent 40913103ff
commit 74100f2733
7 changed files with 151 additions and 276 deletions

View File

@@ -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

View File

@@ -1,49 +1,108 @@
"""
PasteScript base command for Pecan.
"""
from pecan import load_app
from paste.script import command as paste_command
import pkg_resources
import os.path
import argparse
import logging
import sys
from warnings import warn
from pecan import load_app
log = logging.getLogger(__name__)
class Command(paste_command.Command):
"""
Base class for Pecan commands.
class CommandManager(object):
""" Used to discover `pecan.command` entry points. """
This provides some standard functionality for interacting with Pecan
applications and handles some of the basic PasteScript command cruft.
def __init__(self):
self.commands_ = {}
self.load_commands()
See ``paste.script.command.Command`` for more information.
"""
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})
# command information
group_name = 'Pecan'
summary = ''
def add(self, cmd):
self.commands_.update(cmd)
# command parser
parser = paste_command.Command.standard_parser()
@property
def commands(self):
return self.commands_
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)'
@property
def commands(self):
return self.manager.commands_
class BaseCommand(object):
""" Base class for Pecan commands. """
class __metaclass__(type):
@property
def summary(cls):
return cls.__doc__.strip().splitlines()[0].rstrip('.')
arguments = ({
'command': 'config_file',
'help': 'a Pecan configuration file'
},)
def run(self, args):
self.args = args
def load_app(self):
return load_app(self.validate_file(self.args))
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]
def command(self):
pass
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)

View File

@@ -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
arguments = ({
'command': 'template_name',
'help': 'a registered Pecan template',
'nargs': '?',
'default': DEFAULT_TEMPLATE
},)
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
def run(self, args):
super(CreateCommand, self).run(args)
print "NOT IMPLEMENTED"
print args.template_name

View File

@@ -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)

View File

@@ -1,64 +1,36 @@
"""
PasteScript serve command for Pecan.
Serve command for Pecan.
"""
from paste.script.serve import ServeCommand as _ServeCommand
from base import Command
import re
from pecan.commands import BaseCommand
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:])
)
def run(self, args):
super(ServeCommand, self).run(args)
app = self.load_app()
self.serve(app, app.config)
# 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 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

View File

@@ -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()

View File

@@ -13,7 +13,6 @@ requirements = [
"simplegeneric >= 0.7",
"Mako >= 0.4.0",
"Paste >= 1.7.5.1",
"PasteScript >= 1.7.3",
"WebTest >= 1.2.2"
]
@@ -25,6 +24,11 @@ except:
except:
requirements.append("simplejson >= 2.1.1")
try:
import argparse
except:
requirements.append('argparse')
tests_require = requirements + ['virtualenv']
if sys.version_info < (2, 7):
tests_require += ['unittest2']
@@ -91,12 +95,10 @@ setup(
test_suite='pecan',
cmdclass={'test': test},
entry_points="""
[paste.paster_command]
pecan-serve = pecan.commands:ServeCommand
pecan-shell = pecan.commands:ShellCommand
pecan-create = pecan.commands:CreateCommand
[paste.paster_create_template]
pecan-base = pecan.templates:BaseTemplate
[pecan.command]
serve = pecan.commands:ServeCommand
shell = pecan.commands:ShellCommand
create = pecan.commands:CreateCommand
[console_scripts]
pecan = pecan.commands:CommandRunner.handle_command_line
""",