0001# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
0002# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
0003import os
0004import sys
0005if sys.version_info < (2, 4):
0006    from paste.script.util import string24 as string
0007else:
0008    import string
0009import cgi
0010import urllib
0011import re
0012Cheetah = None
0013try:
0014    import subprocess
0015except ImportError:
0016    from paste.script.util import subprocess24 as subprocess
0017import inspect
0018
0019class SkipTemplate(Exception):
0020    """
0021    Raised to indicate that the template should not be copied over.
0022    Raise this exception during the substitution of your template
0023    """
0024
0025def copy_dir(source, dest, vars, verbosity, simulate, indent=0,
0026             use_cheetah=False, sub_vars=True, interactive=False,
0027             svn_add=True, overwrite=True, template_renderer=None):
0028    """
0029    Copies the ``source`` directory to the ``dest`` directory.
0030
0031    ``vars``: A dictionary of variables to use in any substitutions.
0032
0033    ``verbosity``: Higher numbers will show more about what is happening.
0034
0035    ``simulate``: If true, then don't actually *do* anything.
0036
0037    ``indent``: Indent any messages by this amount.
0038
0039    ``sub_vars``: If true, variables in ``_tmpl`` files and ``+var+``
0040    in filenames will be substituted.
0041
0042    ``use_cheetah``: If true, then any templates encountered will be
0043    substituted with Cheetah.  Otherwise ``template_renderer`` or
0044    ``string.Template`` will be used for templates.
0045
0046    ``svn_add``: If true, any files written out in directories with
0047    ``.svn/`` directories will be added (via ``svn add``).
0048
0049    ``overwrite``: If false, then don't every overwrite anything.
0050
0051    ``interactive``: If you are overwriting a file and interactive is
0052    true, then ask before overwriting.
0053
0054    ``template_renderer``: This is a function for rendering templates
0055    (if you don't want to use Cheetah or string.Template).  It should
0056    have the signature ``template_renderer(content_as_string,
0057    vars_as_dict, filename=filename)``.
0058    """
0059    # This allows you to use a leading +dot+ in filenames which would
0060    # otherwise be skipped because leading dots make the file hidden:
0061    vars.setdefault('dot', '.')
0062    vars.setdefault('plus', '+')
0063    names = os.listdir(source)
0064    names.sort()
0065    pad = ' '*(indent*2)
0066    if not os.path.exists(dest):
0067        if verbosity >= 1:
0068            print '%sCreating %s/' % (pad, dest)
0069        if not simulate:
0070            svn_makedirs(dest, svn_add=svn_add, verbosity=verbosity,
0071                         pad=pad)
0072    elif verbosity >= 2:
0073        print '%sDirectory %s exists' % (pad, dest)
0074    for name in names:
0075        full = os.path.join(source, name)
0076        reason = should_skip_file(name)
0077        if reason:
0078            if verbosity >= 2:
0079                reason = pad + reason % {'filename': full}
0080                print reason
0081            continue
0082        if sub_vars:
0083            dest_full = os.path.join(dest, substitute_filename(name, vars))
0084        sub_file = False
0085        if dest_full.endswith('_tmpl'):
0086            dest_full = dest_full[:-5]
0087            sub_file = sub_vars
0088        if os.path.isdir(full):
0089            if verbosity:
0090                print '%sRecursing into %s' % (pad, os.path.basename(full))
0091            copy_dir(full, dest_full, vars, verbosity, simulate,
0092                     indent=indent+1, use_cheetah=use_cheetah,
0093                     sub_vars=sub_vars, interactive=interactive,
0094                     svn_add=svn_add, template_renderer=template_renderer)
0095            continue
0096        f = open(full, 'rb')
0097        content = f.read()
0098        f.close()
0099        if sub_file:
0100            try:
0101                content = substitute_content(content, vars, filename=full,
0102                                             use_cheetah=use_cheetah,
0103                                             template_renderer=template_renderer)
0104            except SkipTemplate:
0105                continue
0106        already_exists = os.path.exists(dest_full)
0107        if already_exists:
0108            f = open(dest_full, 'rb')
0109            old_content = f.read()
0110            f.close()
0111            if old_content == content:
0112                if verbosity:
0113                    print '%s%s already exists (same content)' % (pad, dest_full)
0114                continue
0115            if interactive:
0116                if not query_interactive(
0117                    full, dest_full, content, old_content,
0118                    simulate=simulate):
0119                    continue
0120            elif not overwrite:
0121                continue
0122        if verbosity:
0123            print '%sCopying %s to %s' % (pad, os.path.basename(full), dest_full)
0124        if not simulate:
0125            f = open(dest_full, 'wb')
0126            f.write(content)
0127            f.close()
0128        if svn_add and not already_exists:
0129            if not os.path.exists(os.path.join(os.path.dirname(os.path.abspath(dest_full)), '.svn')):
0130                if verbosity > 1:
0131                    print '%s.svn/ does not exist; cannot add file' % pad
0132            else:
0133                cmd = ['svn', 'add', dest_full]
0134                if verbosity > 1:
0135                    print '%sRunning: %s' % (pad, ' '.join(cmd))
0136                if not simulate:
0137                    # @@: Should
0138                    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
0139                    stdout, stderr = proc.communicate()
0140                    if verbosity > 1 and stdout:
0141                        print 'Script output:'
0142                        print stdout
0143        elif svn_add and already_exists and verbosity > 1:
0144            print '%sFile already exists (not doing svn add)' % pad
0145
0146def should_skip_file(name):
0147    """
0148    Checks if a file should be skipped based on its name.
0149
0150    If it should be skipped, returns the reason, otherwise returns
0151    None.
0152    """
0153    if name.startswith('.'):
0154        return 'Skipping hidden file %(filename)s'
0155    if name.endswith('~') or name.endswith('.bak'):
0156        return 'Skipping backup file %(filename)s'
0157    if name.endswith('.pyc'):
0158        return 'Skipping .pyc file %(filename)s'
0159    if name in ('CVS', '_darcs'):
0160        return 'Skipping version control directory %(filename)s'
0161    return None
0162
0163# Overridden on user's request:
0164all_answer = None
0165
0166def query_interactive(src_fn, dest_fn, src_content, dest_content,
0167                      simulate):
0168    global all_answer
0169    from difflib import unified_diff, context_diff
0170    u_diff = list(unified_diff(
0171        dest_content.splitlines(),
0172        src_content.splitlines(),
0173        dest_fn, src_fn))
0174    c_diff = list(context_diff(
0175        dest_content.splitlines(),
0176        src_content.splitlines(),
0177        dest_fn, src_fn))
0178    added = len([l for l in u_diff if l.startswith('+')
0179                   and not l.startswith('+++')])
0180    removed = len([l for l in u_diff if l.startswith('-')
0181                   and not l.startswith('---')])
0182    if added > removed:
0183        msg = '; %i lines added' % (added-removed)
0184    elif removed > added:
0185        msg = '; %i lines removed' % (removed-added)
0186    else:
0187        msg = ''
0188    print 'Replace %i bytes with %i bytes (%i/%i lines changed%s)' % (
0189        len(dest_content), len(src_content),
0190        removed, len(dest_content.splitlines()), msg)
0191    prompt = 'Overwrite %s [y/n/d/B/?] ' % dest_fn
0192    while 1:
0193        if all_answer is None:
0194            response = raw_input(prompt).strip().lower()
0195        else:
0196            response = all_answer
0197        if not response or response[0] == 'b':
0198            import shutil
0199            new_dest_fn = dest_fn + '.bak'
0200            n = 0
0201            while os.path.exists(new_dest_fn):
0202                n += 1
0203                new_dest_fn = dest_fn + '.bak' + str(n)
0204            print 'Backing up %s to %s' % (dest_fn, new_dest_fn)
0205            if not simulate:
0206                shutil.copyfile(dest_fn, new_dest_fn)
0207            return True
0208        elif response.startswith('all '):
0209            rest = response[4:].strip()
0210            if not rest or rest[0] not in ('y', 'n', 'b'):
0211                print query_usage
0212                continue
0213            response = all_answer = rest[0]
0214        if response[0] == 'y':
0215            return True
0216        elif response[0] == 'n':
0217            return False
0218        elif response == 'dc':
0219            print '\n'.join(c_diff)
0220        elif response[0] == 'd':
0221            print '\n'.join(u_diff)
0222        else:
0223            print query_usage
0224
0225query_usage = """\
0226Responses:
0227  Y(es):    Overwrite the file with the new content.
0228  N(o):     Do not overwrite the file.
0229  D(iff):   Show a unified diff of the proposed changes (dc=context diff)
0230  B(ackup): Save the current file contents to a .bak file
0231            (and overwrite)
0232  Type "all Y/N/B" to use Y/N/B for answer to all future questions
0233"""
0234
0235def svn_makedirs(dir, svn_add, verbosity, pad):
0236    parent = os.path.dirname(os.path.abspath(dir))
0237    if not os.path.exists(parent):
0238        svn_makedirs(parent, svn_add, verbosity, pad)
0239    os.mkdir(dir)
0240    if not svn_add:
0241        return
0242    if not os.path.exists(os.path.join(parent, '.svn')):
0243        if verbosity > 1:
0244            print '%s.svn/ does not exist; cannot add directory' % pad
0245        return
0246    cmd = ['svn', 'add', dir]
0247    if verbosity > 1:
0248        print '%sRunning: %s' % (pad, ' '.join(cmd))
0249    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
0250    stdout, stderr = proc.communicate()
0251    if verbosity > 1 and stdout:
0252        print 'Script output:'
0253        print stdout
0254
0255def substitute_filename(fn, vars):
0256    for var, value in vars.items():
0257        fn = fn.replace('+%s+' % var, str(value))
0258    return fn
0259
0260def substitute_content(content, vars, filename='<string>',
0261                       use_cheetah=False, template_renderer=None):
0262    global Cheetah
0263    v = standard_vars.copy()
0264    v.update(vars)
0265    vars = v
0266    if template_renderer is not None:
0267        return template_renderer(content, vars, filename=filename)
0268    if not use_cheetah:
0269        tmpl = LaxTemplate(content)
0270        try:
0271            return tmpl.substitute(TypeMapper(v))
0272        except Exception, e:
0273            _add_except(e, ' in file %s' % filename)
0274            raise
0275    if Cheetah is None:
0276        import Cheetah.Template
0277    tmpl = Cheetah.Template.Template(source=content,
0278                                     searchList=[vars])
0279    return careful_sub(tmpl, vars, filename)
0280
0281def careful_sub(cheetah_template, vars, filename):
0282    """
0283    Substitutes the template with the variables, using the
0284    .body() method if it exists.  It assumes that the variables
0285    were also passed in via the searchList.
0286    """
0287    if not hasattr(cheetah_template, 'body'):
0288        return sub_catcher(filename, vars, str, cheetah_template)
0289    body = cheetah_template.body
0290    args, varargs, varkw, defaults = inspect.getargspec(body)
0291    call_vars = {}
0292    for arg in args:
0293        if arg in vars:
0294            call_vars[arg] = vars[arg]
0295    return sub_catcher(filename, vars, body, **call_vars)
0296
0297def sub_catcher(filename, vars, func, *args, **kw):
0298    """
0299    Run a substitution, returning the value.  If an error occurs, show
0300    the filename.  If the error is a NameError, show the variables.
0301    """
0302    try:
0303        return func(*args, **kw)
0304    except SkipTemplate, e:
0305        print 'Skipping file %s' % filename
0306        if str(e):
0307            print str(e)
0308    except Exception, e:
0309        print 'Error in file %s:' % filename
0310        if isinstance(e, NameError):
0311            items = vars.items()
0312            items.sort()
0313            for name, value in items:
0314                print '%s = %r' % (name, value)
0315        raise
0316
0317def html_quote(s):
0318    if s is None:
0319        return ''
0320    return cgi.escape(str(s), 1)
0321
0322def url_quote(s):
0323    if s is None:
0324        return ''
0325    return urllib.quote(str(s))
0326
0327def test(conf, true_cond, false_cond=None):
0328    if conf:
0329        return true_cond
0330    else:
0331        return false_cond
0332
0333def skip_template(condition=True, *args):
0334    """
0335    Raise SkipTemplate, which causes copydir to skip the template
0336    being processed.  If you pass in a condition, only raise if that
0337    condition is true (allows you to use this with string.Template)
0338
0339    If you pass any additional arguments, they will be used to
0340    instantiate SkipTemplate (generally use like
0341    ``skip_template(license=='GPL', 'Skipping file; not using GPL')``)
0342    """
0343    if condition:
0344        raise SkipTemplate(*args)
0345
0346def _add_except(exc, info):
0347    if not hasattr(exc, 'args') or exc.args is None:
0348        return
0349    args = list(exc.args)
0350    if args:
0351        args[0] += ' ' + info
0352    else:
0353        args = [info]
0354    exc.args = tuple(args)
0355    return
0356
0357
0358standard_vars = {
0359    'nothing': None,
0360    'html_quote': html_quote,
0361    'url_quote': url_quote,
0362    'empty': '""',
0363    'test': test,
0364    'repr': repr,
0365    'str': str,
0366    'bool': bool,
0367    'SkipTemplate': SkipTemplate,
0368    'skip_template': skip_template,
0369    }
0370
0371class TypeMapper(dict):
0372
0373    def __getitem__(self, item):
0374        options = item.split('|')
0375        for op in options[:-1]:
0376            try:
0377                value = eval_with_catch(op, dict(self.items()))
0378                break
0379            except (NameError, KeyError):
0380                pass
0381        else:
0382            value = eval(options[-1], dict(self.items()))
0383        if value is None:
0384            return ''
0385        else:
0386            return str(value)
0387
0388def eval_with_catch(expr, vars):
0389    try:
0390        return eval(expr, vars)
0391    except Exception, e:
0392        _add_except(e, 'in expression %r' % expr)
0393        raise
0394
0395class LaxTemplate(string.Template):
0396    # This change of pattern allows for anything in braces, but
0397    # only identifiers outside of braces:
0398    pattern = re.compile(r"""
0399    \$(?:
0400      (?P<escaped>\$)             |   # Escape sequence of two delimiters
0401      (?P<named>[_a-z][_a-z0-9]*) |   # delimiter and a Python identifier
0402      {(?P<braced>.*?)}           |   # delimiter and a braced identifier
0403      (?P<invalid>)                   # Other ill-formed delimiter exprs
0404    )
0405    """, re.VERBOSE | re.IGNORECASE)