Commits

James Palmer committed 7d344df

More refactoring. Template listing and creation works.

Comments (0)

Files changed (27)

docs/modules/checkperms.txt

-:mod:`ignite.script.checkperms` -- functions to check and diff file permissions
-==============================================================================
-
-.. automodule:: ignite.script.checkperms
-
-.. contents::
-
-Module Contents
----------------
-
-Permissions
-~~~~~~~~~~~
-
-.. autofunction:: read_perm_spec
-.. autofunction:: mode_diff
-.. autofunction:: calc_mode_diff
-.. autofunction:: calc_set_mode
-.. autofunction:: set_mode
-
-Ownership
-~~~~~~~~~
-
-.. autofunction:: calc_ownership_spec
-.. autofunction:: ownership_diff
-.. autofunction:: set_ownership
-
-Models
-~~~~~~
-
-.. autoclass:: PermissionSpec

docs/modules/filemaker.txt

-:mod:`ignite.script.filemaker` -- interactive file writing helpers
-=================================================================
-
-.. automodule:: ignite.script.filemaker
-
-Module Contents
----------------
-
-.. autoclass:: FileOp

ignite/BasicTemplate.py

+from core import Template
+from core import var
+
+class BasicTemplate(Template):
+
+    name = 'basic'
+    _template_dir = 'templates/basic'
+    summary = "A basic setuptools-enabled package"
+    variables = [
+        var('version', 'Version (like 0.1)'),
+        var('description', 'One-line description of the package'),
+        var('long_description', 'Multi-line description (in reST)'),
+        var('keywords', 'Space-separated keywords/tags'),
+        var('author', 'Author name'),
+        var('author_email', 'Author email'),
+        var('url', 'URL of homepage'),
+        var('license_name', 'License name'),
+        var('zip_safe',
+            'True/False: if the package can be distributed as a .zip file',
+            default=False),
+        ]

ignite/__init__.py

 else:
     for p in __path__:
         modulefinder.AddPackagePath(__name__, p)
+
+from core import Template

ignite/checkperms.py

