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']