Implementing a simple command/plugin library to replace PasteScript, including:
$ pecan serve $ pecan shell
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
16
setup.py
16
setup.py
@@ -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
|
||||
""",
|
||||
|
||||
Reference in New Issue
Block a user