-# (c) 2005 Ian Bicking and contributors; written for Paste
-# (http://pythonpaste.org)
-#
-# Licensed under the MIT license:
-# http://www.opensource.org/licenses/mit-license.php
-"""
-This is a module to check the filesystem for the presence and
-permissions of certain files.  It can also be used to correct the
-permissions (but not existance) of those files.
-
-Currently only supports Posix systems (with Posixy permissions).
-Permission stuff can probably be stubbed out later.
-"""
-import os
-
-
-def read_perm_spec(spec):
-    """
-    Reads a spec like 'rw-r--r--' into a octal number suitable for
-    chmod.  That is characters in groups of three -- first group is
-    user, second for group, third for other (all other people).  The
-    characters are r (read), w (write), and x (executable), though the
-    executable can also be s (sticky).  Files in sticky directories
-    get the directories permission setting.
-
-    Examples::
-
-      >>> print oct(read_perm_spec('rw-r--r--'))
-      0644
-      >>> print oct(read_perm_spec('rw-rwsr--'))
-      02664
-      >>> print oct(read_perm_spec('r-xr--r--'))
-      0544
-      >>> print oct(read_perm_spec('r--------'))
-      0400
-    """
-    total_mask = 0
-    # suid/sgid modes give this mask in user, group, other mode:
-    set_bits = (04000, 02000, 0)
-    pieces = (spec[0:3], spec[3:6], spec[6:9])
-    for i, (mode, set_bit) in enumerate(zip(pieces, set_bits)):
-        mask = 0
-        read, write, exe = list(mode)
-        if read == 'r':
-            mask = mask | 4
-        elif read != '-':
-            raise ValueError(
-                "Character %r unexpected (should be '-' or 'r')"
-                % read)
-        if write == 'w':
-            mask = mask | 2
-        elif write != '-':
-            raise ValueError(
-                "Character %r unexpected (should be '-' or 'w')"
-                % write)
-        if exe == 'x':
-            mask = mask | 1
-        elif exe not in ('s', '-'):
-            raise ValueError(
-                "Character %r unexpected (should be '-', 'x', or 's')"
-                % exe)
-        if exe == 's' and i == 2:
-            raise ValueError(
-                "The 'other' executable setting cannot be suid/sgid ('s')")
-        mask = mask << ((2 - i) * 3)
-        if exe == 's':
-            mask = mask | set_bit
-        total_mask = total_mask | mask
-    return total_mask
-
-modes = [
-    (04000, 'setuid bit',
-     'setuid bit: make contents owned by directory owner'),
-    (02000, 'setgid bit',
-     'setgid bit: make contents inherit permissions from directory'),
-    (01000, 'sticky bit',
-     'sticky bit: append-only directory'),
-    (00400, 'read by owner', 'read by owner'),
-    (00200, 'write by owner', 'write by owner'),
-    (00100, 'execute by owner', 'owner can search directory'),
-    (00040, 'allow read by group members',
-     'allow read by group members',),
-    (00020, 'allow write by group members',
-     'allow write by group members'),
-    (00010, 'execute by group members',
-     'group members can search directory'),
-    (00004, 'read by others', 'read by others'),
-    (00002, 'write by others', 'write by others'),
-    (00001, 'execution by others', 'others can search directory'),
-    ]
-
-exe_bits = [0100, 0010, 0001]
-exe_mask = 0111
-full_mask = 07777
-
-
-def mode_diff(filename, mode, **kw):
-    """
-    Returns the differences calculated using ``calc_mode_diff``
-    """
-    cur_mode = os.stat(filename).st_mode
-    return calc_mode_diff(cur_mode, mode, **kw)
-
-
-def calc_mode_diff(cur_mode, mode, keep_exe=True,
-                   not_set='not set: ',
-                   set='set: '):
-    """
-    Gives the difference between the actual mode of the file and the
-    given mode.  If ``keep_exe`` is true, then if the mode doesn't
-    include any executable information the executable information will
-    simply be ignored.  High bits are also always ignored (except
-    suid/sgid and sticky bit).
-
-    Returns a list of differences (empty list if no differences)
-    """
-    for exe_bit in exe_bits:
-        if mode & exe_bit:
-            keep_exe = False
-    diffs = []
-    isdir = os.path.isdir(filename)
-    for bit, file_desc, dir_desc in modes:
-        if keep_exe and bit in exe_bits:
-            continue
-        if isdir:
-            desc = dir_desc
-        else:
-            desc = file_desc
-        if (mode & bit) and not (cur_mode & bit):
-            diffs.append(not_set + desc)
-        if not (mode & bit) and (cur_mode & bit):
-            diffs.append(set + desc)
-    return diffs
-
-
-def calc_set_mode(cur_mode, mode, keep_exe=True):
-    """
-    Calculates the new mode given the current node ``cur_mode`` and
-    the mode spec ``mode`` and if ``keep_exe`` is true then also keep
-    the executable bits in ``cur_mode`` if ``mode`` has no executable
-    bits in it.  Return the new mode.
-
-    Examples::
-
-      >>> print oct(calc_set_mode(0775, 0644))
-      0755
-      >>> print oct(calc_set_mode(0775, 0744))
-      0744
-      >>> print oct(calc_set_mode(010600, 0644))
-      010644
-      >>> print oct(calc_set_mode(0775, 0644, False))
-      0644
-    """
-    for exe_bit in exe_bits:
-        if mode & exe_bit:
-            keep_exe = False
-    # This zeros-out full_mask parts of the current mode:
-    keep_parts = (cur_mode | full_mask) ^ full_mask
-    if keep_exe:
-        keep_parts = keep_parts | (cur_mode & exe_mask)
-    new_mode = keep_parts | mode
-    return new_mode
-
-
-def set_mode(filename, mode, **kw):
-    """
-    Sets the mode on ``filename`` using ``calc_set_mode``
-    """
-    cur_mode = os.stat(filename).st_mode
-    new_mode = calc_set_mode(cur_mode, mode, **kw)
-    os.chmod(filename, new_mode)
-
-
-def calc_ownership_spec(spec):
-    """
-    Calculates what a string spec means, returning (uid, username,
-    gid, groupname), where there can be None values meaning no
-    preference.
-
-    The spec is a string like ``owner:group``.  It may use numbers
-    instead of user/group names.  It may leave out ``:group``.  It may
-    use '-' to mean any-user/any-group.
-
-    """
-    import grp
-    import pwd
-    user = group = None
-    uid = gid = None
-    if ':' in spec:
-        user_spec, group_spec = spec.split(':', 1)
-    else:
-        user_spec, group_spec = spec, '-'
-    if user_spec == '-':
-        user_spec = '0'
-    if group_spec == '-':
-        group_spec = '0'
-    try:
-        uid = int(user_spec)
-    except ValueError:
-        uid = pwd.getpwnam(user_spec)
-        user = user_spec
-    else:
-        if not uid:
-            uid = user = None
-        else:
-            user = pwd.getpwuid(uid).pw_name
-    try:
-        gid = int(group_spec)
-    except ValueError:
-        gid = grp.getgrnam(group_spec)
-        group = group_spec
-    else:
-        if not gid:
-            gid = group = None
-        else:
-            group = grp.getgrgid(gid).gr_name
-    return (uid, user, gid, group)
-
-
-def ownership_diff(filename, spec):
-    """
-    Return a list of differences between the ownership of ``filename``
-    and the spec given.
-    """
-    import grp
-    import pwd
-    diffs = []
-    uid, user, gid, group = calc_ownership_spec(spec)
-    st = os.stat(filename)
-    if uid and uid != st.st_uid:
-        diffs.append('owned by %s (should be %s)' %
-                     (pwd.getpwuid(st.st_uid).pw_name, user))
-    if gid and gid != st.st_gid:
-        diffs.append('group %s (should be %s)' %
-                     (grp.getgrgid(st.st_gid).gr_name, group))
-    return diffs
-
-
-def set_ownership(filename, spec):
-    """
-    Set the ownership of ``filename`` given the spec.
-    """
-    uid, user, gid, group = calc_ownership_spec(spec)
-    st = os.stat(filename)
-    if not uid:
-        uid = st.st_uid
-    if not gid:
-        gid = st.st_gid
-    os.chmod(filename, uid, gid)
-
-
-class PermissionSpec(object):
-    """
-    Represents a set of specifications for permissions.
-
-    Typically reads from a file that looks like this::
-
-      rwxrwxrwx user:group filename
-
-    If the filename ends in /, then it expected to be a directory, and
-    the directory is made executable automatically, and the contents
-    of the directory are given the same permission (recursively).  By
-    default the executable bit on files is left as-is, unless the
-    permissions specifically say it should be on in some way.
-
-    You can use 'nomodify filename' for permissions to say that any
-    permission is okay, and permissions should not be changed.
-
-    Use 'noexist filename' to say that a specific file should not
-    exist.
-
-    Use 'symlink filename symlinked_to' to assert a symlink destination
-
-    The entire file is read, and most specific rules are used for each
-    file (i.e., a rule for a subdirectory overrides the rule for a
-    superdirectory).  Order does not matter.
-    """
-
-    def __init__(self):
-        self.paths = {}
-
-    def parsefile(self, filename):
-        f = open(filename)
-        lines = f.readlines()
-        f.close()
-        self.parselines(lines, filename=filename)
-
-    commands = {}
-
-    def parselines(self, lines, filename=None):
-        for lineindex, line in enumerate(lines):
-            line = line.strip()
-            if not line or line.startswith('#'):
-                continue
-            parts = line.split()
-            command = parts[0]
-            if command in self.commands:
-                cmd = self.commands[command](*parts[1:])
-            else:
-                cmd = self.commands['*'](*parts)
-            self.paths[cmd.path] = cmd
-
-    def check(self):
-        action = _Check(self)
-        self.traverse(action)
-
-    def fix(self):
-        action = _Fixer(self)
-        self.traverse(action)
-
-    def traverse(self, action):
-        paths = self.paths_sorted()
-        checked = {}
-        for path, checker in list(paths)[::-1]:
-            self.check_tree(action, path, paths, checked)
-        for path, checker in paths:
-            if path not in checked:
-                action.noexists(path, checker)
-
-    def traverse_tree(self, action, path, paths, checked):
-        if path in checked:
-            return
-        self.traverse_path(action, path, paths, checked)
-        if os.path.isdir(path):
-            for fn in os.listdir(path):
-                fn = os.path.join(path, fn)
-                self.traverse_tree(action, fn, paths, checked)
-
-    def traverse_path(self, action, path, paths, checked):
-        checked[path] = None
-        for check_path, checker in paths:
-            if path.startswith(check_path):
-                action.check(check_path, checker)
-                if not checker.inherit:
-                    break
-
-    def paths_sorted(self):
-        paths = self.paths.items()
-        paths.sort(lambda a, b: -cmp(len(a[0]), len(b[0])))
-
-
-class _Rule(object):
-    class __metaclass__(type):
-        def __new__(meta, class_name, bases, d):
-            cls = type.__new__(meta, class_name, bases, d)
-            PermissionSpec.commands[cls.__name__] = cls
-            return cls
-
-    inherit = False
-
-    def noexists(self):
-        return ['Path %s does not exist' % path]
-
-
-class _NoModify(_Rule):
-
-    name = 'nomodify'
-
-    def __init__(self, path):
-        self.path = path
-
-    def fix(self, path):
-        pass
-
-
-class _NoExist(_Rule):
-
-    name = 'noexist'
-
-    def __init__(self, path):
-        self.path = path
-
-    def check(self, path):
-        return ['Path %s should not exist' % path]
-
-    def noexists(self, path):
-        return []
-
-    def fix(self, path):
-        # @@: Should delete?
-        pass
-
-
-class _SymLink(_Rule):
-
-    name = 'symlink'
-    inherit = True
-
-    def __init__(self, path, dest):
-        self.path = path
-        self.dest = dest
-
-    def check(self, path):
-        assert path == self.path, (
-            "_Symlink should only be passed specific path %s (not %s)"
-            % (self.path, path))
-        try:
-            link = os.path.readlink(path)
-        except OSError:
-            if e.errno != 22:
-                raise
-            return ['Path %s is not a symlink (should point to %s)'
-                    % (path, self.dest)]
-        if link != self.dest:
-            return ['Path %s should symlink to %s, not %s'
-                    % (path, self.dest, link)]
-        return []
-
-    def fix(self, path):
-        assert path == self.path, (
-            "_Symlink should only be passed specific path %s (not %s)"
-            % (self.path, path))
-        if not os.path.exists(path):
-            os.symlink(path, self.dest)
-        else:
-            # @@: This should correct the symlink or something:
-            print 'Not symlinking %s' % path
-
-
-class _Permission(_Rule):
-
-    name = '*'
-
-    def __init__(self, perm, owner, dir):
-        self.perm_spec = read_perm_spec(perm)
-        self.owner = owner
-        self.dir = dir
-
-    def check(self, path):
-        return mode_diff(path, self.perm_spec)
-
-    def fix(self, path):
-        set_mode(path, self.perm_spec)
-
-
-class _Strategy(object):
-
-    def __init__(self, spec):
-        self.spec = spec
-
-
-class _Check(_Strategy):
-
-    def noexists(self, path, checker):
-        checker.noexists(path)
-
-    def check(self, path, checker):
-        checker.check(path)
-
-
-class _Fixer(_Strategy):
-
-    def noexists(self, path, checker):
-        pass
-
-    def check(self, path, checker):
-        checker.fix(path)
-
-
-if __name__ == '__main__':
-    import doctest
-    doctest.testmod()

ignite/command.py

