| 1 | # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) |
|---|
| 2 | # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php |
|---|
| 3 | import os |
|---|
| 4 | import pkg_resources |
|---|
| 5 | import sys |
|---|
| 6 | if sys.version_info < (2, 4): |
|---|
| 7 | from paste.script.util import string24 as string |
|---|
| 8 | else: |
|---|
| 9 | import string |
|---|
| 10 | import cgi |
|---|
| 11 | import urllib |
|---|
| 12 | import re |
|---|
| 13 | Cheetah = None |
|---|
| 14 | try: |
|---|
| 15 | import subprocess |
|---|
| 16 | except ImportError: |
|---|
| 17 | from paste.script.util import subprocess24 as subprocess |
|---|
| 18 | import inspect |
|---|
| 19 | |
|---|
| 20 | class SkipTemplate(Exception): |
|---|
| 21 | """ |
|---|
| 22 | Raised to indicate that the template should not be copied over. |
|---|
| 23 | Raise this exception during the substitution of your template |
|---|
| 24 | """ |
|---|
| 25 | |
|---|
| 26 | def copy_dir(source, dest, vars, verbosity, simulate, indent=0, |
|---|
| 27 | use_cheetah=False, sub_vars=True, interactive=False, |
|---|
| 28 | svn_add=True, overwrite=True, template_renderer=None): |
|---|
| 29 | """ |
|---|
| 30 | Copies the ``source`` directory to the ``dest`` directory. |
|---|
| 31 | |
|---|
| 32 | ``vars``: A dictionary of variables to use in any substitutions. |
|---|
| 33 | |
|---|
| 34 | ``verbosity``: Higher numbers will show more about what is happening. |
|---|
| 35 | |
|---|
| 36 | ``simulate``: If true, then don't actually *do* anything. |
|---|
| 37 | |
|---|
| 38 | ``indent``: Indent any messages by this amount. |
|---|
| 39 | |
|---|
| 40 | ``sub_vars``: If true, variables in ``_tmpl`` files and ``+var+`` |
|---|
| 41 | in filenames will be substituted. |
|---|
| 42 | |
|---|
| 43 | ``use_cheetah``: If true, then any templates encountered will be |
|---|
| 44 | substituted with Cheetah. Otherwise ``template_renderer`` or |
|---|
| 45 | ``string.Template`` will be used for templates. |
|---|
| 46 | |
|---|
| 47 | ``svn_add``: If true, any files written out in directories with |
|---|
| 48 | ``.svn/`` directories will be added (via ``svn add``). |
|---|
| 49 | |
|---|
| 50 | ``overwrite``: If false, then don't every overwrite anything. |
|---|
| 51 | |
|---|
| 52 | ``interactive``: If you are overwriting a file and interactive is |
|---|
| 53 | true, then ask before overwriting. |
|---|
| 54 | |
|---|
| 55 | ``template_renderer``: This is a function for rendering templates |
|---|
| 56 | (if you don't want to use Cheetah or string.Template). It should |
|---|
| 57 | have the signature ``template_renderer(content_as_string, |
|---|
| 58 | vars_as_dict, filename=filename)``. |
|---|
| 59 | """ |
|---|
| 60 | # This allows you to use a leading +dot+ in filenames which would |
|---|
| 61 | # otherwise be skipped because leading dots make the file hidden: |
|---|
| 62 | vars.setdefault('dot', '.') |
|---|
| 63 | vars.setdefault('plus', '+') |
|---|
| 64 | use_pkg_resources = isinstance(source, tuple) |
|---|
| 65 | if use_pkg_resources: |
|---|
| 66 | names = pkg_resources.resource_listdir(source[0], source[1]) |
|---|
| 67 | else: |
|---|
| 68 | names = os.listdir(source) |
|---|
| 69 | names.sort() |
|---|
| 70 | pad = ' '*(indent*2) |
|---|
| 71 | if not os.path.exists(dest): |
|---|
| 72 | if verbosity >= 1: |
|---|
| 73 | print '%sCreating %s/' % (pad, dest) |
|---|
| 74 | if not simulate: |
|---|
| 75 | svn_makedirs(dest, svn_add=svn_add, verbosity=verbosity, |
|---|
| 76 | pad=pad) |
|---|
| 77 | elif verbosity >= 2: |
|---|
| 78 | print '%sDirectory %s exists' % (pad, dest) |
|---|
| 79 | for name in names: |
|---|
| 80 | if use_pkg_resources: |
|---|
| 81 | full = '/'.join([source[1], name]) |
|---|
| 82 | else: |
|---|
| 83 | full = os.path.join(source, name) |
|---|
| 84 | reason = should_skip_file(name) |
|---|
| 85 | if reason: |
|---|
| 86 | if verbosity >= 2: |
|---|
| 87 | reason = pad + reason % {'filename': full} |
|---|
| 88 | print reason |
|---|
| 89 | continue |
|---|
| 90 | if sub_vars: |
|---|
| 91 | dest_full = os.path.join(dest, substitute_filename(name, vars)) |
|---|
| 92 | sub_file = False |
|---|
| 93 | if dest_full.endswith('_tmpl'): |
|---|
| 94 | dest_full = dest_full[:-5] |
|---|
| 95 | sub_file = sub_vars |
|---|
| 96 | if use_pkg_resources and pkg_resources.resource_isdir(source[0], full): |
|---|
| 97 | if verbosity: |
|---|
| 98 | print '%sRecursing into %s' % (pad, os.path.basename(full)) |
|---|
| 99 | copy_dir((source[0], full), dest_full, vars, verbosity, simulate, |
|---|
| 100 | indent=indent+1, use_cheetah=use_cheetah, |
|---|
| 101 | sub_vars=sub_vars, interactive=interactive, |
|---|
| 102 | svn_add=svn_add, template_renderer=template_renderer) |
|---|
| 103 | continue |
|---|
| 104 | elif not use_pkg_resources and os.path.isdir(full): |
|---|
| 105 | if verbosity: |
|---|
| 106 | print '%sRecursing into %s' % (pad, os.path.basename(full)) |
|---|
| 107 | copy_dir(full, dest_full, vars, verbosity, simulate, |
|---|
| 108 | indent=indent+1, use_cheetah=use_cheetah, |
|---|
| 109 | sub_vars=sub_vars, interactive=interactive, |
|---|
| 110 | svn_add=svn_add, template_renderer=template_renderer) |
|---|
| 111 | continue |
|---|
| 112 | elif use_pkg_resources: |
|---|
| 113 | content = pkg_resources.resource_string(source[0], full) |
|---|
| 114 | else: |
|---|
| 115 | f = open(full, 'rb') |
|---|
| 116 | content = f.read() |
|---|
| 117 | f.close() |
|---|
| 118 | if sub_file: |
|---|
| 119 | try: |
|---|
| 120 | content = substitute_content(content, vars, filename=full, |
|---|
| 121 | use_cheetah=use_cheetah, |
|---|
| 122 | template_renderer=template_renderer) |
|---|
| 123 | except SkipTemplate: |
|---|
| 124 | continue |
|---|
| 125 | if content is None: |
|---|
| 126 | continue |
|---|
| 127 | already_exists = os.path.exists(dest_full) |
|---|
| 128 | if already_exists: |
|---|
| 129 | f = open(dest_full, 'rb') |
|---|
| 130 | old_content = f.read() |
|---|
| 131 | f.close() |
|---|
| 132 | if old_content == content: |
|---|
| 133 | if verbosity: |
|---|
| 134 | print '%s%s already exists (same content)' % (pad, dest_full) |
|---|
| 135 | continue |
|---|
| 136 | if interactive: |
|---|
| 137 | if not query_interactive( |
|---|
| 138 | full, dest_full, content, old_content, |
|---|
| 139 | simulate=simulate): |
|---|
| 140 | continue |
|---|
| 141 | elif not overwrite: |
|---|
| 142 | continue |
|---|
| 143 | if verbosity and use_pkg_resources: |
|---|
| 144 | print '%sCopying %s to %s' % (pad, full, dest_full) |
|---|
| 145 | elif verbosity: |
|---|
| 146 | print '%sCopying %s to %s' % (pad, os.path.basename(full), dest_full) |
|---|
| 147 | if not simulate: |
|---|
| 148 | f = open(dest_full, 'wb') |
|---|
| 149 | f.write(content) |
|---|
| 150 | f.close() |
|---|
| 151 | if svn_add and not already_exists: |
|---|
| 152 | if not os.path.exists(os.path.join(os.path.dirname(os.path.abspath(dest_full)), '.svn')): |
|---|
| 153 | if verbosity > 1: |
|---|
| 154 | print '%s.svn/ does not exist; cannot add file' % pad |
|---|
| 155 | else: |
|---|
| 156 | cmd = ['svn', 'add', dest_full] |
|---|
| 157 | if verbosity > 1: |
|---|
| 158 | print '%sRunning: %s' % (pad, ' '.join(cmd)) |
|---|
| 159 | if not simulate: |
|---|
| 160 | # @@: Should |
|---|
| 161 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) |
|---|
| 162 | stdout, stderr = proc.communicate() |
|---|
| 163 | if verbosity > 1 and stdout: |
|---|
| 164 | print 'Script output:' |
|---|
| 165 | print stdout |
|---|
| 166 | elif svn_add and already_exists and verbosity > 1: |
|---|
| 167 | print '%sFile already exists (not doing svn add)' % pad |
|---|
| 168 | |
|---|
| 169 | def should_skip_file(name): |
|---|
| 170 | """ |
|---|
| 171 | Checks if a file should be skipped based on its name. |
|---|
| 172 | |
|---|
| 173 | If it should be skipped, returns the reason, otherwise returns |
|---|
| 174 | None. |
|---|
| 175 | """ |
|---|
| 176 | if name.startswith('.'): |
|---|
| 177 | return 'Skipping hidden file %(filename)s' |
|---|
| 178 | if name.endswith('~') or name.endswith('.bak'): |
|---|
| 179 | return 'Skipping backup file %(filename)s' |
|---|
| 180 | if name.endswith('.pyc'): |
|---|
| 181 | return 'Skipping .pyc file %(filename)s' |
|---|
| 182 | if name.endswith('$py.class'): |
|---|
| 183 | return 'Skipping $py.class file %(filename)s' |
|---|
| 184 | if name in ('CVS', '_darcs'): |
|---|
| 185 | return 'Skipping version control directory %(filename)s' |
|---|
| 186 | return None |
|---|
| 187 | |
|---|
| 188 | # Overridden on user's request: |
|---|
| 189 | all_answer = None |
|---|
| 190 | |
|---|
| 191 | def query_interactive(src_fn, dest_fn, src_content, dest_content, |
|---|
| 192 | simulate): |
|---|
| 193 | global all_answer |
|---|
| 194 | from difflib import unified_diff, context_diff |
|---|
| 195 | u_diff = list(unified_diff( |
|---|
| 196 | dest_content.splitlines(), |
|---|
| 197 | src_content.splitlines(), |
|---|
| 198 | dest_fn, src_fn)) |
|---|
| 199 | c_diff = list(context_diff( |
|---|
| 200 | dest_content.splitlines(), |
|---|
| 201 | src_content.splitlines(), |
|---|
| 202 | dest_fn, src_fn)) |
|---|
| 203 | added = len([l for l in u_diff if l.startswith('+') |
|---|
| 204 | and not l.startswith('+++')]) |
|---|
| 205 | removed = len([l for l in u_diff if l.startswith('-') |
|---|
| 206 | and not l.startswith('---')]) |
|---|
| 207 | if added > removed: |
|---|
| 208 | msg = '; %i lines added' % (added-removed) |
|---|
| 209 | elif removed > added: |
|---|
| 210 | msg = '; %i lines removed' % (removed-added) |
|---|
| 211 | else: |
|---|
| 212 | msg = '' |
|---|
| 213 | print 'Replace %i bytes with %i bytes (%i/%i lines changed%s)' % ( |
|---|
| 214 | len(dest_content), len(src_content), |
|---|
| 215 | removed, len(dest_content.splitlines()), msg) |
|---|
| 216 | prompt = 'Overwrite %s [y/n/d/B/?] ' % dest_fn |
|---|
| 217 | while 1: |
|---|
| 218 | if all_answer is None: |
|---|
| 219 | response = raw_input(prompt).strip().lower() |
|---|
| 220 | else: |
|---|
| 221 | response = all_answer |
|---|
| 222 | if not response or response[0] == 'b': |
|---|
| 223 | import shutil |
|---|
| 224 | new_dest_fn = dest_fn + '.bak' |
|---|
| 225 | n = 0 |
|---|
| 226 | while os.path.exists(new_dest_fn): |
|---|
| 227 | n += 1 |
|---|
| 228 | new_dest_fn = dest_fn + '.bak' + str(n) |
|---|
| 229 | print 'Backing up %s to %s' % (dest_fn, new_dest_fn) |
|---|
| 230 | if not simulate: |
|---|
| 231 | shutil.copyfile(dest_fn, new_dest_fn) |
|---|
| 232 | return True |
|---|
| 233 | elif response.startswith('all '): |
|---|
| 234 | rest = response[4:].strip() |
|---|
| 235 | if not rest or rest[0] not in ('y', 'n', 'b'): |
|---|
| 236 | print query_usage |
|---|
| 237 | continue |
|---|
| 238 | response = all_answer = rest[0] |
|---|
| 239 | if response[0] == 'y': |
|---|
| 240 | return True |
|---|
| 241 | elif response[0] == 'n': |
|---|
| 242 | return False |
|---|
| 243 | elif response == 'dc': |
|---|
| 244 | print '\n'.join(c_diff) |
|---|
| 245 | elif response[0] == 'd': |
|---|
| 246 | print '\n'.join(u_diff) |
|---|
| 247 | else: |
|---|
| 248 | print query_usage |
|---|
| 249 | |
|---|
| 250 | query_usage = """\ |
|---|
| 251 | Responses: |
|---|
| 252 | Y(es): Overwrite the file with the new content. |
|---|
| 253 | N(o): Do not overwrite the file. |
|---|
| 254 | D(iff): Show a unified diff of the proposed changes (dc=context diff) |
|---|
| 255 | B(ackup): Save the current file contents to a .bak file |
|---|
| 256 | (and overwrite) |
|---|
| 257 | Type "all Y/N/B" to use Y/N/B for answer to all future questions |
|---|
| 258 | """ |
|---|
| 259 | |
|---|
| 260 | def svn_makedirs(dir, svn_add, verbosity, pad): |
|---|
| 261 | parent = os.path.dirname(os.path.abspath(dir)) |
|---|
| 262 | if not os.path.exists(parent): |
|---|
| 263 | svn_makedirs(parent, svn_add, verbosity, pad) |
|---|
| 264 | os.mkdir(dir) |
|---|
| 265 | if not svn_add: |
|---|
| 266 | return |
|---|
| 267 | if not os.path.exists(os.path.join(parent, '.svn')): |
|---|
| 268 | if verbosity > 1: |
|---|
| 269 | print '%s.svn/ does not exist; cannot add directory' % pad |
|---|
| 270 | return |
|---|
| 271 | cmd = ['svn', 'add', dir] |
|---|
| 272 | if verbosity > 1: |
|---|
| 273 | print '%sRunning: %s' % (pad, ' '.join(cmd)) |
|---|
| 274 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) |
|---|
| 275 | stdout, stderr = proc.communicate() |
|---|
| 276 | if verbosity > 1 and stdout: |
|---|
| 277 | print 'Script output:' |
|---|
| 278 | print stdout |
|---|
| 279 | |
|---|
| 280 | def substitute_filename(fn, vars): |
|---|
| 281 | for var, value in vars.items(): |
|---|
| 282 | fn = fn.replace('+%s+' % var, str(value)) |
|---|
| 283 | return fn |
|---|
| 284 | |
|---|
| 285 | def substitute_content(content, vars, filename='<string>', |
|---|
| 286 | use_cheetah=False, template_renderer=None): |
|---|
| 287 | global Cheetah |
|---|
| 288 | v = standard_vars.copy() |
|---|
| 289 | v.update(vars) |
|---|
| 290 | vars = v |
|---|
| 291 | if template_renderer is not None: |
|---|
| 292 | return template_renderer(content, vars, filename=filename) |
|---|
| 293 | if not use_cheetah: |
|---|
| 294 | tmpl = LaxTemplate(content) |
|---|
| 295 | try: |
|---|
| 296 | return tmpl.substitute(TypeMapper(v)) |
|---|
| 297 | except Exception, e: |
|---|
| 298 | _add_except(e, ' in file %s' % filename) |
|---|
| 299 | raise |
|---|
| 300 | if Cheetah is None: |
|---|
| 301 | import Cheetah.Template |
|---|
| 302 | tmpl = Cheetah.Template.Template(source=content, |
|---|
| 303 | searchList=[vars]) |
|---|
| 304 | return careful_sub(tmpl, vars, filename) |
|---|
| 305 | |
|---|
| 306 | def careful_sub(cheetah_template, vars, filename): |
|---|
| 307 | """ |
|---|
| 308 | Substitutes the template with the variables, using the |
|---|
| 309 | .body() method if it exists. It assumes that the variables |
|---|
| 310 | were also passed in via the searchList. |
|---|
| 311 | """ |
|---|
| 312 | if not hasattr(cheetah_template, 'body'): |
|---|
| 313 | return sub_catcher(filename, vars, str, cheetah_template) |
|---|
| 314 | body = cheetah_template.body |
|---|
| 315 | args, varargs, varkw, defaults = inspect.getargspec(body) |
|---|
| 316 | call_vars = {} |
|---|
| 317 | for arg in args: |
|---|
| 318 | if arg in vars: |
|---|
| 319 | call_vars[arg] = vars[arg] |
|---|
| 320 | return sub_catcher(filename, vars, body, **call_vars) |
|---|
| 321 | |
|---|
| 322 | def sub_catcher(filename, vars, func, *args, **kw): |
|---|
| 323 | """ |
|---|
| 324 | Run a substitution, returning the value. If an error occurs, show |
|---|
| 325 | the filename. If the error is a NameError, show the variables. |
|---|
| 326 | """ |
|---|
| 327 | try: |
|---|
| 328 | return func(*args, **kw) |
|---|
| 329 | except SkipTemplate, e: |
|---|
| 330 | print 'Skipping file %s' % filename |
|---|
| 331 | if str(e): |
|---|
| 332 | print str(e) |
|---|
| 333 | raise |
|---|
| 334 | except Exception, e: |
|---|
| 335 | print 'Error in file %s:' % filename |
|---|
| 336 | if isinstance(e, NameError): |
|---|
| 337 | items = vars.items() |
|---|
| 338 | items.sort() |
|---|
| 339 | for name, value in items: |
|---|
| 340 | print '%s = %r' % (name, value) |
|---|
| 341 | raise |
|---|
| 342 | |
|---|
| 343 | def html_quote(s): |
|---|
| 344 | if s is None: |
|---|
| 345 | return '' |
|---|
| 346 | return cgi.escape(str(s), 1) |
|---|
| 347 | |
|---|
| 348 | def url_quote(s): |
|---|
| 349 | if s is None: |
|---|
| 350 | return '' |
|---|
| 351 | return urllib.quote(str(s)) |
|---|
| 352 | |
|---|
| 353 | def test(conf, true_cond, false_cond=None): |
|---|
| 354 | if conf: |
|---|
| 355 | return true_cond |
|---|
| 356 | else: |
|---|
| 357 | return false_cond |
|---|
| 358 | |
|---|
| 359 | def skip_template(condition=True, *args): |
|---|
| 360 | """ |
|---|
| 361 | Raise SkipTemplate, which causes copydir to skip the template |
|---|
| 362 | being processed. If you pass in a condition, only raise if that |
|---|
| 363 | condition is true (allows you to use this with string.Template) |
|---|
| 364 | |
|---|
| 365 | If you pass any additional arguments, they will be used to |
|---|
| 366 | instantiate SkipTemplate (generally use like |
|---|
| 367 | ``skip_template(license=='GPL', 'Skipping file; not using GPL')``) |
|---|
| 368 | """ |
|---|
| 369 | if condition: |
|---|
| 370 | raise SkipTemplate(*args) |
|---|
| 371 | |
|---|
| 372 | def _add_except(exc, info): |
|---|
| 373 | if not hasattr(exc, 'args') or exc.args is None: |
|---|
| 374 | return |
|---|
| 375 | args = list(exc.args) |
|---|
| 376 | if args: |
|---|
| 377 | args[0] += ' ' + info |
|---|
| 378 | else: |
|---|
| 379 | args = [info] |
|---|
| 380 | exc.args = tuple(args) |
|---|
| 381 | return |
|---|
| 382 | |
|---|
| 383 | |
|---|
| 384 | standard_vars = { |
|---|
| 385 | 'nothing': None, |
|---|
| 386 | 'html_quote': html_quote, |
|---|
| 387 | 'url_quote': url_quote, |
|---|
| 388 | 'empty': '""', |
|---|
| 389 | 'test': test, |
|---|
| 390 | 'repr': repr, |
|---|
| 391 | 'str': str, |
|---|
| 392 | 'bool': bool, |
|---|
| 393 | 'SkipTemplate': SkipTemplate, |
|---|
| 394 | 'skip_template': skip_template, |
|---|
| 395 | } |
|---|
| 396 | |
|---|
| 397 | class TypeMapper(dict): |
|---|
| 398 | |
|---|
| 399 | def __getitem__(self, item): |
|---|
| 400 | options = item.split('|') |
|---|
| 401 | for op in options[:-1]: |
|---|
| 402 | try: |
|---|
| 403 | value = eval_with_catch(op, dict(self.items())) |
|---|
| 404 | break |
|---|
| 405 | except (NameError, KeyError): |
|---|
| 406 | pass |
|---|
| 407 | else: |
|---|
| 408 | value = eval(options[-1], dict(self.items())) |
|---|
| 409 | if value is None: |
|---|
| 410 | return '' |
|---|
| 411 | else: |
|---|
| 412 | return str(value) |
|---|
| 413 | |
|---|
| 414 | def eval_with_catch(expr, vars): |
|---|
| 415 | try: |
|---|
| 416 | return eval(expr, vars) |
|---|
| 417 | except Exception, e: |
|---|
| 418 | _add_except(e, 'in expression %r' % expr) |
|---|
| 419 | raise |
|---|
| 420 | |
|---|
| 421 | class LaxTemplate(string.Template): |
|---|
| 422 | # This change of pattern allows for anything in braces, but |
|---|
| 423 | # only identifiers outside of braces: |
|---|
| 424 | pattern = r""" |
|---|
| 425 | \$(?: |
|---|
| 426 | (?P<escaped>\$) | # Escape sequence of two delimiters |
|---|
| 427 | (?P<named>[_a-z][_a-z0-9]*) | # delimiter and a Python identifier |
|---|
| 428 | {(?P<braced>.*?)} | # delimiter and a braced identifier |
|---|
| 429 | (?P<invalid>) # Other ill-formed delimiter exprs |
|---|
| 430 | ) |
|---|
| 431 | """ |
|---|