From a69db4e794a89b32340630318cbf5d6bf64a7184 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 14 Mar 2012 12:27:54 -0700 Subject: [PATCH 01/12] Implementing a simple command/plugin library to replace PasteScript, including: $ pecan serve $ pecan shell --- pecan/commands/__init__.py | 12 ++-- pecan/commands/base.py | 133 ++++++++++++++++++++++++++----------- pecan/commands/create.py | 40 ++++------- pecan/commands/runner.py | 131 ------------------------------------ pecan/commands/serve.py | 69 +++++++------------ pecan/commands/shell.py | 19 ++---- setup.py | 16 +++-- 7 files changed, 151 insertions(+), 269 deletions(-) delete mode 100644 pecan/commands/runner.py 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. -""" -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) 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 + 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 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 0d699e9..3d38ab9 100644 --- a/pecan/commands/serve.py +++ b/pecan/commands/serve.py @@ -1,57 +1,36 @@ """ -PasteScript serve command for Pecan. +Serve command for Pecan. """ -from paste import httpserver -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: httpserver.serve( - app, app.config.server.host, app.config.server.port - )) - - def loadapp(self, app_spec, name, relative_to, **kw): - return self.load_app() + 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() diff --git a/setup.py b/setup.py index bcdd92b..bab63f2 100644 --- a/setup.py +++ b/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 """, From 5888f7c9b50a72f60b8930f1c34122107cceb195 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 14 Mar 2012 16:32:03 -0700 Subject: [PATCH 02/12] A first pass at simple `paste.script` style templates. Committing my work before this code hoses my filesystem. --- pecan/commands/base.py | 35 +++-- pecan/commands/create.py | 14 +- pecan/compat.py | 40 ++++++ pecan/scaffolds/__init__.py | 133 ++++++++++++++++++ .../base}/+package+/__init__.py | 0 .../base}/+package+/app.py_tmpl | 0 .../base}/+package+/controllers/__init__.py | 0 .../base}/+package+/controllers/root.py | 0 .../base}/+package+/model/__init__.py | 0 .../base}/+package+/templates/error.html | 0 .../base}/+package+/templates/index.html | 0 .../base}/+package+/templates/layout.html | 0 .../base}/+package+/tests/__init__.py_tmpl | 0 .../base}/+package+/tests/config.py_tmpl | 0 .../+package+/tests/test_functional.py_tmpl | 0 .../base}/+package+/tests/test_units.py | 0 .../project => scaffolds/base}/MANIFEST.in | 0 .../project => scaffolds/base}/config.py_tmpl | 0 .../base}/public/css/style.css | 0 .../base}/public/images/logo.png | Bin .../project => scaffolds/base}/setup.cfg_tmpl | 0 .../project => scaffolds/base}/setup.py_tmpl | 0 pecan/templates/__init__.py | 9 -- 23 files changed, 200 insertions(+), 31 deletions(-) create mode 100644 pecan/compat.py create mode 100644 pecan/scaffolds/__init__.py rename pecan/{templates/project => scaffolds/base}/+package+/__init__.py (100%) rename pecan/{templates/project => scaffolds/base}/+package+/app.py_tmpl (100%) rename pecan/{templates/project => scaffolds/base}/+package+/controllers/__init__.py (100%) rename pecan/{templates/project => scaffolds/base}/+package+/controllers/root.py (100%) rename pecan/{templates/project => scaffolds/base}/+package+/model/__init__.py (100%) rename pecan/{templates/project => scaffolds/base}/+package+/templates/error.html (100%) rename pecan/{templates/project => scaffolds/base}/+package+/templates/index.html (100%) rename pecan/{templates/project => scaffolds/base}/+package+/templates/layout.html (100%) rename pecan/{templates/project => scaffolds/base}/+package+/tests/__init__.py_tmpl (100%) rename pecan/{templates/project => scaffolds/base}/+package+/tests/config.py_tmpl (100%) rename pecan/{templates/project => scaffolds/base}/+package+/tests/test_functional.py_tmpl (100%) rename pecan/{templates/project => scaffolds/base}/+package+/tests/test_units.py (100%) rename pecan/{templates/project => scaffolds/base}/MANIFEST.in (100%) rename pecan/{templates/project => scaffolds/base}/config.py_tmpl (100%) rename pecan/{templates/project => scaffolds/base}/public/css/style.css (100%) rename pecan/{templates/project => scaffolds/base}/public/images/logo.png (100%) rename pecan/{templates/project => scaffolds/base}/setup.cfg_tmpl (100%) rename pecan/{templates/project => scaffolds/base}/setup.py_tmpl (100%) delete mode 100644 pecan/templates/__init__.py diff --git a/pecan/commands/base.py b/pecan/commands/base.py index 57c4115..b997d9f 100644 --- a/pecan/commands/base.py +++ b/pecan/commands/base.py @@ -9,11 +9,27 @@ from pecan import load_app log = logging.getLogger(__name__) +class HelpfulArgumentParser(argparse.ArgumentParser): + + def error(self, message): + """error(message: string) + + Prints a usage message incorporating the message to stderr and + exits. + + If you override this in a subclass, it should not return -- it + should either exit or raise an exception. + """ + self.print_help(sys.stderr) + self._print_message('\n') + self.exit(2, '%s: %s\n' % (self.prog, message)) + + class CommandManager(object): """ Used to discover `pecan.command` entry points. """ def __init__(self): - self.commands_ = {} + self.commands = {} self.load_commands() def load_commands(self): @@ -28,11 +44,7 @@ class CommandManager(object): self.add({ep.name: cmd}) def add(self, cmd): - self.commands_.update(cmd) - - @property - def commands(self): - return self.commands_ + self.commands.update(cmd) class CommandRunner(object): @@ -40,13 +52,13 @@ class CommandRunner(object): def __init__(self): self.manager = CommandManager() - self.parser = argparse.ArgumentParser( + self.parser = HelpfulArgumentParser( version='Pecan %s' % self.version, add_help=True ) - self.parse_commands() + self.parse_sub_commands() - def parse_commands(self): + def parse_sub_commands(self): subparsers = self.parser.add_subparsers( dest='command_name', metavar='command' @@ -67,8 +79,7 @@ class CommandRunner(object): @classmethod def handle_command_line(cls): runner = CommandRunner() - exit_code = runner.run(sys.argv[1:]) - sys.exit(exit_code) + runner.run(sys.argv[1:]) @property def version(self): @@ -83,7 +94,7 @@ class CommandRunner(object): @property def commands(self): - return self.manager.commands_ + return self.manager.commands class BaseCommand(object): diff --git a/pecan/commands/create.py b/pecan/commands/create.py index ade6301..b516e1f 100644 --- a/pecan/commands/create.py +++ b/pecan/commands/create.py @@ -2,29 +2,23 @@ Create command for Pecan """ from pecan.commands import BaseCommand -from pecan.templates import DEFAULT_TEMPLATE - -import copy -import sys +from pecan.scaffolds import DEFAULT_SCAFFOLD, BaseScaffold class CreateCommand(BaseCommand): """ - Creates the file layout for a new Pecan distribution. - - For a template to show up when using this command, its name must begin - with "pecan-". Although not required, it should also include the "Pecan" - egg plugin for user convenience. + Creates the file layout for a new Pecan scaffolded project. """ arguments = ({ 'command': 'template_name', 'help': 'a registered Pecan template', 'nargs': '?', - 'default': DEFAULT_TEMPLATE + 'default': DEFAULT_SCAFFOLD },) def run(self, args): super(CreateCommand, self).run(args) print "NOT IMPLEMENTED" print args.template_name + BaseScaffold().copy_to(args.template_name) diff --git a/pecan/compat.py b/pecan/compat.py new file mode 100644 index 0000000..0e0bdc0 --- /dev/null +++ b/pecan/compat.py @@ -0,0 +1,40 @@ +import sys + +# True if we are running on Python 3. +PY3 = sys.version_info[0] == 3 + +if PY3: # pragma: no cover + text_type = str +else: + text_type = unicode + + +def bytes_(s, encoding='latin-1', errors='strict'): + """ If ``s`` is an instance of ``text_type``, return + ``s.encode(encoding, errors)``, otherwise return ``s``""" + if isinstance(s, text_type): # pragma: no cover + return s.encode(encoding, errors) + return s + +if PY3: # pragma: no cover + def native_(s, encoding='latin-1', errors='strict'): + """ If ``s`` is an instance of ``text_type``, return + ``s``, otherwise return ``str(s, encoding, errors)``""" + if isinstance(s, text_type): + return s + return str(s, encoding, errors) +else: + def native_(s, encoding='latin-1', errors='strict'): # noqa + """ If ``s`` is an instance of ``text_type``, return + ``s.encode(encoding, errors)``, otherwise return ``str(s)``""" + if isinstance(s, text_type): + return s.encode(encoding, errors) + return str(s) + +native_.__doc__ = """ +Python 3: If ``s`` is an instance of ``text_type``, return ``s``, otherwise +return ``str(s, encoding, errors)`` + +Python 2: If ``s`` is an instance of ``text_type``, return +``s.encode(encoding, errors)``, otherwise return ``str(s)`` +""" diff --git a/pecan/scaffolds/__init__.py b/pecan/scaffolds/__init__.py new file mode 100644 index 0000000..eb3d1af --- /dev/null +++ b/pecan/scaffolds/__init__.py @@ -0,0 +1,133 @@ +import sys +import os +import re +import pkg_resources +from pecan.compat import native_, bytes_ + +DEFAULT_SCAFFOLD = 'base' + + +class PecanScaffold(object): + + @property + def template_dir(self): + if isinstance(self._scaffold_dir, tuple): + return self._scaffold_dir + else: + return os.path.join(self.module_dir, self._scaffold_dir) + + @property + def module_dir(self): + mod = sys.modules[self.__class__.__module__] + return os.path.dirname(mod.__file__) + + @property + def variables(self): + return {} + + def copy_to(self, dest, **kwargs): + copy_dir(self.template_dir, dest, self.variables) + + +class BaseScaffold(PecanScaffold): + _scaffold_dir = 'base' + + +def copy_dir(source, dest, variables, out_=sys.stdout): + """ + Copies the ``source`` directory to the ``dest`` directory. + + ``variables``: A dictionary of variables to use in any substitutions. + + ``out_``: File object to write to + """ + def out(msg): + out_.write(msg) + out_.write('\n') + out_.flush() + + use_pkg_resources = isinstance(source, tuple) + + if use_pkg_resources: + names = sorted(pkg_resources.resource_listdir(source[0], source[1])) + else: + names = sorted(os.listdir(source)) + if not os.path.exists(dest): + out('Creating %s' % dest) + makedirs(dest) + else: + out('Directory %s already exists' % dest) + return + + for name in names: + + if use_pkg_resources: + full = '/'.join([source[1], name]) + else: + full = os.path.join(source, name) + + dest_full = os.path.join(dest, substitute_filename(name, variables)) + + sub_file = False + if dest_full.endswith('_tmpl'): + dest_full = dest_full[:-5] + sub_file = True + + if use_pkg_resources and pkg_resources.resource_isdir(source[0], full): + out('Recursing into %s' % os.path.basename(full)) + copy_dir((source[0], full), dest_full, variables, out_) + continue + elif not use_pkg_resources and os.path.isdir(full): + out('Recursing into %s' % os.path.basename(full)) + copy_dir(full, dest_full, variables, out_) + continue + elif use_pkg_resources: + content = pkg_resources.resource_string(source[0], full) + else: + f = open(full, 'rb') + content = f.read() + f.close() + + if sub_file: + content = render_template(content, variables) + if content is None: + continue # pragma: no cover + + if use_pkg_resources: + out('Copying %s to %s' % (full, dest_full)) + else: + out('Copying %s to %s' % ( + os.path.basename(full), + dest_full) + ) + + +def makedirs(directory): + parent = os.path.dirname(os.path.abspath(directory)) + if not os.path.exists(parent): + makedirs(parent) + os.mkdir(directory) + + +def substitute_filename(fn, variables): + for var, value in variables.items(): + fn = fn.replace('+%s+' % var, str(value)) + return fn + + +def render_template(self, content, variables): + """ Return a bytestring representing a templated file based on the + input (content) and the variable names defined (vars).""" + fsenc = sys.getfilesystemencoding() + content = native_(content, fsenc) + return bytes_( + substitute_double_braces(content, variables), fsenc) + + +def substitute_double_braces(content, values): + double_brace_pattern = re.compile(r'{{(?P.*?)}}') + + def double_bracerepl(match): + value = match.group('braced').strip() + return values[value] + return double_brace_pattern.sub(double_bracerepl, content) diff --git a/pecan/templates/project/+package+/__init__.py b/pecan/scaffolds/base/+package+/__init__.py similarity index 100% rename from pecan/templates/project/+package+/__init__.py rename to pecan/scaffolds/base/+package+/__init__.py diff --git a/pecan/templates/project/+package+/app.py_tmpl b/pecan/scaffolds/base/+package+/app.py_tmpl similarity index 100% rename from pecan/templates/project/+package+/app.py_tmpl rename to pecan/scaffolds/base/+package+/app.py_tmpl diff --git a/pecan/templates/project/+package+/controllers/__init__.py b/pecan/scaffolds/base/+package+/controllers/__init__.py similarity index 100% rename from pecan/templates/project/+package+/controllers/__init__.py rename to pecan/scaffolds/base/+package+/controllers/__init__.py diff --git a/pecan/templates/project/+package+/controllers/root.py b/pecan/scaffolds/base/+package+/controllers/root.py similarity index 100% rename from pecan/templates/project/+package+/controllers/root.py rename to pecan/scaffolds/base/+package+/controllers/root.py diff --git a/pecan/templates/project/+package+/model/__init__.py b/pecan/scaffolds/base/+package+/model/__init__.py similarity index 100% rename from pecan/templates/project/+package+/model/__init__.py rename to pecan/scaffolds/base/+package+/model/__init__.py diff --git a/pecan/templates/project/+package+/templates/error.html b/pecan/scaffolds/base/+package+/templates/error.html similarity index 100% rename from pecan/templates/project/+package+/templates/error.html rename to pecan/scaffolds/base/+package+/templates/error.html diff --git a/pecan/templates/project/+package+/templates/index.html b/pecan/scaffolds/base/+package+/templates/index.html similarity index 100% rename from pecan/templates/project/+package+/templates/index.html rename to pecan/scaffolds/base/+package+/templates/index.html diff --git a/pecan/templates/project/+package+/templates/layout.html b/pecan/scaffolds/base/+package+/templates/layout.html similarity index 100% rename from pecan/templates/project/+package+/templates/layout.html rename to pecan/scaffolds/base/+package+/templates/layout.html diff --git a/pecan/templates/project/+package+/tests/__init__.py_tmpl b/pecan/scaffolds/base/+package+/tests/__init__.py_tmpl similarity index 100% rename from pecan/templates/project/+package+/tests/__init__.py_tmpl rename to pecan/scaffolds/base/+package+/tests/__init__.py_tmpl diff --git a/pecan/templates/project/+package+/tests/config.py_tmpl b/pecan/scaffolds/base/+package+/tests/config.py_tmpl similarity index 100% rename from pecan/templates/project/+package+/tests/config.py_tmpl rename to pecan/scaffolds/base/+package+/tests/config.py_tmpl diff --git a/pecan/templates/project/+package+/tests/test_functional.py_tmpl b/pecan/scaffolds/base/+package+/tests/test_functional.py_tmpl similarity index 100% rename from pecan/templates/project/+package+/tests/test_functional.py_tmpl rename to pecan/scaffolds/base/+package+/tests/test_functional.py_tmpl diff --git a/pecan/templates/project/+package+/tests/test_units.py b/pecan/scaffolds/base/+package+/tests/test_units.py similarity index 100% rename from pecan/templates/project/+package+/tests/test_units.py rename to pecan/scaffolds/base/+package+/tests/test_units.py diff --git a/pecan/templates/project/MANIFEST.in b/pecan/scaffolds/base/MANIFEST.in similarity index 100% rename from pecan/templates/project/MANIFEST.in rename to pecan/scaffolds/base/MANIFEST.in diff --git a/pecan/templates/project/config.py_tmpl b/pecan/scaffolds/base/config.py_tmpl similarity index 100% rename from pecan/templates/project/config.py_tmpl rename to pecan/scaffolds/base/config.py_tmpl diff --git a/pecan/templates/project/public/css/style.css b/pecan/scaffolds/base/public/css/style.css similarity index 100% rename from pecan/templates/project/public/css/style.css rename to pecan/scaffolds/base/public/css/style.css diff --git a/pecan/templates/project/public/images/logo.png b/pecan/scaffolds/base/public/images/logo.png similarity index 100% rename from pecan/templates/project/public/images/logo.png rename to pecan/scaffolds/base/public/images/logo.png diff --git a/pecan/templates/project/setup.cfg_tmpl b/pecan/scaffolds/base/setup.cfg_tmpl similarity index 100% rename from pecan/templates/project/setup.cfg_tmpl rename to pecan/scaffolds/base/setup.cfg_tmpl diff --git a/pecan/templates/project/setup.py_tmpl b/pecan/scaffolds/base/setup.py_tmpl similarity index 100% rename from pecan/templates/project/setup.py_tmpl rename to pecan/scaffolds/base/setup.py_tmpl diff --git a/pecan/templates/__init__.py b/pecan/templates/__init__.py deleted file mode 100644 index e9034fa..0000000 --- a/pecan/templates/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from paste.script.templates import Template - -DEFAULT_TEMPLATE = 'base' - - -class BaseTemplate(Template): - summary = 'Template for creating a basic Pecan project' - _template_dir = 'project' - egg_plugins = ['Pecan'] From e7093cc14a194ca80d6b5b9757a60c68302eb98f Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 14 Mar 2012 18:13:23 -0700 Subject: [PATCH 03/12] Now `pecan create works`. --- pecan/commands/create.py | 9 +++++---- pecan/scaffolds/__init__.py | 27 ++++++++++++--------------- pecan/scaffolds/base/setup.py_tmpl | 3 +-- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/pecan/commands/create.py b/pecan/commands/create.py index b516e1f..006b837 100644 --- a/pecan/commands/create.py +++ b/pecan/commands/create.py @@ -11,14 +11,15 @@ class CreateCommand(BaseCommand): """ arguments = ({ + 'command': 'destination', + 'help': 'the destination to create the new project' + }, { 'command': 'template_name', 'help': 'a registered Pecan template', 'nargs': '?', 'default': DEFAULT_SCAFFOLD - },) + }) def run(self, args): super(CreateCommand, self).run(args) - print "NOT IMPLEMENTED" - print args.template_name - BaseScaffold().copy_to(args.template_name) + BaseScaffold().copy_to(args.destination) diff --git a/pecan/scaffolds/__init__.py b/pecan/scaffolds/__init__.py index eb3d1af..be7b24c 100644 --- a/pecan/scaffolds/__init__.py +++ b/pecan/scaffolds/__init__.py @@ -1,7 +1,7 @@ import sys import os -import re import pkg_resources +from string import Template from pecan.compat import native_, bytes_ DEFAULT_SCAFFOLD = 'base' @@ -23,10 +23,13 @@ class PecanScaffold(object): @property def variables(self): - return {} + return { + 'package': self.dest + } def copy_to(self, dest, **kwargs): - copy_dir(self.template_dir, dest, self.variables) + self.dest = dest + copy_dir(self.template_dir, self.dest, self.variables) class BaseScaffold(PecanScaffold): @@ -101,6 +104,10 @@ def copy_dir(source, dest, variables, out_=sys.stdout): dest_full) ) + f = open(dest_full, 'wb') + f.write(content) + f.close() + def makedirs(directory): parent = os.path.dirname(os.path.abspath(directory)) @@ -115,19 +122,9 @@ def substitute_filename(fn, variables): return fn -def render_template(self, content, variables): +def render_template(content, variables): """ Return a bytestring representing a templated file based on the input (content) and the variable names defined (vars).""" fsenc = sys.getfilesystemencoding() content = native_(content, fsenc) - return bytes_( - substitute_double_braces(content, variables), fsenc) - - -def substitute_double_braces(content, values): - double_brace_pattern = re.compile(r'{{(?P.*?)}}') - - def double_bracerepl(match): - value = match.group('braced').strip() - return values[value] - return double_brace_pattern.sub(double_bracerepl, content) + return bytes_(Template(content).substitute(variables), fsenc) diff --git a/pecan/scaffolds/base/setup.py_tmpl b/pecan/scaffolds/base/setup.py_tmpl index 6a61f15..ed32100 100644 --- a/pecan/scaffolds/base/setup.py_tmpl +++ b/pecan/scaffolds/base/setup.py_tmpl @@ -7,7 +7,7 @@ except ImportError: from setuptools import setup, find_packages setup( - name = '${project}', + name = '${package}', version = '0.1', description = '', author = '', @@ -17,7 +17,6 @@ setup( ], test_suite = '${package}', zip_safe = False, - paster_plugins = ${egg_plugins}, include_package_data = True, packages = find_packages(exclude=['ez_setup']) ) From b1beb746462e4d275aeb2428915ef0c019c7cf6c Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 14 Mar 2012 19:15:22 -0700 Subject: [PATCH 04/12] Fixing a few bugs in `pecan create`. --- pecan/commands/create.py | 6 +++--- pecan/scaffolds/__init__.py | 13 +++++-------- .../tests/{test_templates.py => test_scaffolds.py} | 4 +++- 3 files changed, 11 insertions(+), 12 deletions(-) rename pecan/tests/{test_templates.py => test_scaffolds.py} (97%) diff --git a/pecan/commands/create.py b/pecan/commands/create.py index 006b837..4d687f5 100644 --- a/pecan/commands/create.py +++ b/pecan/commands/create.py @@ -11,8 +11,8 @@ class CreateCommand(BaseCommand): """ arguments = ({ - 'command': 'destination', - 'help': 'the destination to create the new project' + 'command': 'project_name', + 'help': 'the (package) name of the new project' }, { 'command': 'template_name', 'help': 'a registered Pecan template', @@ -22,4 +22,4 @@ class CreateCommand(BaseCommand): def run(self, args): super(CreateCommand, self).run(args) - BaseScaffold().copy_to(args.destination) + BaseScaffold().copy_to(args.project_name) diff --git a/pecan/scaffolds/__init__.py b/pecan/scaffolds/__init__.py index be7b24c..304d0c6 100644 --- a/pecan/scaffolds/__init__.py +++ b/pecan/scaffolds/__init__.py @@ -1,10 +1,12 @@ import sys import os +import re import pkg_resources from string import Template from pecan.compat import native_, bytes_ DEFAULT_SCAFFOLD = 'base' +_bad_chars_re = re.compile('[^a-zA-Z0-9_]') class PecanScaffold(object): @@ -21,15 +23,10 @@ class PecanScaffold(object): mod = sys.modules[self.__class__.__module__] return os.path.dirname(mod.__file__) - @property - def variables(self): - return { - 'package': self.dest - } - def copy_to(self, dest, **kwargs): - self.dest = dest - copy_dir(self.template_dir, self.dest, self.variables) + output_dir = os.path.abspath(os.path.normpath(dest)) + pkg_name = _bad_chars_re.sub('', dest.lower()) + copy_dir(self.template_dir, output_dir, {'package': pkg_name}) class BaseScaffold(PecanScaffold): diff --git a/pecan/tests/test_templates.py b/pecan/tests/test_scaffolds.py similarity index 97% rename from pecan/tests/test_templates.py rename to pecan/tests/test_scaffolds.py index 225378a..ad13941 100644 --- a/pecan/tests/test_templates.py +++ b/pecan/tests/test_scaffolds.py @@ -132,7 +132,9 @@ class TestTemplateBuilds(unittest.TestCase): self.poll(proc) - out, _ = proc.communicate('{"model" : model, "conf" : conf, "app" : app}') + out, _ = proc.communicate( + '{"model" : model, "conf" : conf, "app" : app}' + ) assert 'testing123.model' in out assert 'Config(' in out assert 'webtest.app.TestApp' in out From 8b92278c7c95320541df5284044bef4fcf9bbbe8 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 14 Mar 2012 21:55:05 -0700 Subject: [PATCH 05/12] More refactoring. --- pecan/commands/create.py | 41 +++++++++++++++++++++++++++++++++++++--- setup.py | 2 ++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/pecan/commands/create.py b/pecan/commands/create.py index 4d687f5..508f766 100644 --- a/pecan/commands/create.py +++ b/pecan/commands/create.py @@ -1,8 +1,37 @@ """ Create command for Pecan """ +import pkg_resources +import logging +from warnings import warn from pecan.commands import BaseCommand -from pecan.scaffolds import DEFAULT_SCAFFOLD, BaseScaffold +from pecan.scaffolds import DEFAULT_SCAFFOLD + +log = logging.getLogger(__name__) + + +class ScaffoldManager(object): + """ Used to discover `pecan.scaffold` entry points. """ + + def __init__(self): + self.scaffolds = {} + self.load_scaffolds() + + def load_scaffolds(self): + for ep in pkg_resources.iter_entry_points('pecan.scaffold'): + log.debug('%s loading scaffold %s', self.__class__.__name__, ep) + try: + cmd = ep.load() + assert hasattr(cmd, 'copy_to') + except Exception, e: + warn( + "Unable to load scaffold %s: %s" % (ep, e), RuntimeWarning + ) + continue + self.add({ep.name: cmd}) + + def add(self, cmd): + self.scaffolds.update(cmd) class CreateCommand(BaseCommand): @@ -10,16 +39,22 @@ class CreateCommand(BaseCommand): Creates the file layout for a new Pecan scaffolded project. """ + manager = ScaffoldManager() + arguments = ({ 'command': 'project_name', 'help': 'the (package) name of the new project' }, { + 'metavar': 'template_name', 'command': 'template_name', 'help': 'a registered Pecan template', 'nargs': '?', - 'default': DEFAULT_SCAFFOLD + 'default': DEFAULT_SCAFFOLD, + 'choices': manager.scaffolds.keys() }) def run(self, args): super(CreateCommand, self).run(args) - BaseScaffold().copy_to(args.project_name) + CreateCommand.manager.scaffolds[args.template_name]().copy_to( + args.project_name + ) diff --git a/setup.py b/setup.py index bab63f2..f6141b0 100644 --- a/setup.py +++ b/setup.py @@ -99,6 +99,8 @@ setup( serve = pecan.commands:ServeCommand shell = pecan.commands:ShellCommand create = pecan.commands:CreateCommand + [pecan.scaffold] + base = pecan.scaffolds:BaseScaffold [console_scripts] pecan = pecan.commands:CommandRunner.handle_command_line """, From 5681e8e11b155d7ef77b2c78a28fe343b9f65966 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 14 Mar 2012 22:19:13 -0700 Subject: [PATCH 06/12] Fixing a few .rst formatting issues. --- docs/source/databases.rst | 10 ++++++---- docs/source/hooks.rst | 2 +- docs/source/sessions.rst | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/source/databases.rst b/docs/source/databases.rst index 97d7336..0ab5f30 100644 --- a/docs/source/databases.rst +++ b/docs/source/databases.rst @@ -1,14 +1,14 @@ .. _databases: Working with Databases, Transactions, and ORM's -============= +=============================================== Out of the box, Pecan provides no opinionated support for working with databases, but it's easy to hook into your ORM of choice with minimal effort. This article details best practices for integrating the popular Python ORM, SQLAlchemy, into your Pecan project. ``init_model`` and Preparing Your Model ----------------- +--------------------------------------- Pecan's default quickstart project includes an empty stub directory for implementing your model as you see fit:: @@ -96,7 +96,8 @@ Here's what a sample Pecan configuration file with database bindings might look Session.remove() Binding Within the Application ----------------- +------------------------------ + There are several approaches that can be taken to wrap your application's requests with calls to appropriate model function calls. One approach is WSGI middleware. We also recommend Pecan :ref:`hooks`. Pecan comes with ``TransactionHook``, a hook which can @@ -146,7 +147,8 @@ manner: Also note that there is a useful ``@after_commit`` decorator provided in :ref:`pecan_decorators`. Splitting Reads and Writes ----------------- +-------------------------- + Employing the strategy above with ``TransactionHook`` makes it very simple to split database reads and writes based upon HTTP methods (i.e., GET/HEAD requests are read-only and would potentially be routed to a read-only database slave, while POST/PUT/DELETE requests require writing, and diff --git a/docs/source/hooks.rst b/docs/source/hooks.rst index d95e21e..08d8b7f 100644 --- a/docs/source/hooks.rst +++ b/docs/source/hooks.rst @@ -45,7 +45,7 @@ was chosen by Pecan's routing. ``on_error`` is passed a shared state object **and** the original exception. Attaching Hooks --------------- +--------------- Hooks can be attached in a project-wide manner by specifying a list of hooks in your project's ``app.py`` file:: diff --git a/docs/source/sessions.rst b/docs/source/sessions.rst index 260dc18..b7960b2 100644 --- a/docs/source/sessions.rst +++ b/docs/source/sessions.rst @@ -1,7 +1,7 @@ .. _session: Working with Sessions and User Authentication -============= +============================================= Out of the box, Pecan provides no opinionated support for managing user sessions, but it's easy to hook into your session framework of choice with minimal effort. @@ -10,7 +10,7 @@ This article details best practices for integrating the popular session framework, `Beaker `_, into your Pecan project. Setting up Session Management ----------------- +----------------------------- There are several approaches that can be taken to set up session management. One approach is WSGI middleware. Another is Pecan :ref:`hooks`. From 62c2ed5a20cd107d6071ccb4aa718d647ffbfd91 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 14 Mar 2012 23:13:00 -0700 Subject: [PATCH 07/12] Fixing a few bugs in `pecan create`. --- MANIFEST.in | 4 +-- pecan/scaffolds/__init__.py | 57 +++++++------------------------------ 2 files changed, 13 insertions(+), 48 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 872e419..f1289f7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -recursive-include pecan/templates/project * -include pecan/templates/project/* +recursive-include pecan/scaffolds/base * +include pecan/scaffolds/base/* diff --git a/pecan/scaffolds/__init__.py b/pecan/scaffolds/__init__.py index 304d0c6..d802784 100644 --- a/pecan/scaffolds/__init__.py +++ b/pecan/scaffolds/__init__.py @@ -11,29 +11,17 @@ _bad_chars_re = re.compile('[^a-zA-Z0-9_]') class PecanScaffold(object): - @property - def template_dir(self): - if isinstance(self._scaffold_dir, tuple): - return self._scaffold_dir - else: - return os.path.join(self.module_dir, self._scaffold_dir) - - @property - def module_dir(self): - mod = sys.modules[self.__class__.__module__] - return os.path.dirname(mod.__file__) - def copy_to(self, dest, **kwargs): output_dir = os.path.abspath(os.path.normpath(dest)) pkg_name = _bad_chars_re.sub('', dest.lower()) - copy_dir(self.template_dir, output_dir, {'package': pkg_name}) + copy_dir(self._scaffold_dir, output_dir, {'package': pkg_name}) class BaseScaffold(PecanScaffold): - _scaffold_dir = 'base' + _scaffold_dir = ('pecan', 'scaffolds/base') -def copy_dir(source, dest, variables, out_=sys.stdout): +def copy_dir(source, dest, variables, out_=sys.stdout, i=0): """ Copies the ``source`` directory to the ``dest`` directory. @@ -42,30 +30,21 @@ def copy_dir(source, dest, variables, out_=sys.stdout): ``out_``: File object to write to """ def out(msg): - out_.write(msg) + out_.write('%s%s' % (' ' * (i * 2), msg)) out_.write('\n') out_.flush() - use_pkg_resources = isinstance(source, tuple) - - if use_pkg_resources: - names = sorted(pkg_resources.resource_listdir(source[0], source[1])) - else: - names = sorted(os.listdir(source)) + names = sorted(pkg_resources.resource_listdir(source[0], source[1])) if not os.path.exists(dest): out('Creating %s' % dest) makedirs(dest) else: - out('Directory %s already exists' % dest) + out('%s already exists' % dest) return for name in names: - if use_pkg_resources: - full = '/'.join([source[1], name]) - else: - full = os.path.join(source, name) - + full = '/'.join([source[1], name]) dest_full = os.path.join(dest, substitute_filename(name, variables)) sub_file = False @@ -73,33 +52,19 @@ def copy_dir(source, dest, variables, out_=sys.stdout): dest_full = dest_full[:-5] sub_file = True - if use_pkg_resources and pkg_resources.resource_isdir(source[0], full): + if pkg_resources.resource_isdir(source[0], full): out('Recursing into %s' % os.path.basename(full)) - copy_dir((source[0], full), dest_full, variables, out_) + copy_dir((source[0], full), dest_full, variables, out_, i + 1) continue - elif not use_pkg_resources and os.path.isdir(full): - out('Recursing into %s' % os.path.basename(full)) - copy_dir(full, dest_full, variables, out_) - continue - elif use_pkg_resources: - content = pkg_resources.resource_string(source[0], full) else: - f = open(full, 'rb') - content = f.read() - f.close() + content = pkg_resources.resource_string(source[0], full) if sub_file: content = render_template(content, variables) if content is None: continue # pragma: no cover - if use_pkg_resources: - out('Copying %s to %s' % (full, dest_full)) - else: - out('Copying %s to %s' % ( - os.path.basename(full), - dest_full) - ) + out('Copying %s to %s' % (full, dest_full)) f = open(dest_full, 'wb') f.write(content) From 8f79aba8fc9e417d5e05742acb774164a6a4b4b4 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 15 Mar 2012 09:40:21 -0700 Subject: [PATCH 08/12] Clarifying some scaffolding docs. --- pecan/scaffolds/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pecan/scaffolds/__init__.py b/pecan/scaffolds/__init__.py index d802784..f2d65a9 100644 --- a/pecan/scaffolds/__init__.py +++ b/pecan/scaffolds/__init__.py @@ -18,12 +18,17 @@ class PecanScaffold(object): class BaseScaffold(PecanScaffold): - _scaffold_dir = ('pecan', 'scaffolds/base') + _scaffold_dir = ('pecan', os.path.join('scaffolds', 'base')) def copy_dir(source, dest, variables, out_=sys.stdout, i=0): """ - Copies the ``source`` directory to the ``dest`` directory. + Copies the ``source`` directory to the ``dest`` directory, where + ``source`` is some tuple representing an installed package and a + subdirectory, e.g., + + ('pecan', os.path.join('scaffolds', 'base')) + ('pecan_sqlalchemy', os.path.join('scaffolds', 'sqlalchemy')) ``variables``: A dictionary of variables to use in any substitutions. From 392f0ebb6e1e46c04bc28b4ff4794c50936dbf56 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 15 Mar 2012 10:04:58 -0700 Subject: [PATCH 09/12] More `pecan.scaffold` inline documentation and 100% test coverage. --- pecan/scaffolds/__init__.py | 41 ++++- pecan/tests/scaffold_fixtures/__init__.py | 0 .../content_sub/bar/spam.txt_tmpl | 1 + .../scaffold_fixtures/content_sub/foo_tmpl | 1 + .../file_sub/bar_+package+/spam.txt | 1 + .../scaffold_fixtures/file_sub/foo_+package+ | 1 + .../scaffold_fixtures/simple/bar/spam.txt | 1 + pecan/tests/scaffold_fixtures/simple/foo | 1 + pecan/tests/test_scaffolds.py | 148 +++++++++++++++++- 9 files changed, 181 insertions(+), 14 deletions(-) create mode 100644 pecan/tests/scaffold_fixtures/__init__.py create mode 100644 pecan/tests/scaffold_fixtures/content_sub/bar/spam.txt_tmpl create mode 100644 pecan/tests/scaffold_fixtures/content_sub/foo_tmpl create mode 100644 pecan/tests/scaffold_fixtures/file_sub/bar_+package+/spam.txt create mode 100644 pecan/tests/scaffold_fixtures/file_sub/foo_+package+ create mode 100644 pecan/tests/scaffold_fixtures/simple/bar/spam.txt create mode 100644 pecan/tests/scaffold_fixtures/simple/foo diff --git a/pecan/scaffolds/__init__.py b/pecan/scaffolds/__init__.py index f2d65a9..1da3a44 100644 --- a/pecan/scaffolds/__init__.py +++ b/pecan/scaffolds/__init__.py @@ -10,10 +10,30 @@ _bad_chars_re = re.compile('[^a-zA-Z0-9_]') class PecanScaffold(object): + """ + A base Pecan scaffold. New scaffolded implementations should extend this + class and define a ``_scaffold_dir`` attribute, e.g., - def copy_to(self, dest, **kwargs): - output_dir = os.path.abspath(os.path.normpath(dest)) - pkg_name = _bad_chars_re.sub('', dest.lower()) + class CoolAddOnScaffold(PecanScaffold): + + _scaffold_dir = ('package', os.path.join('scaffolds', 'scaffold_name')) + + ...where... + + pkg_resources.resource_listdir(_scaffold_dir[0], _scaffold_dir[1])) + + ...points to some scaffold directory root. + """ + + def normalize_output_dir(self, dest): + return os.path.abspath(os.path.normpath(dest)) + + def normalize_pkg_name(self, dest): + return _bad_chars_re.sub('', dest.lower()) + + def copy_to(self, dest): + output_dir = self.normalize_output_dir(dest) + pkg_name = self.normalize_pkg_name(dest) copy_dir(self._scaffold_dir, output_dir, {'package': pkg_name}) @@ -25,14 +45,15 @@ def copy_dir(source, dest, variables, out_=sys.stdout, i=0): """ Copies the ``source`` directory to the ``dest`` directory, where ``source`` is some tuple representing an installed package and a - subdirectory, e.g., + subdirectory in the package, e.g., ('pecan', os.path.join('scaffolds', 'base')) - ('pecan_sqlalchemy', os.path.join('scaffolds', 'sqlalchemy')) + ('pecan_extension', os.path.join('scaffolds', 'scaffold_name')) ``variables``: A dictionary of variables to use in any substitutions. + Substitution is performed via ``string.Template``. - ``out_``: File object to write to + ``out_``: File object to write to (default is sys.stdout). """ def out(msg): out_.write('%s%s' % (' ' * (i * 2), msg)) @@ -77,6 +98,7 @@ def copy_dir(source, dest, variables, out_=sys.stdout, i=0): def makedirs(directory): + """ Resursively create a named directory. """ parent = os.path.dirname(os.path.abspath(directory)) if not os.path.exists(parent): makedirs(parent) @@ -84,14 +106,17 @@ def makedirs(directory): def substitute_filename(fn, variables): + """ Substitute +variables+ in file directory names. """ for var, value in variables.items(): fn = fn.replace('+%s+' % var, str(value)) return fn def render_template(content, variables): - """ Return a bytestring representing a templated file based on the - input (content) and the variable names defined (vars).""" + """ + Return a bytestring representing a templated file based on the + input (content) and the variable names defined (vars). + """ fsenc = sys.getfilesystemencoding() content = native_(content, fsenc) return bytes_(Template(content).substitute(variables), fsenc) diff --git a/pecan/tests/scaffold_fixtures/__init__.py b/pecan/tests/scaffold_fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pecan/tests/scaffold_fixtures/content_sub/bar/spam.txt_tmpl b/pecan/tests/scaffold_fixtures/content_sub/bar/spam.txt_tmpl new file mode 100644 index 0000000..95a9c91 --- /dev/null +++ b/pecan/tests/scaffold_fixtures/content_sub/bar/spam.txt_tmpl @@ -0,0 +1 @@ +Pecan ${package} diff --git a/pecan/tests/scaffold_fixtures/content_sub/foo_tmpl b/pecan/tests/scaffold_fixtures/content_sub/foo_tmpl new file mode 100644 index 0000000..25591f3 --- /dev/null +++ b/pecan/tests/scaffold_fixtures/content_sub/foo_tmpl @@ -0,0 +1 @@ +YAR ${package} diff --git a/pecan/tests/scaffold_fixtures/file_sub/bar_+package+/spam.txt b/pecan/tests/scaffold_fixtures/file_sub/bar_+package+/spam.txt new file mode 100644 index 0000000..02c61ad --- /dev/null +++ b/pecan/tests/scaffold_fixtures/file_sub/bar_+package+/spam.txt @@ -0,0 +1 @@ +Pecan diff --git a/pecan/tests/scaffold_fixtures/file_sub/foo_+package+ b/pecan/tests/scaffold_fixtures/file_sub/foo_+package+ new file mode 100644 index 0000000..035599b --- /dev/null +++ b/pecan/tests/scaffold_fixtures/file_sub/foo_+package+ @@ -0,0 +1 @@ +YAR diff --git a/pecan/tests/scaffold_fixtures/simple/bar/spam.txt b/pecan/tests/scaffold_fixtures/simple/bar/spam.txt new file mode 100644 index 0000000..02c61ad --- /dev/null +++ b/pecan/tests/scaffold_fixtures/simple/bar/spam.txt @@ -0,0 +1 @@ +Pecan diff --git a/pecan/tests/scaffold_fixtures/simple/foo b/pecan/tests/scaffold_fixtures/simple/foo new file mode 100644 index 0000000..035599b --- /dev/null +++ b/pecan/tests/scaffold_fixtures/simple/foo @@ -0,0 +1 @@ +YAR diff --git a/pecan/tests/test_scaffolds.py b/pecan/tests/test_scaffolds.py index ad13941..aa2e7bb 100644 --- a/pecan/tests/test_scaffolds.py +++ b/pecan/tests/test_scaffolds.py @@ -12,18 +12,155 @@ import pecan if sys.version_info < (2, 7): import unittest2 as unittest else: - import unittest + import unittest # noqa def has_internet(): try: - response = urllib2.urlopen('http://google.com', timeout=1) + urllib2.urlopen('http://google.com', timeout=1) return True except urllib2.URLError: pass # pragma: no cover return False +class TestPecanScaffold(unittest.TestCase): + + def test_normalize_pkg_name(self): + from pecan.scaffolds import PecanScaffold + s = PecanScaffold() + assert s.normalize_pkg_name('sam') == 'sam' + assert s.normalize_pkg_name('sam1') == 'sam1' + assert s.normalize_pkg_name('sam_') == 'sam_' + assert s.normalize_pkg_name('Sam') == 'sam' + assert s.normalize_pkg_name('SAM') == 'sam' + assert s.normalize_pkg_name('sam ') == 'sam' + assert s.normalize_pkg_name(' sam') == 'sam' + assert s.normalize_pkg_name('sam$') == 'sam' + assert s.normalize_pkg_name('sam-sam') == 'samsam' + + +class TestScaffoldUtils(unittest.TestCase): + + def setUp(self): + self.scaffold_destination = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.scaffold_destination) + + def test_copy_dir(self): + from pecan.scaffolds import PecanScaffold + + class SimpleScaffold(PecanScaffold): + _scaffold_dir = ('pecan', os.path.join( + 'tests', 'scaffold_fixtures', 'simple' + )) + + SimpleScaffold().copy_to(os.path.join( + self.scaffold_destination, + 'someapp' + )) + + assert os.path.isfile(os.path.join( + self.scaffold_destination, 'someapp', 'foo' + )) + assert os.path.isfile(os.path.join( + self.scaffold_destination, 'someapp', 'bar', 'spam.txt' + )) + assert open(os.path.join( + self.scaffold_destination, 'someapp', 'foo' + ), 'r').read().strip() == 'YAR' + assert open(os.path.join( + self.scaffold_destination, 'someapp', 'foo' + ), 'r').read().strip() == 'YAR' + + def test_destination_directory_levels_deep(self): + from pecan.scaffolds import copy_dir + from cStringIO import StringIO + f = StringIO() + copy_dir(('pecan', os.path.join( + 'tests', 'scaffold_fixtures', 'simple' + )), + os.path.join(self.scaffold_destination, 'some', 'app'), + {}, + out_=f + ) + + assert os.path.isfile(os.path.join( + self.scaffold_destination, 'some', 'app', 'foo') + ) + assert os.path.isfile(os.path.join( + self.scaffold_destination, 'some', 'app', 'bar', 'spam.txt') + ) + assert open(os.path.join( + self.scaffold_destination, 'some', 'app', 'foo' + ), 'r').read().strip() == 'YAR' + assert open(os.path.join( + self.scaffold_destination, 'some', 'app', 'bar', 'spam.txt' + ), 'r').read().strip() == 'Pecan' + + def test_destination_directory_already_exists(self): + from pecan.scaffolds import copy_dir + from cStringIO import StringIO + f = StringIO() + copy_dir(('pecan', os.path.join( + 'tests', 'scaffold_fixtures', 'simple' + )), + os.path.join(self.scaffold_destination), + {}, + out_=f + ) + assert 'already exists' in f.getvalue() + + def test_copy_dir_with_filename_substitution(self): + from pecan.scaffolds import copy_dir + copy_dir(('pecan', os.path.join( + 'tests', 'scaffold_fixtures', 'file_sub' + )), + os.path.join( + self.scaffold_destination, 'someapp' + ), + {'package': 'thingy'} + ) + + assert os.path.isfile(os.path.join( + self.scaffold_destination, 'someapp', 'foo_thingy') + ) + assert os.path.isfile(os.path.join( + self.scaffold_destination, 'someapp', 'bar_thingy', 'spam.txt') + ) + assert open(os.path.join( + self.scaffold_destination, 'someapp', 'foo_thingy' + ), 'r').read().strip() == 'YAR' + assert open(os.path.join( + self.scaffold_destination, 'someapp', 'bar_thingy', 'spam.txt' + ), 'r').read().strip() == 'Pecan' + + def test_copy_dir_with_file_content_substitution(self): + from pecan.scaffolds import copy_dir + copy_dir(('pecan', os.path.join( + 'tests', 'scaffold_fixtures', 'content_sub' + )), + os.path.join( + self.scaffold_destination, 'someapp' + ), + {'package': 'thingy'} + ) + + assert os.path.isfile(os.path.join( + self.scaffold_destination, 'someapp', 'foo') + ) + assert os.path.isfile(os.path.join( + self.scaffold_destination, 'someapp', 'bar', 'spam.txt') + ) + assert open(os.path.join( + self.scaffold_destination, 'someapp', 'foo' + ), 'r').read().strip() == 'YAR thingy' + assert open(os.path.join( + self.scaffold_destination, 'someapp', 'bar', 'spam.txt' + ), 'r').read().strip() == 'Pecan thingy' + + class TestTemplateBuilds(unittest.TestCase): """ Used to build and test the templated quickstart project(s). @@ -86,7 +223,7 @@ class TestTemplateBuilds(unittest.TestCase): @unittest.skipUnless(has_internet(), 'Internet connectivity unavailable.') @unittest.skipUnless( getattr(pecan, '__run_all_tests__', False) is True, - 'Skipping (really slow). To run, `$ python setup.py test --functional.`' + 'Skipping (slow). To run, `$ python setup.py test --functional.`' ) def test_project_pecan_serve_command(self): pecan_exe = os.path.join(self.install_dir, 'bin', 'pecan') @@ -113,10 +250,9 @@ class TestTemplateBuilds(unittest.TestCase): @unittest.skipUnless(has_internet(), 'Internet connectivity unavailable.') @unittest.skipUnless( getattr(pecan, '__run_all_tests__', False) is True, - 'Skipping (really slow). To run, `$ python setup.py test --functional.`' + 'Skipping (slow). To run, `$ python setup.py test --functional.`' ) def test_project_pecan_shell_command(self): - from pecan.testing import load_test_app pecan_exe = os.path.join(self.install_dir, 'bin', 'pecan') # Start the server @@ -148,7 +284,7 @@ class TestTemplateBuilds(unittest.TestCase): @unittest.skipUnless(has_internet(), 'Internet connectivity unavailable.') @unittest.skipUnless( getattr(pecan, '__run_all_tests__', False) is True, - 'Skipping (really slow). To run, `$ python setup.py test --functional.`' + 'Skipping (slow). To run, `$ python setup.py test --functional.`' ) def test_project_tests_command(self): py_exe = os.path.join(self.install_dir, 'bin', 'python') From cf573298e05ffe9f2302f0b8bf3763794af90e3d Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 15 Mar 2012 12:07:53 -0700 Subject: [PATCH 10/12] Rearranging some test fixtures. --- .../bad/importerror.py | 0 .../bad/module_and_underscore.py | 0 .../bad/syntaxerror.py | 0 .../config.py | 0 .../{test_config => config_fixtures}/empty.py | 0 .../foobar.py | 0 .../forcedict.py | 0 pecan/tests/test_conf.py | 29 ++++++++++--------- 8 files changed, 16 insertions(+), 13 deletions(-) rename pecan/tests/{test_config => config_fixtures}/bad/importerror.py (100%) rename pecan/tests/{test_config => config_fixtures}/bad/module_and_underscore.py (100%) rename pecan/tests/{test_config => config_fixtures}/bad/syntaxerror.py (100%) rename pecan/tests/{test_config => config_fixtures}/config.py (100%) rename pecan/tests/{test_config => config_fixtures}/empty.py (100%) rename pecan/tests/{test_config => config_fixtures}/foobar.py (100%) rename pecan/tests/{test_config => config_fixtures}/forcedict.py (100%) diff --git a/pecan/tests/test_config/bad/importerror.py b/pecan/tests/config_fixtures/bad/importerror.py similarity index 100% rename from pecan/tests/test_config/bad/importerror.py rename to pecan/tests/config_fixtures/bad/importerror.py diff --git a/pecan/tests/test_config/bad/module_and_underscore.py b/pecan/tests/config_fixtures/bad/module_and_underscore.py similarity index 100% rename from pecan/tests/test_config/bad/module_and_underscore.py rename to pecan/tests/config_fixtures/bad/module_and_underscore.py diff --git a/pecan/tests/test_config/bad/syntaxerror.py b/pecan/tests/config_fixtures/bad/syntaxerror.py similarity index 100% rename from pecan/tests/test_config/bad/syntaxerror.py rename to pecan/tests/config_fixtures/bad/syntaxerror.py diff --git a/pecan/tests/test_config/config.py b/pecan/tests/config_fixtures/config.py similarity index 100% rename from pecan/tests/test_config/config.py rename to pecan/tests/config_fixtures/config.py diff --git a/pecan/tests/test_config/empty.py b/pecan/tests/config_fixtures/empty.py similarity index 100% rename from pecan/tests/test_config/empty.py rename to pecan/tests/config_fixtures/empty.py diff --git a/pecan/tests/test_config/foobar.py b/pecan/tests/config_fixtures/foobar.py similarity index 100% rename from pecan/tests/test_config/foobar.py rename to pecan/tests/config_fixtures/foobar.py diff --git a/pecan/tests/test_config/forcedict.py b/pecan/tests/config_fixtures/forcedict.py similarity index 100% rename from pecan/tests/test_config/forcedict.py rename to pecan/tests/config_fixtures/forcedict.py diff --git a/pecan/tests/test_conf.py b/pecan/tests/test_conf.py index 459bd02..1a64905 100644 --- a/pecan/tests/test_conf.py +++ b/pecan/tests/test_conf.py @@ -20,7 +20,7 @@ class TestConf(TestCase): conf = configuration.initconf() conf.update(configuration.conf_from_file(os.path.join( __here__, - 'test_config/config.py' + 'config_fixtures/config.py' ))) self.assertEqual(conf.app.root, None) @@ -37,7 +37,7 @@ class TestConf(TestCase): conf = configuration.initconf() conf.update(configuration.conf_from_file(os.path.join( __here__, - 'test_config/empty.py' + 'config_fixtures/empty.py' ))) self.assertEqual(conf.app.root, None) @@ -53,7 +53,7 @@ class TestConf(TestCase): conf = configuration.initconf() conf.update(configuration.conf_from_file(os.path.join( __here__, - 'test_config/forcedict.py' + 'config_fixtures/forcedict.py' ))) self.assertEqual(conf.app.root, None) @@ -93,16 +93,16 @@ class TestConf(TestCase): def test_config_from_file(self): from pecan import configuration path = os.path.join( - os.path.dirname(__file__), 'test_config', 'config.py' + os.path.dirname(__file__), 'config_fixtures', 'config.py' ) - conf = configuration.conf_from_file(path) + configuration.conf_from_file(path) def test_config_illegal_ids(self): from pecan import configuration conf = configuration.Config({}) conf.update(configuration.conf_from_file(os.path.join( __here__, - 'test_config/bad/module_and_underscore.py' + 'config_fixtures/bad/module_and_underscore.py' ))) self.assertEqual([], list(conf)) @@ -112,7 +112,7 @@ class TestConf(TestCase): configuration.Config({}) self.assertRaises(IOError, configuration.conf_from_file, os.path.join( __here__, - 'test_config', + 'config_fixtures', *path )) @@ -123,7 +123,7 @@ class TestConf(TestCase): self.assertRaises(IOError, configuration.conf_from_file, os.path.join( __here__, - 'test_config', + 'config_fixtures', *path )) @@ -135,7 +135,7 @@ class TestConf(TestCase): self.assertRaises( SyntaxError, configuration.conf_from_file, - os.path.join(__here__, 'test_config', *path) + os.path.join(__here__, 'config_fixtures', *path) ) def test_config_with_bad_import(self): @@ -148,7 +148,7 @@ class TestConf(TestCase): configuration.conf_from_file, os.path.join( __here__, - 'test_config', + 'config_fixtures', *path ) ) @@ -238,7 +238,10 @@ class TestGlobalConfig(TestCase): def tearDown(self): from pecan import configuration - configuration.set_config(dict(configuration.initconf()), overwrite=True) + configuration.set_config( + dict(configuration.initconf()), + overwrite=True + ) def test_paint_from_dict(self): from pecan import configuration @@ -255,7 +258,7 @@ class TestGlobalConfig(TestCase): from pecan import configuration configuration.set_config(os.path.join( __here__, - 'test_config/foobar.py' + 'config_fixtures/foobar.py' )) assert dict(configuration._runtime_conf) != {'foo': 'bar'} assert configuration._runtime_conf.foo == 'bar' @@ -264,7 +267,7 @@ class TestGlobalConfig(TestCase): from pecan import configuration configuration.set_config(os.path.join( __here__, - 'test_config/foobar.py', + 'config_fixtures/foobar.py', ), overwrite=True ) From 2b34c0359334458382dba67bce624c23f07c7421 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 15 Mar 2012 13:10:57 -0700 Subject: [PATCH 11/12] Silencing warnings and stdout writes in our test suite. --- pecan/scaffolds/__init__.py | 4 ++-- pecan/tests/test_base.py | 28 +++++++++++++++++++--------- pecan/tests/test_rest.py | 29 +++++++++++++++++++---------- pecan/tests/test_scaffolds.py | 14 ++++++++++---- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/pecan/scaffolds/__init__.py b/pecan/scaffolds/__init__.py index 1da3a44..cd816da 100644 --- a/pecan/scaffolds/__init__.py +++ b/pecan/scaffolds/__init__.py @@ -31,10 +31,10 @@ class PecanScaffold(object): def normalize_pkg_name(self, dest): return _bad_chars_re.sub('', dest.lower()) - def copy_to(self, dest): + def copy_to(self, dest, **kw): output_dir = self.normalize_output_dir(dest) pkg_name = self.normalize_pkg_name(dest) - copy_dir(self._scaffold_dir, output_dir, {'package': pkg_name}) + copy_dir(self._scaffold_dir, output_dir, {'package': pkg_name}, **kw) class BaseScaffold(PecanScaffold): diff --git a/pecan/tests/test_base.py b/pecan/tests/test_base.py index 17cdbe3..7898342 100644 --- a/pecan/tests/test_base.py +++ b/pecan/tests/test_base.py @@ -1,11 +1,12 @@ import sys +import warnings from paste.translogger import TransLogger from webtest import TestApp if sys.version_info < (2, 7): import unittest2 as unittest else: - import unittest + import unittest # noqa from pecan import ( Pecan, expose, request, response, redirect, abort, make_app, @@ -172,9 +173,11 @@ class TestLookups(unittest.TestCase): def _lookup(self, someID): return 'Bad arg spec' - app = TestApp(Pecan(RootController())) - r = app.get('/foo/bar', expect_errors=True) - assert r.status_int == 404 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + app = TestApp(Pecan(RootController())) + r = app.get('/foo/bar', expect_errors=True) + assert r.status_int == 404 class TestControllerArguments(unittest.TestCase): @@ -409,7 +412,10 @@ class TestControllerArguments(unittest.TestCase): assert r.body == 'optional: 7' def test_optional_arg_with_multiple_url_encoded_dictionary_kwargs(self): - r = self.app_.post('/optional', {'id': 'Some%20Number', 'dummy': 'dummy'}) + r = self.app_.post('/optional', { + 'id': 'Some%20Number', + 'dummy': 'dummy' + }) assert r.status_int == 200 assert r.body == 'optional: Some%20Number' @@ -572,7 +578,9 @@ class TestControllerArguments(unittest.TestCase): assert r.body == 'variable_kwargs: dummy=dummy, id=2' def test_multiple_variable_kwargs_with_explicit_encoded_kwargs(self): - r = self.app_.get('/variable_kwargs?id=Two%21&dummy=This%20is%20a%20test') + r = self.app_.get( + '/variable_kwargs?id=Two%21&dummy=This%20is%20a%20test' + ) assert r.status_int == 200 assert r.body == 'variable_kwargs: dummy=This is a test, id=Two!' @@ -895,8 +903,10 @@ class TestFileTypeExtensions(unittest.TestCase): assert r.status_int == 200 assert r.body == '/' - r = app.get('/index.txt', expect_errors=True) - assert r.status_int == 404 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + r = app.get('/index.txt', expect_errors=True) + assert r.status_int == 404 class TestCanonicalRouting(unittest.TestCase): @@ -964,7 +974,7 @@ class TestCanonicalRouting(unittest.TestCase): def test_posts_fail(self): try: - r = self.app_.post('/sub', dict(foo=1)) + self.app_.post('/sub', dict(foo=1)) raise Exception("Post should fail") except Exception, e: assert isinstance(e, RuntimeError) diff --git a/pecan/tests/test_rest.py b/pecan/tests/test_rest.py index 86e56ea..4935c00 100644 --- a/pecan/tests/test_rest.py +++ b/pecan/tests/test_rest.py @@ -1,7 +1,8 @@ -from pecan import abort, expose, make_app, request, response +from pecan import abort, expose, make_app, response from pecan.rest import RestController from unittest import TestCase from webtest import TestApp +import warnings try: from simplejson import dumps, loads except: @@ -208,21 +209,27 @@ class TestRestController(TestCase): assert r.body == 'OPTIONS' # test the "other" custom action - r = app.request('/things/other', method='MISC', status=405) - assert r.status_int == 405 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + r = app.request('/things/other', method='MISC', status=405) + assert r.status_int == 405 # test the "other" custom action with the _method parameter r = app.post('/things/other', {'_method': 'MISC'}, status=405) assert r.status_int == 405 # test the "others" custom action - r = app.request('/things/others/', method='MISC') - assert r.status_int == 200 - assert r.body == 'OTHERS' + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + r = app.request('/things/others/', method='MISC') + assert r.status_int == 200 + assert r.body == 'OTHERS' # test the "others" custom action missing trailing slash - r = app.request('/things/others', method='MISC', status=302) - assert r.status_int == 302 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + r = app.request('/things/others', method='MISC', status=302) + assert r.status_int == 302 # test the "others" custom action with the _method parameter r = app.get('/things/others/?_method=MISC') @@ -609,8 +616,10 @@ class TestRestController(TestCase): assert r.status_int == 404 # test "RESET" custom action - r = app.request('/things', method='RESET', status=404) - assert r.status_int == 404 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + r = app.request('/things', method='RESET', status=404) + assert r.status_int == 404 def test_custom_delete(self): diff --git a/pecan/tests/test_scaffolds.py b/pecan/tests/test_scaffolds.py index aa2e7bb..5698de7 100644 --- a/pecan/tests/test_scaffolds.py +++ b/pecan/tests/test_scaffolds.py @@ -7,6 +7,7 @@ import pkg_resources import httplib import urllib2 import time +from cStringIO import StringIO import pecan if sys.version_info < (2, 7): @@ -44,9 +45,13 @@ class TestScaffoldUtils(unittest.TestCase): def setUp(self): self.scaffold_destination = tempfile.mkdtemp() + self.out = sys.stdout + + sys.stdout = StringIO() def tearDown(self): shutil.rmtree(self.scaffold_destination) + sys.stdout = self.out def test_copy_dir(self): from pecan.scaffolds import PecanScaffold @@ -59,7 +64,7 @@ class TestScaffoldUtils(unittest.TestCase): SimpleScaffold().copy_to(os.path.join( self.scaffold_destination, 'someapp' - )) + ), out_=StringIO()) assert os.path.isfile(os.path.join( self.scaffold_destination, 'someapp', 'foo' @@ -76,7 +81,6 @@ class TestScaffoldUtils(unittest.TestCase): def test_destination_directory_levels_deep(self): from pecan.scaffolds import copy_dir - from cStringIO import StringIO f = StringIO() copy_dir(('pecan', os.path.join( 'tests', 'scaffold_fixtures', 'simple' @@ -120,7 +124,8 @@ class TestScaffoldUtils(unittest.TestCase): os.path.join( self.scaffold_destination, 'someapp' ), - {'package': 'thingy'} + {'package': 'thingy'}, + out_=StringIO() ) assert os.path.isfile(os.path.join( @@ -144,7 +149,8 @@ class TestScaffoldUtils(unittest.TestCase): os.path.join( self.scaffold_destination, 'someapp' ), - {'package': 'thingy'} + {'package': 'thingy'}, + out_=StringIO() ) assert os.path.isfile(os.path.join( From 93f9352b8e2c9254b0b9a3f7a3a14b41d439ac01 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Thu, 15 Mar 2012 14:52:32 -0700 Subject: [PATCH 12/12] More test coverage. --- pecan/commands/base.py | 12 ++++---- pecan/commands/create.py | 4 +-- pecan/tests/test_commands.py | 53 ++++++++++++++++++++++++++++++++++++ setup.py | 8 +++--- 4 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 pecan/tests/test_commands.py diff --git a/pecan/commands/base.py b/pecan/commands/base.py index b997d9f..2bb2581 100644 --- a/pecan/commands/base.py +++ b/pecan/commands/base.py @@ -4,14 +4,13 @@ import argparse import logging import sys from warnings import warn -from pecan import load_app log = logging.getLogger(__name__) class HelpfulArgumentParser(argparse.ArgumentParser): - def error(self, message): + def error(self, message): # pragma: nocover """error(message: string) Prints a usage message incorporating the message to stderr and @@ -38,7 +37,7 @@ class CommandManager(object): try: cmd = ep.load() assert hasattr(cmd, 'run') - except Exception, e: + except Exception, e: # pragma: nocover warn("Unable to load plugin %s: %s" % (ep, e), RuntimeWarning) continue self.add({ep.name: cmd}) @@ -77,7 +76,7 @@ class CommandRunner(object): self.commands[ns.command_name]().run(ns) @classmethod - def handle_command_line(cls): + def handle_command_line(cls): # pragma: nocover runner = CommandRunner() runner.run(sys.argv[1:]) @@ -86,10 +85,10 @@ class CommandRunner(object): try: dist = pkg_resources.get_distribution('Pecan') if os.path.dirname(os.path.dirname(__file__)) == dist.location: - return dist.version + return dist.version # pragma: nocover else: return '(development)' - except: + except: # pragma: nocover return '(development)' @property @@ -114,6 +113,7 @@ class BaseCommand(object): self.args = args def load_app(self): + from pecan import load_app 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 508f766..df84f01 100644 --- a/pecan/commands/create.py +++ b/pecan/commands/create.py @@ -23,7 +23,7 @@ class ScaffoldManager(object): try: cmd = ep.load() assert hasattr(cmd, 'copy_to') - except Exception, e: + except Exception, e: # pragma: nocover warn( "Unable to load scaffold %s: %s" % (ep, e), RuntimeWarning ) @@ -55,6 +55,6 @@ class CreateCommand(BaseCommand): def run(self, args): super(CreateCommand, self).run(args) - CreateCommand.manager.scaffolds[args.template_name]().copy_to( + self.manager.scaffolds[args.template_name]().copy_to( args.project_name ) diff --git a/pecan/tests/test_commands.py b/pecan/tests/test_commands.py new file mode 100644 index 0000000..5d390d8 --- /dev/null +++ b/pecan/tests/test_commands.py @@ -0,0 +1,53 @@ +import unittest + + +class TestCommandManager(unittest.TestCase): + + def test_commands(self): + from pecan.commands import ServeCommand, ShellCommand, CreateCommand + from pecan.commands.base import CommandManager + m = CommandManager() + assert m.commands['serve'] == ServeCommand + assert m.commands['shell'] == ShellCommand + assert m.commands['create'] == CreateCommand + + +class TestCommandRunner(unittest.TestCase): + + def test_commands(self): + from pecan.commands import ( + ServeCommand, ShellCommand, CreateCommand, CommandRunner + ) + runner = CommandRunner() + assert runner.commands['serve'] == ServeCommand + assert runner.commands['shell'] == ShellCommand + assert runner.commands['create'] == CreateCommand + + def test_run(self): + from pecan.commands import CommandRunner + runner = CommandRunner() + with self.assertRaises(RuntimeError): + runner.run(['serve', 'missing_file.py']) + + +class TestCreateCommand(unittest.TestCase): + + def test_run(self): + from pecan.commands import CreateCommand + + class FakeArg(object): + project_name = 'default' + template_name = 'default' + + class FakeScaffold(object): + def copy_to(self, project_name): + assert project_name == 'default' + + class FakeManager(object): + scaffolds = { + 'default': FakeScaffold + } + + c = CreateCommand() + c.manager = FakeManager() + c.run(FakeArg()) diff --git a/setup.py b/setup.py index f6141b0..6e6f2d0 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ import sys -from setuptools import setup, command, find_packages +from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand version = '0.1.0' @@ -17,15 +17,15 @@ requirements = [ ] try: - import json + import json # noqa except: try: - import simplejson + import simplejson # noqa except: requirements.append("simplejson >= 2.1.1") try: - import argparse + import argparse # noqa except: requirements.append('argparse')