-# (c) 2005 Ian Bicking and contributors; written for Paste
-# (http://pythonpaste.org)
-#
-# Licensed under the MIT license:
-# http://www.opensource.org/licenses/mit-license.php
-import pkg_resources
-import sys
-from optparse2 import OptionParser
-import os
-import re
-import textwrap
-import pluginlib
-import ConfigParser
-import getpass
-
-try:
-    import subprocess
-except ImportError:
-    # jython
-    subprocess = None
-
-difflib = None
-
-
-class BadCommand(Exception):
-
-    def __init__(self, message, exit_code=2):
-        self.message = message
-        self.exit_code = exit_code
-        Exception.__init__(self, message)
-
-    def _get_message(self):
-        """Getter for 'message'; needed only to override deprecation
-        in BaseException."""
-        return self.__message
-
-    def _set_message(self, value):
-        """Setter for 'message'; needed only to override deprecation
-        in BaseException."""
-        self.__message = value
-
-    # BaseException.message has been deprecated since Python 2.6.
-    # To prevent DeprecationWarning from popping up over this
-    # pre-existing attribute, use a new property that takes lookup
-    # precedence.
-    message = property(_get_message, _set_message)
-
-
-class NoDefault(object):
-    pass
-
-dist = pkg_resources.get_distribution('Ignite')
-
-python_version = sys.version.splitlines()[0].strip()
-
-parser = OptionParser(
-    add_help_option=False,
-    version='%s from %s (python %s)'
-    % (dist, dist.location, python_version),
-    usage='%prog [ignite_options] COMMAND [command_options]')
-
-parser.add_option(
-    '--plugin',
-    action='append',
-    dest='plugins',
-    help="Add a plugin to the list of commands (plugins are Egg specs; " + \
-        "will also require() the Egg)")
-parser.add_option(
-    '-h', '--help',
-    action='store_true',
-    dest='do_help',
-    help="Show this help message")
-parser.disable_interspersed_args()
-
-# @@: Add an option to run this in another Python interpreter
-
-system_plugins = []
-
-
-def run(args=None):
-    if (not args and
-        len(sys.argv) >= 2
-        and os.environ.get('_') and sys.argv[0] != os.environ['_']
-        and os.environ['_'] == sys.argv[1]):
-        # probably it's an exe execution
-        args = ['exe', os.environ['_']] + sys.argv[2:]
-    if args is None:
-        args = sys.argv[1:]
-    options, args = parser.parse_args(args)
-    options.base_parser = parser
-    system_plugins.extend(options.plugins or [])
-    commands = get_commands()
-    if options.do_help:
-        args = ['help'] + args
-    if not args:
-        print 'Usage: %s COMMAND' % sys.argv[0]
-        args = ['help']
-
-    command_name = args[0]
-    if command_name not in commands:
-        print ('Command %r not known (you may need to run setup.py egg_info)'
-               % command_name)
-
-        commands = get_commands().items()
-        if not commands:
-            print 'No commands registered.'
-            print 'Have you installed Ignite?'
-            print '(try running python setup.py develop)'
-            sys.exit(2)
-
-        print 'Known commands:'
-        commands.sort()
-        longest = max([len(n) for n, c in commands])
-        for name, command in commands:
-            print '  %s  %s' % (self.pad(name, length=longest),
-                                command.load().summary)
-
-        sys.exit(2)
-
-    command_class = commands[command_name].load()
-
-    try:
-        command = command_class()
-        exit_code = command.run(args)
-    except BadCommand, e:
-        print e.message
-        exit_code = e.exit_code
-
-    sys.exit(exit_code)
-
-
-def parse_exe_file(config):
-    import shlex
-    p = ConfigParser.RawConfigParser()
-    p.read([config])
-    command_name = 'exe'
-    options = []
-    if p.has_option('exe', 'command'):
-        command_name = p.get('exe', 'command')
-    if p.has_option('exe', 'options'):
-        options = shlex.split(p.get('exe', 'options'))
-    if p.has_option('exe', 'sys.path'):
-        paths = shlex.split(p.get('exe', 'sys.path'))
-        paths = [os.path.abspath(os.path.join(os.path.dirname(config), p))
-                 for p in paths]
-        for path in paths:
-            pkg_resources.working_set.add_entry(path)
-            sys.path.insert(0, path)
-    args = [command_name, config] + options
-    return args
-
-
-def get_commands():
-    plugins = system_plugins[:]
-    egg_info_dir = pluginlib.find_egg_info_dir(os.getcwd())
-    if egg_info_dir:
-        plugins.append(os.path.splitext(os.path.basename(egg_info_dir))[0])
-        base_dir = os.path.dirname(egg_info_dir)
-        if base_dir not in sys.path:
-            sys.path.insert(0, base_dir)
-            pkg_resources.working_set.add_entry(base_dir)
-    # REFACTOR resolve_plugins
-    plugins = pluginlib.resolve_plugins(plugins)
-    # REFACTOR load_commands_from_plugin called once
-    commands = pluginlib.load_commands_from_plugins(plugins)
-    # REFACTOR load_global_commands called once
-    commands.update(pluginlib.load_global_commands())
-
-    # Load local stuff..
-    # STEP 1. GO UP PARENT CHAIN LOOKING FOR "IgniteFile"
-    # STEP 2. GET THE libs DIRECTORY from THAT DIR
-    # STEP 3. INSIDE EACH SUBDIR LOOK FOR ignite/__init__.py
-    # STEP 4. LOOK FOR CLASSES THAT GOT PUBLISHED IN INIT..
-    # STEP 5. ITERATE OVER CLASSES AND ADD TO COMMANDS
-
-    return commands
-
-
-class Command(object):
-
-    command_name = ''
-    description = None
-    usage = None
-
-    # Subclasses must implement these:
-    #   parser
-    #   summary
-    #   command()
-
-    def run(self, args):
-        self.parse_args(args)
-
-        # Setup defaults:
-        for name, default in [('verbose', 0),
-                              ('quiet', 0),
-                              ('interactive', False),
-                              ('overwrite', False)]:
-            if not hasattr(self.options, name):
-                setattr(self.options, name, default)
-        if getattr(self.options, 'simulate', False):
-            self.options.verbose = max(self.options.verbose, 1)
-
-        self.interactive = 0
-        if getattr(self.options, 'interactive', False):
-            self.interactive += self.options.interactive
-        if getattr(self.options, 'no_interactive', False):
-            self.interactive = False
-
-        self.verbose = 0
-        self.verbose += self.options.verbose
-        self.verbose -= self.options.quiet
-        self.simulate = getattr(self.options, 'simulate', False)
-
-        if (os.environ.get('IGNITE_DEFAULT_QUIET')):
-            self.verbose = 0
-
-        result = self.command()
-        if result is None:
-            return 0
-        else:
-            return result
-
-    def parse_args(self, args):
-        if self.usage:
-            usage = ' ' + self.usage
-        else:
-            usage = ''
-        self.parser.usage = "%%prog [options]%s\n%s" % (
-            usage, self.summary)
-        self.parser.prog = self._prog_name()
-        if self.description:
-            desc = self.description
-            desc = textwrap.dedent(desc)
-            self.parser.description = desc
-        self.options, self.args = self.parser.parse_args(args)
-
-    def _prog_name(self):
-        return '%s %s' % (os.path.basename(sys.argv[0]), self.__class__.command_name)
-
-    ########################################
-    ## Utility methods
-    ########################################
-
-    def here(cls):
-        mod = sys.modules[cls.__module__]
-        return os.path.dirname(mod.__file__)
-
-    here = classmethod(here)
-
-    def ask(self, prompt, safe=False, default=True):
-        """
-        Prompt the user.  Default can be true, false, ``'careful'`` or
-        ``'none'``.  If ``'none'`` then the user must enter y/n.  If
-        ``'careful'`` then the user must enter yes/no (long form).
-
-        If the interactive option is over two (``-ii``) then ``safe``
-        will be used as a default.  This option should be the
-        do-nothing option.
-        """
-        # @@: Should careful be a separate argument?
-
-        if self.options.interactive >= 2:
-            default = safe
-        if default == 'careful':
-            prompt += ' [yes/no]?'
-        elif default == 'none':
-            prompt += ' [y/n]?'
-        elif default:
-            prompt += ' [Y/n]? '
-        else:
-            prompt += ' [y/N]? '
-        while 1:
-            response = raw_input(prompt).strip().lower()
-            if not response:
-                if default in ('careful', 'none'):
-                    print 'Please enter yes or no'
-                    continue
-                return default
-            if default == 'careful':
-                if response in ('yes', 'no'):
-                    return response == 'yes'
-                print 'Please enter "yes" or "no"'
-                continue
-            if response[0].lower() in ('y', 'n'):
-                return response[0].lower() == 'y'
-            print 'Y or N please'
-
-    def challenge(self, prompt, default=NoDefault, should_echo=True):
-        """
-        Prompt the user for a variable.
-        """
-        if default is not NoDefault:
-            prompt += ' [%r]' % default
-        prompt += ': '
-        while 1:
-            if should_echo:
-                prompt_method = raw_input
-            else:
-                prompt_method = getpass.getpass
-            response = prompt_method(prompt).strip()
-            if not response:
-                if default is not NoDefault:
-                    return default
-                else:
-                    continue
-            else:
-                return response
-
-    def pad(self, s, length, dir='left'):
-        if len(s) >= length:
-            return s
-        if dir == 'left':
-            return s + ' ' * (length - len(s))
-        else:
-            return ' ' * (length - len(s)) + s
-
-    def standard_parser(cls, verbose=True,
-                        interactive=False,
-                        no_interactive=False,
-                        simulate=False,
-                        quiet=False,
-                        overwrite=False):
-        """
-        Create a standard ``OptionParser`` instance.
-
-        Typically used like::
-
-            class MyCommand(Command):
-                parser = Command.standard_parser()
-
-        Subclasses may redefine ``standard_parser``, so use the
-        nearest superclass's class method.
-        """
-        parser = OptionParser()
-        if verbose:
-            parser.add_option('-v', '--verbose',
-                              action='count',
-                              dest='verbose',
-                              default=0)
-        if quiet:
-            parser.add_option('-q', '--quiet',
-                              action='count',
-                              dest='quiet',
-                              default=0)
-        if no_interactive:
-            parser.add_option('--no-interactive',
-                              action="count",
-                              dest="no_interactive",
-                              default=0)
-        if interactive:
-            parser.add_option('-i', '--interactive',
-                              action='count',
-                              dest='interactive',
-                              default=0)
-        if simulate:
-            parser.add_option('-n', '--simulate',
-                              action='store_true',
-                              dest='simulate',
-                              default=False)
-        if overwrite:
-            parser.add_option('-f', '--overwrite',
-                              dest="overwrite",
-                              action="store_true",
-                              help="Overwrite files (warnings will be " + \
-                                  "emitted for non-matching files otherwise)")
-        return parser
-
-    standard_parser = classmethod(standard_parser)
-
-    def shorten(self, fn, *paths):
-        """
-        Return a shorted form of the filename (relative to the current
-        directory), typically for displaying in messages.  If
-        ``*paths`` are present, then use os.path.join to create the
-        full filename before shortening.
-        """
-        if paths:
-            fn = os.path.join(fn, *paths)
-        if fn.startswith(os.getcwd()):
-            return fn[len(os.getcwd()):].lstrip(os.path.sep)
-        else:
-            return fn
-
-    def ensure_dir(self, dir, svn_add=True):
-        """
-        Ensure that the directory exists, creating it if necessary.
-        Respects verbosity and simulation.
-
-        Adds directory to subversion if ``.svn/`` directory exists in
-        parent, and directory was created.
-        """
-        dir = dir.rstrip(os.sep)
-        if not dir:
-            # we either reached the parent-most directory, or we got
-            # a relative directory
-            # @@: Should we make sure we resolve relative directories
-            # first?  Though presumably the current directory always
-            # exists.
-            return
-        if not os.path.exists(dir):
-            self.ensure_dir(os.path.dirname(dir))
-            if self.verbose:
-                print 'Creating %s' % self.shorten(dir)
-            if not self.simulate:
-                os.mkdir(dir)
-            if (svn_add and
-                os.path.exists(os.path.join(os.path.dirname(dir), '.svn'))):
-                self.svn_command('add', dir)
-        else:
-            if self.verbose > 1:
-                print "Directory already exists: %s" % self.shorten(dir)
-
-    def ensure_file(self, filename, content, svn_add=True):
-        """
-        Ensure a file named ``filename`` exists with the given
-        content.  If ``--interactive`` has been enabled, this will ask
-        the user what to do if a file exists with different content.
-        """
-        global difflib
-        assert content is not None, (
-            "You cannot pass a content of None")
-        self.ensure_dir(os.path.dirname(filename), svn_add=svn_add)
-        if not os.path.exists(filename):
-            if self.verbose:
-                print 'Creating %s' % filename
-            if not self.simulate:
-                f = open(filename, 'wb')
-                f.write(content)
-                f.close()
-            if svn_add and os.path.exists(os.path.join(
-                    os.path.dirname(filename), '.svn')):
-                self.svn_command('add', filename,
-                                 warn_returncode=True)
-            return
-        f = open(filename, 'rb')
-        old_content = f.read()
-        f.close()
-        if content == old_content:
-            if self.verbose > 1:
-                print 'File %s matches expected content' % filename
-            return
-        if not self.options.overwrite:
-            print 'Warning: file %s does not match expected content' % filename
-            if difflib is None:
-                import difflib
-            diff = difflib.context_diff(
-                content.splitlines(),
-                old_content.splitlines(),
-                'expected ' + filename,
-                filename)
-            print '\n'.join(diff)
-            if self.interactive:
-                while 1:
-                    s = raw_input(
-                        'Overwrite file with new content? [y/N]').lower()
-                    if not s:
-                        s = 'n'
-                    if s.startswith('y'):
-                        break
-                    if s.startswith('n'):
-                        return
-                    print 'Unknown response; Y or N please'
-            else:
-                return
-
-        if self.verbose:
-            print 'Overwriting %s with new content' % filename
-        if not self.simulate:
-            f = open(filename, 'wb')
-            f.write(content)
-            f.close()
-
-    def insert_into_file(self, filename, marker_name, text,
-                         indent=False):
-        """
-        Inserts ``text`` into the file, right after the given marker.
-        Markers look like: ``-*- <marker_name>[:]? -*-``, and the text
-        will go on the immediately following line.
-
-        Raises ``ValueError`` if the marker is not found.
-
-        If ``indent`` is true, then the text will be indented at the
-        same level as the marker.
-        """
-        if not text.endswith('\n'):
-            raise ValueError(
-                "The text must end with a newline: %r" % text)
-        if not os.path.exists(filename) and self.simulate:
-            # If we are doing a simulation, it's expected that some
-            # files won't exist...
-            if self.verbose:
-                print 'Would (if not simulating) insert text into %s' % (
-                    self.shorten(filename))
-            return
-
-        f = open(filename)
-        lines = f.readlines()
-        f.close()
-        regex = re.compile(r'-\*-\s+%s:?\s+-\*-' % re.escape(marker_name),
-                           re.I)
-        for i in range(len(lines)):
-            if regex.search(lines[i]):
-                # Found it!
-                if (lines[i:] and len(lines[i:]) > 1 and
-                    ''.join(lines[(i + 1):]).strip().startswith(text.strip())):
-                    # Already have it!
-                    print 'Warning: line already found in %s (not ' % filename
-                    print 'inserting %s' % lines[i]
-                    return
-
-                if indent:
-                    text = text.lstrip()
-                    match = re.search(r'^[ \t]*', lines[i])
-                    text = match.group(0) + text
-                lines[(i + 1):(i + 1)] = [text]
-                break
-        else:
-            errstr = (
-                "Marker '-*- %s -*-' not found in %s"
-                % (marker_name, filename))
-            if 1 or self.simulate:  # @@: being permissive right now
-                print 'Warning: %s' % errstr
-            else:
-                raise ValueError(errstr)
-        if self.verbose:
-            print 'Updating %s' % self.shorten(filename)
-        if not self.simulate:
-            f = open(filename, 'w')
-            f.write(''.join(lines))
-            f.close()
-
-    def run_command(self, cmd, *args, **kw):
-        """
-        Runs the command, respecting verbosity and simulation.
-        Returns stdout, or None if simulating.
-
-        Keyword arguments:
-
-        cwd:
-            the current working directory to run the command in
-        capture_stderr:
-            if true, then both stdout and stderr will be returned
-        expect_returncode:
-            if true, then don't fail if the return code is not 0
-        force_no_simulate:
-            if true, run the command even if --simulate
-        """
-        if subprocess is None:
-            raise RuntimeError('Environment does not support subprocess '
-                               'module, cannot run command.')
-        cmd = self.quote_first_command_arg(cmd)
-        cwd = popdefault(kw, 'cwd', os.getcwd())
-        capture_stderr = popdefault(kw, 'capture_stderr', False)
-        expect_returncode = popdefault(kw, 'expect_returncode', False)
-        force = popdefault(kw, 'force_no_simulate', False)
-        warn_returncode = popdefault(kw, 'warn_returncode', False)
-        if warn_returncode:
-            expect_returncode = True
-        simulate = self.simulate
-        if force:
-            simulate = False
-        assert not kw, ("Arguments not expected: %s" % kw)
-        if capture_stderr:
-            stderr_pipe = subprocess.STDOUT
-        else:
-            stderr_pipe = subprocess.PIPE
-        try:
-            proc = subprocess.Popen([cmd] + list(args),
-                                    cwd=cwd,
-                                    stderr=stderr_pipe,
-                                    stdout=subprocess.PIPE)
-        except OSError, e:
-            if e.errno != 2:
-                # File not found
-                raise
-            raise OSError(
-                "The expected executable %s was not found (%s)"
-                % (cmd, e))
-        if self.verbose:
-            print 'Running %s %s' % (cmd, ' '.join(args))
-        if simulate:
-            return None
-        stdout, stderr = proc.communicate()
-        if proc.returncode and not expect_returncode:
-            if not self.verbose:
-                print 'Running %s %s' % (cmd, ' '.join(args))
-            print 'Error (exit code: %s)' % proc.returncode
-            if stderr:
-                print stderr
-            raise OSError("Error executing command %s" % cmd)
-        if self.verbose > 2:
-            if stderr:
-                print 'Command error output:'
-                print stderr
-            if stdout:
-                print 'Command output:'
-                print stdout
-        elif proc.returncode and warn_returncode:
-            print 'Warning: command failed (%s %s)' % (cmd, ' '.join(args))
-            print 'Exited with code %s' % proc.returncode
-        return stdout
-
-    def quote_first_command_arg(self, arg):
-        """
-        There's a bug in Windows when running an executable that's
-        located inside a path with a space in it.  This method handles
-        that case, or on non-Windows systems or an executable with no
-        spaces, it just leaves well enough alone.
-        """
-        if (sys.platform != 'win32'
-            or ' ' not in arg):
-            # Problem does not apply:
-            return arg
-        try:
-            import win32api
-        except ImportError:
-            raise ValueError(
-                "The executable %r contains a space, and in order to "
-                "handle this issue you must have the win32api module "
-                "installed" % arg)
-        arg = win32api.GetShortPathName(arg)
-        return arg
-
-    _svn_failed = False
-
-    def svn_command(self, *args, **kw):
-        """
-        Run an svn command, but don't raise an exception if it fails.
-        """
-        try:
-            return self.run_command('svn', *args, **kw)
-        except OSError, e:
-            if not self._svn_failed:
-                print 'Unable to run svn command (%s); proceeding anyway' % e
-                self._svn_failed = True
-
-    def write_file(self, filename, content, source=None,
-                   binary=True, svn_add=True):
-        """
-        Like ``ensure_file``, but without the interactivity.  Mostly
-        deprecated.  (I think I forgot it existed)
-        """
-        import warnings
-        warnings.warn(
-            "command.write_file has been replaced with "
-            "command.ensure_file",
-            DeprecationWarning, 2)
-        if os.path.exists(filename):
-            if binary:
-                f = open(filename, 'rb')
-            else:
-                f = open(filename, 'r')
-            old_content = f.read()
-            f.close()
-            if content == old_content:
-                if self.verbose:
-                    print 'File %s exists with same content' % (
-                        self.shorten(filename))
-                return
-            if (not self.simulate and self.options.interactive):
-                if not self.ask('Overwrite file %s?' % filename):
-                    return
-        if self.verbose > 1 and source:
-            print 'Writing %s from %s' % (self.shorten(filename),
-                                          self.shorten(source))
-        elif self.verbose:
-            print 'Writing %s' % self.shorten(filename)
-        if not self.simulate:
-            already_existed = os.path.exists(filename)
-            if binary:
-                f = open(filename, 'wb')
-            else:
-                f = open(filename, 'w')
-            f.write(content)
-            f.close()
-            if (not already_existed
-                and svn_add
-                and os.path.exists(os.path.join(
-                        os.path.dirname(filename), '.svn'))):
-                self.svn_command('add', filename)
-
-    def parse_vars(self, args):
-        """
-        Given variables like ``['a=b', 'c=d']`` turns it into ``{'a':
-        'b', 'c': 'd'}``
-        """
-        result = {}
-        for arg in args:
-            if '=' not in arg:
-                raise BadCommand(
-                    'Variable assignment %r invalid (no "=")'
-                    % arg)
-            name, value = arg.split('=', 1)
-            result[name] = value
-        return result
-
-    def read_vars(self, config, section='ignite'):
-        """
-        Given a configuration filename, this will return a map of values.
-        """
-        result = {}
-        p = ConfigParser.RawConfigParser()
-        p.read([config])
-        if p.has_section(section):
-            for key, value in p.items(section):
-                if key.endswith('__eval__'):
-                    result[key[:-len('__eval__')]] = eval(value)
-                else:
-                    result[key] = value
-        return result
-
-    def write_vars(self, config, vars, section='ignite'):
-        """
-        Given a configuration filename, this will add items in the
-        vars mapping to the configuration file.  Will create the
-        configuration file if it doesn't exist.
-        """
-        modified = False
-
-        p = ConfigParser.RawConfigParser()
-        if not os.path.exists(config):
-            f = open(config, 'w')
-            f.write('')
-            f.close()
-            modified = True
-        p.read([config])
-        if not p.has_section(section):
-            p.add_section(section)
-            modified = True
-
-        existing_options = p.options(section)
-        for key, value in vars.items():
-            if (key not in existing_options and
-                '%s__eval__' % key not in existing_options):
-                if not isinstance(value, str):
-                    p.set(section, '%s__eval__' % key, repr(value))
-                else:
-                    p.set(section, key, value)
-                modified = True
-
-        if modified:
-            p.write(open(config, 'w'))
-
-    def indent_block(self, text, indent=2, initial=None):
-        """
-        Indent the block of text (each line is indented).  If you give
-        ``initial``, then that is used in lieue of ``indent`` for the
-        first line.
-        """
-        if initial is None:
-            initial = indent
-        lines = text.splitlines()
-        first = (' ' * initial) + lines[0]
-        rest = [(' ' * indent) + l for l in lines[1:]]
-        return '\n'.join([first] + rest)
-
-
-class NotFoundCommand(Command):
-
-    def run(self, args):
-        #for name, value in os.environ.items():
-        #    print '%s: %s' % (name, value)
-        #print sys.argv
-        print ('Command %r not known (you may need to run setup.py egg_info)'
-               % self.command_name)
-        commands = get_commands().items()
-        commands.sort()
-        if not commands:
-            print 'No commands registered.'
-            print 'Have you installed Ignite?'
-            print '(try running python setup.py develop)'
-            return 2
-        print 'Known commands:'
-        longest = max([len(n) for n, c in commands])
-        for name, command in commands:
-            print '  %s  %s' % (self.pad(name, length=longest),
-                                command.load().summary)
-        return 2
-
-
-def popdefault(dict, name, default=None):
-    if name not in dict:
-        return default
-    else:
-        v = dict[name]
-        del dict[name]
-        return v

