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 | """ |
---|