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