ignite/commands.py

+# (c) 2005 Ian Bicking and contributors; written for Paste
+# (http://pythonpaste.org)
+#
+# Licensed under the MIT license:
+# http://www.opensource.org/licenses/mit-license.php
+import re
+import sys
+import os
+import pkg_resources
+
+from optparse2 import OptionParser
+
+try:
+    set
+except NameError:
+    from sets import Set as set
+
+try:
+    import subprocess
+except ImportError:
+    subprocess = None
+
+
+class Command(object):
+
+    command_name = ''
+    description = None
+    usage = None
+
+    # Subclasses must implement these:
+    #   parser
+    #   summary
+    #   command()
+
+    def run(self, args):
+        self.parse_args(args)
+
+        # Setup defaults:
+        for name, default in [('verbose', 0),
+                              ('quiet', 0),
+                              ('overwrite', False)]:
+            if not hasattr(self.options, name):
+                setattr(self.options, name, default)
+        if getattr(self.options, 'simulate', False):
+            self.options.verbose = max(self.options.verbose, 1)
+
+        self.verbose = 0
+        self.verbose += self.options.verbose
+        self.verbose -= self.options.quiet
+        self.simulate = getattr(self.options, 'simulate', False)
+
+        if (os.environ.get('IGNITE_DEFAULT_QUIET')):
+            self.verbose = 0
+
+        result = self.command()
+        if result is None:
+            return 0
+        else:
+            return result
+
+    def parse_args(self, args):
+        if self.usage:
+            usage = ' ' + self.usage
+        else:
+            usage = ''
+        self.parser.usage = "%%prog [options]%s\n%s" % (
+            usage, self.summary)
+        self.parser.prog = self._prog_name()
+        if self.description:
+            desc = self.description
+            desc = textwrap.dedent(desc)
+            self.parser.description = desc
+        self.options, self.args = self.parser.parse_args(args)
+
+    def _prog_name(self):
+        return '%s %s' % (os.path.basename(sys.argv[0]),
+                          self.__class__.command_name)
+
+    def pad(self, s, length, dir='left'):
+        if len(s) >= length:
+            return s
+        if dir == 'left':
+            return s + ' ' * (length - len(s))
+        else:
+            return ' ' * (length - len(s)) + s
+
+    def standard_parser(cls, verbose=True,
+                        simulate=False,
+                        quiet=False,
+                        overwrite=False):
+
+        parser = OptionParser()
+
+        if verbose:
+            parser.add_option('-v', '--verbose',
+                              action='count',
+                              dest='verbose',
+                              default=0)
+        if quiet:
+            parser.add_option('-q', '--quiet',
+                              action='count',
+                              dest='quiet',
+                              default=0)
+        if simulate:
+            parser.add_option('-n', '--simulate',
+                              action='store_true',
+                              dest='simulate',
+                              default=False)
+        if overwrite:
+            parser.add_option('-f', '--overwrite',
+                              dest="overwrite",
+                              action="store_true",
+                              help="Overwrite files (warnings will be " + \
+                                  "emitted for non-matching files otherwise)")
+        return parser
+
+    standard_parser = classmethod(standard_parser)
+
+    def shorten(self, fn, *paths):
+        if paths:
+            fn = os.path.join(fn, *paths)
+        if fn.startswith(os.getcwd()):
+            return fn[len(os.getcwd()):].lstrip(os.path.sep)
+        else:
+            return fn
+
+    def insert_into_file(self, filename, marker_name, text,
+                         indent=False):
+        if not text.endswith('\n'):
+            raise ValueError(
+                "The text must end with a newline: %r" % text)
+        if not os.path.exists(filename) and self.simulate:
+            # If we are doing a simulation, it's expected that some
+            # files won't exist...
+            if self.verbose:
+                print 'Would (if not simulating) insert text into %s' % (
+                    self.shorten(filename))
+            return
+
+        f = open(filename)
+        lines = f.readlines()
+        f.close()
+        regex = re.compile(r'-\*-\s+%s:?\s+-\*-' % re.escape(marker_name),
+                           re.I)
+        for i in range(len(lines)):
+            if regex.search(lines[i]):
+                # Found it!
+                if (lines[i:] and len(lines[i:]) > 1 and
+                    ''.join(lines[(i + 1):]).strip().startswith(text.strip())):
+                    # Already have it!
+                    print 'Warning: line already found in %s (not ' % filename
+                    print 'inserting %s' % lines[i]
+                    return
+
+                if indent:
+                    text = text.lstrip()
+                    match = re.search(r'^[ \t]*', lines[i])
+                    text = match.group(0) + text
+                lines[(i + 1):(i + 1)] = [text]
+                break
+        else:
+            errstr = (
+                "Marker '-*- %s -*-' not found in %s"
+                % (marker_name, filename))
+            if 1 or self.simulate:  # @@: being permissive right now
+                print 'Warning: %s' % errstr
+            else:
+                raise ValueError(errstr)
+        if self.verbose:
+            print 'Updating %s' % self.shorten(filename)
+        if not self.simulate:
+            f = open(filename, 'w')
+            f.write(''.join(lines))
+            f.close()
+
+    def run_command(self, cmd, *args, **kw):
+        """
+        Runs the command, respecting verbosity and simulation.
+        Returns stdout, or None if simulating.
+
+        Keyword arguments:
+
+        cwd:
+            the current working directory to run the command in
+        capture_stderr:
+            if true, then both stdout and stderr will be returned
+        expect_returncode:
+            if true, then don't fail if the return code is not 0
+        force_no_simulate:
+            if true, run the command even if --simulate
+        """
+        if subprocess is None:
+            raise RuntimeError('Environment does not support subprocess '
+                               'module, cannot run command.')
+        cmd = self.quote_first_command_arg(cmd)
+        cwd = popdefault(kw, 'cwd', os.getcwd())
+        capture_stderr = popdefault(kw, 'capture_stderr', False)
+        expect_returncode = popdefault(kw, 'expect_returncode', False)
+        force = popdefault(kw, 'force_no_simulate', False)
+        warn_returncode = popdefault(kw, 'warn_returncode', False)
+        if warn_returncode:
+            expect_returncode = True
+        simulate = self.simulate
+        if force:
+            simulate = False
+        assert not kw, ("Arguments not expected: %s" % kw)
+        if capture_stderr:
+            stderr_pipe = subprocess.STDOUT
+        else:
+            stderr_pipe = subprocess.PIPE
+        try:
+            proc = subprocess.Popen([cmd] + list(args),
+                                    cwd=cwd,
+                                    stderr=stderr_pipe,
+                                    stdout=subprocess.PIPE)
+        except OSError, e:
+            if e.errno != 2:
+                # File not found
+                raise
+            raise OSError(
+                "The expected executable %s was not found (%s)"
+                % (cmd, e))
+        if self.verbose:
+            print 'Running %s %s' % (cmd, ' '.join(args))
+        if simulate:
+            return None
+        stdout, stderr = proc.communicate()
+        if proc.returncode and not expect_returncode:
+            if not self.verbose:
+                print 'Running %s %s' % (cmd, ' '.join(args))
+            print 'Error (exit code: %s)' % proc.returncode
+            if stderr:
+                print stderr
+            raise OSError("Error executing command %s" % cmd)
+        if self.verbose > 2:
+            if stderr:
+                print 'Command error output:'
+                print stderr
+            if stdout:
+                print 'Command output:'
+                print stdout
+        elif proc.returncode and warn_returncode:
+            print 'Warning: command failed (%s %s)' % (cmd, ' '.join(args))
+            print 'Exited with code %s' % proc.returncode
+        return stdout
+
+    def quote_first_command_arg(self, arg):
+        if (sys.platform != 'win32'
+            or ' ' not in arg):
+            # Problem does not apply:
+            return arg
+        try:
+            import win32api
+        except ImportError:
+            raise ValueError(
+                "The executable %r contains a space, and in order to "
+                "handle this issue you must have the win32api module "
+                "installed" % arg)
+        arg = win32api.GetShortPathName(arg)
+        return arg
+
+    def parse_variables(self, args):
+        """
+        Given variables like ``['a=b', 'c=d']`` turns it into ``{'a':
+        'b', 'c': 'd'}``
+        """
+        result = {}
+        for arg in args:
+            if '=' not in arg:
+                raise BadCommand(
+                    'Variable assignment %r invalid (no "=")'
+                    % arg)
+            name, value = arg.split('=', 1)
+            result[name] = value
+        return result
+
+
+def popdefault(dict, name, default=None):
+    if name not in dict:
+        return default
+    else:
+        v = dict[name]
+        del dict[name]
+        return v
+
+
+class BadCommand(Exception):
+
+    def __init__(self, message, exit_code=2):
+        self.message = message
+        self.exit_code = exit_code
+        Exception.__init__(self, message)
+
+    def _get_message(self):
+        """Getter for 'message'; needed only to override deprecation
+        in BaseException."""
+        return self.__message
+
+    def _set_message(self, value):
+        """Setter for 'message'; needed only to override deprecation
+        in BaseException."""
+        self.__message = value
+
+    # BaseException.message has been deprecated since Python 2.6.
+    # To prevent DeprecationWarning from popping up over this
+    # pre-existing attribute, use a new property that takes lookup
+    # precedence.
+    message = property(_get_message, _set_message)
+
+
+class TemplateCommand(Command):
+
+    def get_template(self, template_name):
+
+        if '#' in template_name:
+            dist_name, template_name = template_name.split('#', 1)
+        else:
+            dist_name, template_name = None, template_name
+
+        if dist_name is None:
+            for entry in self.find_entry_points('new'):
+                if entry.name == template_name:
+                    template = entry.load()()
+                    dist_name = entry.dist.project_name
+                    break
+            else:
+                raise LookupError(
+                    'Template by name %r not found' % template_name)
+        else:
+            dist = pkg_resources.get_distribution(dist_name)
+            entry = dist.get_entry_info('ignite.ignite_new',
+                                        template_name)
+            template = entry.load()()
+
+        template.full_name = '%s#%s' % (dist_name, template_name)
+        return template
+
+    def find_entry_points(self, name):
+
+        if not hasattr(self, '_entry_points'):
+            self._entry_points = list(pkg_resources.iter_entry_points(
+            'ignite.ignite_' + name))
+
+        return self._entry_points
+
+
+class NewCommand(TemplateCommand):
+
+    name = 'new'
+    usage = 'TEMPLATE_NAME PROJECT_NAME [VAR=VALUE VAR2=VALUE2 ...]'
+    summary = 'Create the file layout for a Python project'
+
+    parser = Command.standard_parser(simulate=True,
+                                     quiet=True,
+                                     overwrite=True)
+
+    parser.add_option('-o', '--output-dir',
+                      dest='output_dir',
+                      metavar='DIR',
+                      default='.',
+                      help='Write put the directory into DIR ' +
+                           '(default current directory)')
+
+    parser.add_option('--config',
+                      action='store',
+                      dest='config',
+                      help='Template variables file')
+
+    _bad_chars_re = re.compile('[^a-zA-Z0-9_]')
+
+    def command(self):
+
+        if len(self.args) < 1:
+            raise BadCommand('You must provide a TEMPLATE_NAME')
+
+        if len(self.args) < 2:
+            raise BadCommand('You must provide a PROJECT_NAME')
+
+        template_name = self.args[0]
+        project_name = self.args[1]
+
+        template = self.get_template(template_name)
+
+        output_dir = os.path.join(self.options.output_dir, project_name)
+
+        package_name = self._bad_chars_re.sub('', project_name.lower())
+
+        # Load variables from the command paramaters
+        egg_name = pkg_resources.to_filename(pkg_resources.safe_name(project_name))
+        variables = {'project': project_name,
+                'package': package_name,
+                'egg': egg_name,
+                }
+        variables.update(self.parse_variables(self.args[2:]))
+
+        # Load variables from a config file if it was passed
+        if self.options.config and os.path.exists(self.options.config):
+            for key, value in self.read_variables(self.options.config).items():
+                variables.setdefault(key, value)
+
+        template.run(self, output_dir, variables)
+
+
+class AddCommand(TemplateCommand):
+    pass
+
+
+class ListCommand(TemplateCommand):
+
+    name = 'list [TEMPLATE_NAME]'
+    usage = ''
+    summary = 'List the available templates or template variables.'
+
+    parser = Command.standard_parser(simulate=True,
+                                     quiet=True,
+                                     overwrite=True)
+
+    def command(self):
+
+        if len(self.args) > 1:
+            raise BadCommand('Too many arguments')
+
+        if len(self.args) == 1:
+            try:
+                template = self.get_template(self.args[0])
+            except LookupError, e:
+                print e.message
+                return 2
+            self.list_variables(template)
+        else:
+            self.list_templates()
+
+    def list_templates(self):
+
+        templates = []
+        for entry in self.find_entry_points('new'):
+            try:
+                templates.append(entry.load()())
+            except Exception, e:
+                # We will not be stopped!
+                print 'Warning: could not load entry point %s (%s: %s)' % (
+                    entry.name, e.__class__.__name__, e)
+
+        if len(templates) == 0:
+            print 'No templates are installed.'
+            return 0
+
+        max_name = max([len(t.name) for t in templates])
+        templates.sort(lambda a, b: cmp(a.name, b.name))
+
+        print 'Available templates:'
+        for template in templates:
+            # @@: Wrap description
+            print '  %s:%s  %s' % (
+                template.name,
+                ' ' * (max_name - len(template.name)),
+                template.summary)
+
+    def list_variables(self, template):
+        title = '%s (from %s)' % (template.name, template.full_name)
+        print title
+        print '-' * len(title)
+
+        template.__class__.print_variables(indent=2)

