0001
0002
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
0060
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
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
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
0397
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)