Michael Merickel
2018-10-15 0c29cf2df41600d3906d521c72991c7686018b71
commit | author | age
0e0cb7 1 # (c) 2005 Ian Bicking and contributors; written for Paste
CM 2 # (http://pythonpaste.org) Licensed under the MIT license:
3 # http://www.opensource.org/licenses/mit-license.php
4
5 import os
6 import sys
7 import pkg_resources
8
9 from pyramid.compat import (
10     input_,
f3840e 11     native_,
GH 12     url_quote as compat_url_quote,
13     escape,
0c29cf 14 )
0e0cb7 15
CM 16 fsenc = sys.getfilesystemencoding()
17
25c64c 18
0e0cb7 19 class SkipTemplate(Exception):
CM 20     """
21     Raised to indicate that the template should not be copied over.
22     Raise this exception during the substitution of your template
23     """
24
0c29cf 25
MM 26 def copy_dir(
27     source,
28     dest,
29     vars,
30     verbosity,
31     simulate,
32     indent=0,
33     sub_vars=True,
34     interactive=False,
35     overwrite=True,
36     template_renderer=None,
37     out_=sys.stdout,
38 ):
0e0cb7 39     """
CM 40     Copies the ``source`` directory to the ``dest`` directory.
41
42     ``vars``: A dictionary of variables to use in any substitutions.
43
44     ``verbosity``: Higher numbers will show more about what is happening.
45
46     ``simulate``: If true, then don't actually *do* anything.
47
48     ``indent``: Indent any messages by this amount.
49
50     ``sub_vars``: If true, variables in ``_tmpl`` files and ``+var+``
51     in filenames will be substituted.
52
53     ``overwrite``: If false, then don't every overwrite anything.
54
55     ``interactive``: If you are overwriting a file and interactive is
56     true, then ask before overwriting.
57
58     ``template_renderer``: This is a function for rendering templates (if you
59     don't want to use string.Template).  It should have the signature
60     ``template_renderer(content_as_string, vars_as_dict,
61     filename=filename)``.
62     """
0c29cf 63
360eba 64     def out(msg):
CM 65         out_.write(msg)
dfc127 66         out_.write('\n')
360eba 67         out_.flush()
0c29cf 68
0e0cb7 69     # This allows you to use a leading +dot+ in filenames which would
CM 70     # otherwise be skipped because leading dots make the file hidden:
71     vars.setdefault('dot', '.')
72     vars.setdefault('plus', '+')
73     use_pkg_resources = isinstance(source, tuple)
74     if use_pkg_resources:
75         names = sorted(pkg_resources.resource_listdir(source[0], source[1]))
76     else:
77         names = sorted(os.listdir(source))
25c64c 78     pad = ' ' * (indent * 2)
0e0cb7 79     if not os.path.exists(dest):
CM 80         if verbosity >= 1:
360eba 81             out('%sCreating %s/' % (pad, dest))
0e0cb7 82         if not simulate:
CM 83             makedirs(dest, verbosity=verbosity, pad=pad)
84     elif verbosity >= 2:
360eba 85         out('%sDirectory %s exists' % (pad, dest))
0e0cb7 86     for name in names:
CM 87         if use_pkg_resources:
88             full = '/'.join([source[1], name])
89         else:
90             full = os.path.join(source, name)
91         reason = should_skip_file(name)
92         if reason:
93             if verbosity >= 2:
94                 reason = pad + reason % {'filename': full}
360eba 95                 out(reason)
0c29cf 96             continue  # pragma: no cover
0e0cb7 97         if sub_vars:
CM 98             dest_full = os.path.join(dest, substitute_filename(name, vars))
99         sub_file = False
100         if dest_full.endswith('_tmpl'):
101             dest_full = dest_full[:-5]
102             sub_file = sub_vars
103         if use_pkg_resources and pkg_resources.resource_isdir(source[0], full):
104             if verbosity:
360eba 105                 out('%sRecursing into %s' % (pad, os.path.basename(full)))
0c29cf 106             copy_dir(
MM 107                 (source[0], full),
108                 dest_full,
109                 vars,
110                 verbosity,
111                 simulate,
112                 indent=indent + 1,
113                 sub_vars=sub_vars,
114                 interactive=interactive,
115                 overwrite=overwrite,
116                 template_renderer=template_renderer,
117                 out_=out_,
118             )
0e0cb7 119             continue
CM 120         elif not use_pkg_resources and os.path.isdir(full):
121             if verbosity:
360eba 122                 out('%sRecursing into %s' % (pad, os.path.basename(full)))
0c29cf 123             copy_dir(
MM 124                 full,
125                 dest_full,
126                 vars,
127                 verbosity,
128                 simulate,
129                 indent=indent + 1,
130                 sub_vars=sub_vars,
131                 interactive=interactive,
132                 overwrite=overwrite,
133                 template_renderer=template_renderer,
134                 out_=out_,
135             )
0e0cb7 136             continue
CM 137         elif use_pkg_resources:
138             content = pkg_resources.resource_string(source[0], full)
139         else:
0f2a11 140             with open(full, 'rb') as f:
MM 141                 content = f.read()
0e0cb7 142         if sub_file:
CM 143             try:
144                 content = substitute_content(
0c29cf 145                     content,
MM 146                     vars,
147                     filename=full,
148                     template_renderer=template_renderer,
149                 )
150             except SkipTemplate:
151                 continue  # pragma: no cover
152             if content is None:
f3840e 153                 continue  # pragma: no cover
0e0cb7 154         already_exists = os.path.exists(dest_full)
CM 155         if already_exists:
0f2a11 156             with open(dest_full, 'rb') as f:
MM 157                 old_content = f.read()
0e0cb7 158             if old_content == content:
CM 159                 if verbosity:
0c29cf 160                     out(
MM 161                         '%s%s already exists (same content)' % (pad, dest_full)
162                     )
163                 continue  # pragma: no cover
0e0cb7 164             if interactive:
CM 165                 if not query_interactive(
0c29cf 166                     native_(full, fsenc),
MM 167                     native_(dest_full, fsenc),
168                     native_(content, fsenc),
169                     native_(old_content, fsenc),
170                     simulate=simulate,
171                     out_=out_,
172                 ):
0e0cb7 173                     continue
CM 174             elif not overwrite:
0c29cf 175                 continue  # pragma: no cover
0e0cb7 176         if verbosity and use_pkg_resources:
360eba 177             out('%sCopying %s to %s' % (pad, full, dest_full))
0e0cb7 178         elif verbosity:
360eba 179             out(
0c29cf 180                 '%sCopying %s to %s' % (pad, os.path.basename(full), dest_full)
MM 181             )
0e0cb7 182         if not simulate:
0f2a11 183             with open(dest_full, 'wb') as f:
MM 184                 f.write(content)
0c29cf 185
0e0cb7 186
CM 187 def should_skip_file(name):
188     """
189     Checks if a file should be skipped based on its name.
190
191     If it should be skipped, returns the reason, otherwise returns
192     None.
193     """
194     if name.startswith('.'):
195         return 'Skipping hidden file %(filename)s'
0f9823 196     if name.endswith(('~', '.bak')):
0e0cb7 197         return 'Skipping backup file %(filename)s'
0f9823 198     if name.endswith(('.pyc', '.pyo')):
74e69a 199         return 'Skipping %s file ' % os.path.splitext(name)[1] + '%(filename)s'
0e0cb7 200     if name.endswith('$py.class'):
CM 201         return 'Skipping $py.class file %(filename)s'
202     if name in ('CVS', '_darcs'):
203         return 'Skipping version control directory %(filename)s'
204     return None
205
0c29cf 206
0e0cb7 207 # Overridden on user's request:
CM 208 all_answer = None
209
0c29cf 210
MM 211 def query_interactive(
212     src_fn, dest_fn, src_content, dest_content, simulate, out_=sys.stdout
213 ):
360eba 214     def out(msg):
CM 215         out_.write(msg)
dfc127 216         out_.write('\n')
360eba 217         out_.flush()
0c29cf 218
0e0cb7 219     global all_answer
CM 220     from difflib import unified_diff, context_diff
0c29cf 221
MM 222     u_diff = list(
223         unified_diff(
224             dest_content.splitlines(),
225             src_content.splitlines(),
226             dest_fn,
227             src_fn,
228         )
229     )
230     c_diff = list(
231         context_diff(
232             dest_content.splitlines(),
233             src_content.splitlines(),
234             dest_fn,
235             src_fn,
236         )
237     )
238     added = len(
239         [l for l in u_diff if l.startswith('+') and not l.startswith('+++')]
240     )
241     removed = len(
242         [l for l in u_diff if l.startswith('-') and not l.startswith('---')]
243     )
0e0cb7 244     if added > removed:
25c64c 245         msg = '; %i lines added' % (added - removed)
0e0cb7 246     elif removed > added:
25c64c 247         msg = '; %i lines removed' % (removed - added)
0e0cb7 248     else:
CM 249         msg = ''
0c29cf 250     out(
MM 251         'Replace %i bytes with %i bytes (%i/%i lines changed%s)'
252         % (
253             len(dest_content),
254             len(src_content),
255             removed,
256             len(dest_content.splitlines()),
257             msg,
258         )
259     )
0e0cb7 260     prompt = 'Overwrite %s [y/n/d/B/?] ' % dest_fn
CM 261     while 1:
262         if all_answer is None:
263             response = input_(prompt).strip().lower()
264         else:
265             response = all_answer
266         if not response or response[0] == 'b':
267             import shutil
0c29cf 268
0e0cb7 269             new_dest_fn = dest_fn + '.bak'
CM 270             n = 0
271             while os.path.exists(new_dest_fn):
272                 n += 1
273                 new_dest_fn = dest_fn + '.bak' + str(n)
360eba 274             out('Backing up %s to %s' % (dest_fn, new_dest_fn))
0e0cb7 275             if not simulate:
CM 276                 shutil.copyfile(dest_fn, new_dest_fn)
277             return True
278         elif response.startswith('all '):
279             rest = response[4:].strip()
280             if not rest or rest[0] not in ('y', 'n', 'b'):
360eba 281                 out(query_usage)
0e0cb7 282                 continue
CM 283             response = all_answer = rest[0]
284         if response[0] == 'y':
285             return True
286         elif response[0] == 'n':
287             return False
288         elif response == 'dc':
360eba 289             out('\n'.join(c_diff))
0e0cb7 290         elif response[0] == 'd':
360eba 291             out('\n'.join(u_diff))
0e0cb7 292         else:
360eba 293             out(query_usage)
0e0cb7 294
0c29cf 295
0e0cb7 296 query_usage = """\
CM 297 Responses:
298   Y(es):    Overwrite the file with the new content.
299   N(o):     Do not overwrite the file.
300   D(iff):   Show a unified diff of the proposed changes (dc=context diff)
301   B(ackup): Save the current file contents to a .bak file
302             (and overwrite)
303   Type "all Y/N/B" to use Y/N/B for answer to all future questions
304 """
305
0c29cf 306
0e0cb7 307 def makedirs(dir, verbosity, pad):
CM 308     parent = os.path.dirname(os.path.abspath(dir))
309     if not os.path.exists(parent):
76c9c2 310         makedirs(parent, verbosity, pad)  # pragma: no cover
0e0cb7 311     os.mkdir(dir)
CM 312
0c29cf 313
0e0cb7 314 def substitute_filename(fn, vars):
CM 315     for var, value in vars.items():
316         fn = fn.replace('+%s+' % var, str(value))
317     return fn
318
0c29cf 319
MM 320 def substitute_content(
321     content, vars, filename='<string>', template_renderer=None
322 ):
0e0cb7 323     v = standard_vars.copy()
CM 324     v.update(vars)
325     return template_renderer(content, v, filename=filename)
0c29cf 326
0e0cb7 327
CM 328 def html_quote(s):
329     if s is None:
330         return ''
f3840e 331     return escape(str(s), 1)
0e0cb7 332
0c29cf 333
0e0cb7 334 def url_quote(s):
CM 335     if s is None:
336         return ''
f3840e 337     return compat_url_quote(str(s))
0c29cf 338
0e0cb7 339
CM 340 def test(conf, true_cond, false_cond=None):
341     if conf:
342         return true_cond
343     else:
344         return false_cond
0c29cf 345
0e0cb7 346
CM 347 def skip_template(condition=True, *args):
348     """
349     Raise SkipTemplate, which causes copydir to skip the template
350     being processed.  If you pass in a condition, only raise if that
351     condition is true (allows you to use this with string.Template)
352
353     If you pass any additional arguments, they will be used to
354     instantiate SkipTemplate (generally use like
355     ``skip_template(license=='GPL', 'Skipping file; not using GPL')``)
356     """
357     if condition:
358         raise SkipTemplate(*args)
359
0c29cf 360
0e0cb7 361 standard_vars = {
CM 362     'nothing': None,
363     'html_quote': html_quote,
364     'url_quote': url_quote,
365     'empty': '""',
366     'test': test,
367     'repr': repr,
368     'str': str,
369     'bool': bool,
370     'SkipTemplate': SkipTemplate,
371     'skip_template': skip_template,
0c29cf 372 }