ignite/copydir.py

-# (c) 2005 Ian Bicking and contributors; written for Paste
-# (http://pythonpaste.org)
-#
-# Licensed under the MIT license:
-# http://www.opensource.org/licenses/mit-license.php
-import os
-import pkg_resources
-import sys
-import string
-import cgi
-import urllib
-import re
-Cheetah = None
-try:
-    import subprocess
-except ImportError:
-    # jython
-    subprocess = None
-import inspect
-
-
-class SkipTemplate(Exception):
-    """
-    Raised to indicate that the template should not be copied over.
-    Raise this exception during the substitution of your template
-    """
-
-
-def copy_dir(source, dest, vars, verbosity, simulate, indent=0,
-             use_cheetah=False, sub_vars=True, interactive=False,
-             svn_add=True, overwrite=True, template_renderer=None):
-    """
-    Copies the ``source`` directory to the ``dest`` directory.
-
-    ``vars``: A dictionary of variables to use in any substitutions.
-
-    ``verbosity``: Higher numbers will show more about what is happening.
-
-    ``simulate``: If true, then don't actually *do* anything.
-
-    ``indent``: Indent any messages by this amount.
-
-    ``sub_vars``: If true, variables in ``_tmpl`` files and ``+var+``
-    in filenames will be substituted.
-
-    ``use_cheetah``: If true, then any templates encountered will be
-    substituted with Cheetah.  Otherwise ``template_renderer`` or
-    ``string.Template`` will be used for templates.
-
-    ``svn_add``: If true, any files written out in directories with
-    ``.svn/`` directories will be added (via ``svn add``).
-
-    ``overwrite``: If false, then don't every overwrite anything.
-
-    ``interactive``: If you are overwriting a file and interactive is
-    true, then ask before overwriting.
-
-    ``template_renderer``: This is a function for rendering templates
-    (if you don't want to use Cheetah or string.Template).  It should
-    have the signature ``template_renderer(content_as_string,
-    vars_as_dict, filename=filename)``.
-    """
-    # This allows you to use a leading +dot+ in filenames which would
-    # otherwise be skipped because leading dots make the file hidden:
-    vars.setdefault('dot', '.')
-    vars.setdefault('plus', '+')
-    use_pkg_resources = isinstance(source, tuple)
-    if use_pkg_resources:
-        names = pkg_resources.resource_listdir(source[0], source[1])
-    else:
-        names = os.listdir(source)
-    names.sort()
-    pad = ' ' * (indent * 2)
-    if not os.path.exists(dest):
-        if verbosity >= 1:
-            print '%sCreating %s/' % (pad, dest)
-        if not simulate:
-            svn_makedirs(dest, svn_add=svn_add, verbosity=verbosity,
-                         pad=pad)
-    elif verbosity >= 2:
-        print '%sDirectory %s exists' % (pad, dest)
-    for name in names:
-        if use_pkg_resources:
-            full = '/'.join([source[1], name])
-        else:
-            full = os.path.join(source, name)
-        reason = should_skip_file(name)
-        if reason:
-            if verbosity >= 2:
-                reason = pad + reason % {'filename': full}
-                print reason
-            continue
-        if sub_vars:
-            dest_full = os.path.join(dest, substitute_filename(name, vars))
-        sub_file = False
-        if dest_full.endswith('_tmpl'):
-            dest_full = dest_full[:-5]
-            sub_file = sub_vars
-        if use_pkg_resources and pkg_resources.resource_isdir(source[0], full):
-            if verbosity:
-                print '%sRecursing into %s' % (pad, os.path.basename(full))
-            copy_dir((source[0], full), dest_full, vars, verbosity, simulate,
-                     indent=indent + 1, use_cheetah=use_cheetah,
-                     sub_vars=sub_vars, interactive=interactive,
-                     svn_add=svn_add, template_renderer=template_renderer)
-            continue
-        elif not use_pkg_resources and os.path.isdir(full):
-            if verbosity:
-                print '%sRecursing into %s' % (pad, os.path.basename(full))
-            copy_dir(full, dest_full, vars, verbosity, simulate,
-                     indent=indent + 1, use_cheetah=use_cheetah,
-                     sub_vars=sub_vars, interactive=interactive,
-                     svn_add=svn_add, template_renderer=template_renderer)
-            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:
-            try:
-                content = substitute_content(
-                    content, vars, filename=full,
-                    use_cheetah=use_cheetah,
-                    template_renderer=template_renderer)
-            except SkipTemplate:
-                continue
-            if content is None:
-                continue
-        already_exists = os.path.exists(dest_full)
-        if already_exists:
-            f = open(dest_full, 'rb')
-            old_content = f.read()
-            f.close()
-            if old_content == content:
-                if verbosity:
-                    print '%s%s already exists (same content)' % \
-                        (pad, dest_full)
-                continue
-            if interactive:
-                if not query_interactive(
-                    full, dest_full, content, old_content,
-                    simulate=simulate):
-                    continue
-            elif not overwrite:
-                continue
-        if verbosity and use_pkg_resources:
-            print '%sCopying %s to %s' % (pad, full, dest_full)
-        elif verbosity:
-            print '%sCopying %s to %s' % (
-                pad, os.path.basename(full), dest_full)
-        if not simulate:
-            f = open(dest_full, 'wb')
-            f.write(content)
-            f.close()
-        if svn_add and not already_exists:
-            if not os.path.exists(os.path.join(os.path.dirname(
-                        os.path.abspath(dest_full)), '.svn')):
-                if verbosity > 1:
-                    print '%s.svn/ does not exist; cannot add file' % pad
-            else:
-                cmd = ['svn', 'add', dest_full]
-                if verbosity > 1:
-                    print '%sRunning: %s' % (pad, ' '.join(cmd))
-                if not simulate:
-                    # @@: Should
-                    if subprocess is None:
-                        raise RuntimeError('copydir failed, environment '
-                                           'does not support subprocess '
-                                           'module')
-                    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
-                    stdout, stderr = proc.communicate()
-                    if verbosity > 1 and stdout:
-                        print 'Script output:'
-                        print stdout
-        elif svn_add and already_exists and verbosity > 1:
-            print '%sFile already exists (not doing svn add)' % pad
-
-
-def should_skip_file(name):
-    """
-    Checks if a file should be skipped based on its name.
-
-    If it should be skipped, returns the reason, otherwise returns
-    None.
-    """
-    if name.startswith('.'):
-        return 'Skipping hidden file %(filename)s'
-    if name.endswith('~') or name.endswith('.bak'):
-        return 'Skipping backup file %(filename)s'
-    if name.endswith('.pyc') or name.endswith('.pyo'):
-        return 'Skipping %s file %%(filename)s' % os.path.splitext(name)[1]
-    if name.endswith('$py.class'):
-        return 'Skipping $py.class file %(filename)s'
-    if name in ('CVS', '_darcs'):
-        return 'Skipping version control directory %(filename)s'
-    return None
-
-# Overridden on user's request:
-all_answer = None
-
-
-def query_interactive(src_fn, dest_fn, src_content, dest_content,
-                      simulate):
-    global all_answer
-    from difflib import unified_diff, context_diff
-    u_diff = list(unified_diff(
-        dest_content.splitlines(),
-        src_content.splitlines(),
-        dest_fn, src_fn))
-    c_diff = list(context_diff(
-        dest_content.splitlines(),
-        src_content.splitlines(),
-        dest_fn, src_fn))
-    added = len([l for l in u_diff if l.startswith('+')
-                   and not l.startswith('+++')])
-    removed = len([l for l in u_diff if l.startswith('-')
-                   and not l.startswith('---')])
-    if added > removed:
-        msg = '; %i lines added' % (added - removed)
-    elif removed > added:
-        msg = '; %i lines removed' % (removed - added)
-    else:
-        msg = ''
-    print 'Replace %i bytes with %i bytes (%i/%i lines changed%s)' % (
-        len(dest_content), len(src_content),
-        removed, len(dest_content.splitlines()), msg)
-    prompt = 'Overwrite %s [y/n/d/B/?] ' % dest_fn
-    while 1:
-        if all_answer is None:
-            response = raw_input(prompt).strip().lower()
-        else:
-            response = all_answer
-        if not response or response[0] == 'b':
-            import shutil
-            new_dest_fn = dest_fn + '.bak'
-            n = 0
-            while os.path.exists(new_dest_fn):
-                n += 1
-                new_dest_fn = dest_fn + '.bak' + str(n)
-            print 'Backing up %s to %s' % (dest_fn, new_dest_fn)
-            if not simulate:
-                shutil.copyfile(dest_fn, new_dest_fn)
-            return True
-        elif response.startswith('all '):
-            rest = response[4:].strip()
-            if not rest or rest[0] not in ('y', 'n', 'b'):
-                print query_usage
-                continue
-            response = all_answer = rest[0]
-        if response[0] == 'y':
-            return True
-        elif response[0] == 'n':
-            return False
-        elif response == 'dc':
-            print '\n'.join(c_diff)
-        elif response[0] == 'd':
-            print '\n'.join(u_diff)
-        else:
-            print query_usage
-
-query_usage = """\
-Responses:
-  Y(es):    Overwrite the file with the new content.
-  N(o):     Do not overwrite the file.
-  D(iff):   Show a unified diff of the proposed changes (dc=context diff)
-  B(ackup): Save the current file contents to a .bak file
-            (and overwrite)
-  Type "all Y/N/B" to use Y/N/B for answer to all future questions
-"""
-
-
-def svn_makedirs(dir, svn_add, verbosity, pad):
-    parent = os.path.dirname(os.path.abspath(dir))
-    if not os.path.exists(parent):
-        svn_makedirs(parent, svn_add, verbosity, pad)
-    os.mkdir(dir)
-    if not svn_add:
-        return
-    if not os.path.exists(os.path.join(parent, '.svn')):
-        if verbosity > 1:
-            print '%s.svn/ does not exist; cannot add directory' % pad
-        return
-    cmd = ['svn', 'add', dir]
-    if verbosity > 1:
-        print '%sRunning: %s' % (pad, ' '.join(cmd))
-    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
-    stdout, stderr = proc.communicate()
-    if verbosity > 1 and stdout:
-        print 'Script output:'
-        print stdout
-
-
-def substitute_filename(fn, vars):
-    for var, value in vars.items():
-        fn = fn.replace('+%s+' % var, str(value))
-    return fn
-
-
-def substitute_content(content, vars, filename='<string>',
-                       use_cheetah=False, template_renderer=None):
-    global Cheetah
-    v = standard_vars.copy()
-    v.update(vars)
-    vars = v
-    if template_renderer is not None:
-        return template_renderer(content, vars, filename=filename)
-    if not use_cheetah:
-        tmpl = LaxTemplate(content)
-        try:
-            return tmpl.substitute(TypeMapper(v))
-        except Exception, e:
-            _add_except(e, ' in file %s' % filename)
-            raise
-    if Cheetah is None:
-        import Cheetah.Template
-    tmpl = Cheetah.Template.Template(source=content,
-                                     searchList=[vars])
-    return careful_sub(tmpl, vars, filename)
-
-
-def careful_sub(cheetah_template, vars, filename):
-    """
-    Substitutes the template with the variables, using the
-    .body() method if it exists.  It assumes that the variables
-    were also passed in via the searchList.
-    """
-    if not hasattr(cheetah_template, 'body'):
-        return sub_catcher(filename, vars, str, cheetah_template)
-    body = cheetah_template.body
-    args, varargs, varkw, defaults = inspect.getargspec(body)
-    call_vars = {}
-    for arg in args:
-        if arg in vars:
-            call_vars[arg] = vars[arg]
-    return sub_catcher(filename, vars, body, **call_vars)
-
-
-def sub_catcher(filename, vars, func, *args, **kw):
-    """
-    Run a substitution, returning the value.  If an error occurs, show
-    the filename.  If the error is a NameError, show the variables.
-    """
-    try:
-        return func(*args, **kw)
-    except SkipTemplate, e:
-        print 'Skipping file %s' % filename
-        if str(e):
-            print str(e)
-        raise
-    except Exception, e:
-        print 'Error in file %s:' % filename
-        if isinstance(e, NameError):
-            items = vars.items()
-            items.sort()