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 glob |
---|
5 | import pkg_resources |
---|
6 | from paste.script import pluginlib, copydir |
---|
7 | from paste.script.command import BadCommand |
---|
8 | difflib = None |
---|
9 | try: |
---|
10 | import subprocess |
---|
11 | except ImportError: |
---|
12 | from paste.script.util import subprocess24 as subprocess |
---|
13 | |
---|
14 | class FileOp(object): |
---|
15 | """ |
---|
16 | Enhance the ease of file copying/processing from a package into a target |
---|
17 | project |
---|
18 | """ |
---|
19 | |
---|
20 | def __init__(self, simulate=False, |
---|
21 | verbose=True, |
---|
22 | interactive=True, |
---|
23 | source_dir=None, |
---|
24 | template_vars=None): |
---|
25 | """ |
---|
26 | Initialize our File operation helper object |
---|
27 | |
---|
28 | source_dir |
---|
29 | Should refer to the directory within the package |
---|
30 | that contains the templates to be used for the other copy |
---|
31 | operations. It is assumed that packages will keep all their |
---|
32 | templates under a hierarchy starting here. |
---|
33 | |
---|
34 | This should be an absolute path passed in, for example:: |
---|
35 | |
---|
36 | FileOp(source_dir=os.path.dirname(__file__) + '/templates') |
---|
37 | """ |
---|
38 | self.simulate = simulate |
---|
39 | self.verbose = verbose |
---|
40 | self.interactive = interactive |
---|
41 | if template_vars is None: |
---|
42 | template_vars = {} |
---|
43 | self.template_vars = template_vars |
---|
44 | self.source_dir = source_dir |
---|
45 | self.use_pkg_resources = isinstance(source_dir, tuple) |
---|
46 | |
---|
47 | def copy_file(self, template, dest, filename=None, add_py=True, package=True, |
---|
48 | template_renderer=None): |
---|
49 | """ |
---|
50 | Copy a file from the source location to somewhere in the |
---|
51 | destination. |
---|
52 | |
---|
53 | template |
---|
54 | The filename underneath self.source_dir to copy/process |
---|
55 | dest |
---|
56 | The destination directory in the project relative to where |
---|
57 | this command is being run |
---|
58 | filename |
---|
59 | What to name the file in the target project, use the same name |
---|
60 | as the template if not provided |
---|
61 | add_py |
---|
62 | Add a .py extension to all files copied |
---|
63 | package |
---|
64 | Whether or not this file is part of a Python package, and any |
---|
65 | directories created should contain a __init__.py file as well. |
---|
66 | template_renderer |
---|
67 | An optional template renderer |
---|
68 | |
---|
69 | """ |
---|
70 | if not filename: |
---|
71 | filename = template.split('/')[0] |
---|
72 | if filename.endswith('_tmpl'): |
---|
73 | filename = filename[:-5] |
---|
74 | base_package, cdir = self.find_dir(dest, package) |
---|
75 | self.template_vars['base_package'] = base_package |
---|
76 | content = self.load_content(base_package, cdir, filename, template, |
---|
77 | template_renderer=template_renderer) |
---|
78 | if add_py: |
---|
79 | # @@: Why is it a default to add a .py extension? |
---|
80 | filename = '%s.py' % filename |
---|
81 | dest = os.path.join(cdir, filename) |
---|
82 | self.ensure_file(dest, content, package) |
---|
83 | |
---|
84 | def copy_dir(self, template_dir, dest, destname=None, package=True): |
---|
85 | """ |
---|
86 | Copy a directory recursively, processing any files within it |
---|
87 | that need to be processed (end in _tmpl). |
---|
88 | |
---|
89 | template_dir |
---|
90 | Directory under self.source_dir to copy/process |
---|
91 | dest |
---|
92 | Destination directory into which this directory will be copied |
---|
93 | to. |
---|
94 | destname |
---|
95 | Use this name instead of the original template_dir name for |
---|
96 | creating the directory |
---|
97 | package |
---|
98 | This directory will be a Python package and needs to have a |
---|
99 | __init__.py file. |
---|
100 | """ |
---|
101 | # @@: This should actually be implemented |
---|
102 | raise NotImplementedError |
---|
103 | |
---|
104 | def load_content(self, base_package, base, name, template, |
---|
105 | template_renderer=None): |
---|
106 | blank = os.path.join(base, name + '.py') |
---|
107 | read_content = True |
---|
108 | if not os.path.exists(blank): |
---|
109 | if self.use_pkg_resources: |
---|
110 | fullpath = '/'.join([self.source_dir[1], template]) |
---|
111 | content = pkg_resources.resource_string( |
---|
112 | self.source_dir[0], fullpath) |
---|
113 | read_content = False |
---|
114 | blank = fullpath |
---|
115 | else: |
---|
116 | blank = os.path.join(self.source_dir, |
---|
117 | template) |
---|
118 | if read_content: |
---|
119 | f = open(blank, 'r') |
---|
120 | content = f.read() |
---|
121 | f.close() |
---|
122 | if blank.endswith('_tmpl'): |
---|
123 | content = copydir.substitute_content( |
---|
124 | content, self.template_vars, filename=blank, |
---|
125 | template_renderer=template_renderer) |
---|
126 | return content |
---|
127 | |
---|
128 | def find_dir(self, dirname, package=False): |
---|
129 | egg_info = pluginlib.find_egg_info_dir(os.getcwd()) |
---|
130 | # @@: Should give error about egg_info when top_level.txt missing |
---|
131 | f = open(os.path.join(egg_info, 'top_level.txt')) |
---|
132 | packages = [l.strip() for l in f.readlines() |
---|
133 | if l.strip() and not l.strip().startswith('#')] |
---|
134 | f.close() |
---|
135 | if not len(packages): |
---|
136 | raise BadCommand("No top level dir found for %s" % dirname) |
---|
137 | # @@: This doesn't support deeper servlet directories, |
---|
138 | # or packages not kept at the top level. |
---|
139 | base = os.path.dirname(egg_info) |
---|
140 | possible = [] |
---|
141 | for pkg in packages: |
---|
142 | d = os.path.join(base, pkg, dirname) |
---|
143 | if os.path.exists(d): |
---|
144 | possible.append((pkg, d)) |
---|
145 | if not possible: |
---|
146 | self.ensure_dir(os.path.join(base, packages[0], dirname), |
---|
147 | package=package) |
---|
148 | return self.find_dir(dirname) |
---|
149 | if len(possible) > 1: |
---|
150 | raise BadCommand( |
---|
151 | "Multiple %s dirs found (%s)" % (dirname, possible)) |
---|
152 | return possible[0] |
---|
153 | |
---|
154 | def parse_path_name_args(self, name): |
---|
155 | """ |
---|
156 | Given the name, assume that the first argument is a path/filename |
---|
157 | combination. Return the name and dir of this. If the name ends with |
---|
158 | '.py' that will be erased. |
---|
159 | |
---|
160 | Examples: |
---|
161 | comments -> comments, '' |
---|
162 | admin/comments -> comments, 'admin' |
---|
163 | h/ab/fred -> fred, 'h/ab' |
---|
164 | """ |
---|
165 | if name.endswith('.py'): |
---|
166 | # Erase extensions |
---|
167 | name = name[:-3] |
---|
168 | if '.' in name: |
---|
169 | # Turn into directory name: |
---|
170 | name = name.replace('.', os.path.sep) |
---|
171 | if '/' != os.path.sep: |
---|
172 | name = name.replace('/', os.path.sep) |
---|
173 | parts = name.split(os.path.sep) |
---|
174 | name = parts[-1] |
---|
175 | if not parts[:-1]: |
---|
176 | dir = '' |
---|
177 | elif len(parts[:-1]) == 1: |
---|
178 | dir = parts[0] |
---|
179 | else: |
---|
180 | dir = os.path.join(*parts[:-1]) |
---|
181 | return name, dir |
---|
182 | |
---|
183 | def ensure_dir(self, dir, svn_add=True, package=False): |
---|
184 | """ |
---|
185 | Ensure that the directory exists, creating it if necessary. |
---|
186 | Respects verbosity and simulation. |
---|
187 | |
---|
188 | Adds directory to subversion if ``.svn/`` directory exists in |
---|
189 | parent, and directory was created. |
---|
190 | |
---|
191 | package |
---|
192 | If package is True, any directories created will contain a |
---|
193 | __init__.py file. |
---|
194 | |
---|
195 | """ |
---|
196 | dir = dir.rstrip(os.sep) |
---|
197 | if not dir: |
---|
198 | # we either reached the parent-most directory, or we got |
---|
199 | # a relative directory |
---|
200 | # @@: Should we make sure we resolve relative directories |
---|
201 | # first? Though presumably the current directory always |
---|
202 | # exists. |
---|
203 | return |
---|
204 | if not os.path.exists(dir): |
---|
205 | self.ensure_dir(os.path.dirname(dir), svn_add=svn_add, package=package) |
---|
206 | if self.verbose: |
---|
207 | print 'Creating %s' % self.shorten(dir) |
---|
208 | if not self.simulate: |
---|
209 | os.mkdir(dir) |
---|
210 | if (svn_add and |
---|
211 | os.path.exists(os.path.join(os.path.dirname(dir), '.svn'))): |
---|
212 | self.svn_command('add', dir) |
---|
213 | if package: |
---|
214 | initfile = os.path.join(dir, '__init__.py') |
---|
215 | f = open(initfile, 'wb') |
---|
216 | f.write("#\n") |
---|
217 | f.close() |
---|
218 | print 'Creating %s' % self.shorten(initfile) |
---|
219 | if (svn_add and |
---|
220 | os.path.exists(os.path.join(os.path.dirname(dir), '.svn'))): |
---|
221 | self.svn_command('add', initfile) |
---|
222 | else: |
---|
223 | if self.verbose > 1: |
---|
224 | print "Directory already exists: %s" % self.shorten(dir) |
---|
225 | |
---|
226 | def ensure_file(self, filename, content, svn_add=True, package=False): |
---|
227 | """ |
---|
228 | Ensure a file named ``filename`` exists with the given |
---|
229 | content. If ``--interactive`` has been enabled, this will ask |
---|
230 | the user what to do if a file exists with different content. |
---|
231 | """ |
---|
232 | global difflib |
---|
233 | self.ensure_dir(os.path.dirname(filename), svn_add=svn_add, package=package) |
---|
234 | if not os.path.exists(filename): |
---|
235 | if self.verbose: |
---|
236 | print 'Creating %s' % filename |
---|
237 | if not self.simulate: |
---|
238 | f = open(filename, 'wb') |
---|
239 | f.write(content) |
---|
240 | f.close() |
---|
241 | if svn_add and os.path.exists(os.path.join(os.path.dirname(filename), '.svn')): |
---|
242 | self.svn_command('add', filename) |
---|
243 | return |
---|
244 | f = open(filename, 'rb') |
---|
245 | old_content = f.read() |
---|
246 | f.close() |
---|
247 | if content == old_content: |
---|
248 | if self.verbose > 1: |
---|
249 | print 'File %s matches expected content' % filename |
---|
250 | return |
---|
251 | if self.interactive: |
---|
252 | print 'Warning: file %s does not match expected content' % filename |
---|
253 | if difflib is None: |
---|
254 | import difflib |
---|
255 | diff = difflib.context_diff( |
---|
256 | content.splitlines(), |
---|
257 | old_content.splitlines(), |
---|
258 | 'expected ' + filename, |
---|
259 | filename) |
---|
260 | print '\n'.join(diff) |
---|
261 | if self.interactive: |
---|
262 | while 1: |
---|
263 | s = raw_input( |
---|
264 | 'Overwrite file with new content? [y/N] ').strip().lower() |
---|
265 | if not s: |
---|
266 | s = 'n' |
---|
267 | if s.startswith('y'): |
---|
268 | break |
---|
269 | if s.startswith('n'): |
---|
270 | return |
---|
271 | print 'Unknown response; Y or N please' |
---|
272 | else: |
---|
273 | return |
---|
274 | |
---|
275 | if self.verbose: |
---|
276 | print 'Overwriting %s with new content' % filename |
---|
277 | if not self.simulate: |
---|
278 | f = open(filename, 'wb') |
---|
279 | f.write(content) |
---|
280 | f.close() |
---|
281 | |
---|
282 | def shorten(self, fn, *paths): |
---|
283 | """ |
---|
284 | Return a shorted form of the filename (relative to the current |
---|
285 | directory), typically for displaying in messages. If |
---|
286 | ``*paths`` are present, then use os.path.join to create the |
---|
287 | full filename before shortening. |
---|
288 | """ |
---|
289 | if paths: |
---|
290 | fn = os.path.join(fn, *paths) |
---|
291 | if fn.startswith(os.getcwd()): |
---|
292 | return fn[len(os.getcwd()):].lstrip(os.path.sep) |
---|
293 | else: |
---|
294 | return fn |
---|
295 | |
---|
296 | _svn_failed = False |
---|
297 | |
---|
298 | def svn_command(self, *args, **kw): |
---|
299 | """ |
---|
300 | Run an svn command, but don't raise an exception if it fails. |
---|
301 | """ |
---|
302 | try: |
---|
303 | return self.run_command('svn', *args, **kw) |
---|
304 | except OSError, e: |
---|
305 | if not self._svn_failed: |
---|
306 | print 'Unable to run svn command (%s); proceeding anyway' % e |
---|
307 | self._svn_failed = True |
---|
308 | |
---|
309 | def run_command(self, cmd, *args, **kw): |
---|
310 | """ |
---|
311 | Runs the command, respecting verbosity and simulation. |
---|
312 | Returns stdout, or None if simulating. |
---|
313 | """ |
---|
314 | cwd = popdefault(kw, 'cwd', os.getcwd()) |
---|
315 | capture_stderr = popdefault(kw, 'capture_stderr', False) |
---|
316 | expect_returncode = popdefault(kw, 'expect_returncode', False) |
---|
317 | assert not kw, ("Arguments not expected: %s" % kw) |
---|
318 | if capture_stderr: |
---|
319 | stderr_pipe = subprocess.STDOUT |
---|
320 | else: |
---|
321 | stderr_pipe = subprocess.PIPE |
---|
322 | try: |
---|
323 | proc = subprocess.Popen([cmd] + list(args), |
---|
324 | cwd=cwd, |
---|
325 | stderr=stderr_pipe, |
---|
326 | stdout=subprocess.PIPE) |
---|
327 | except OSError, e: |
---|
328 | if e.errno != 2: |
---|
329 | # File not found |
---|
330 | raise |
---|
331 | raise OSError( |
---|
332 | "The expected executable %s was not found (%s)" |
---|
333 | % (cmd, e)) |
---|
334 | if self.verbose: |
---|
335 | print 'Running %s %s' % (cmd, ' '.join(args)) |
---|
336 | if self.simulate: |
---|
337 | return None |
---|
338 | stdout, stderr = proc.communicate() |
---|
339 | if proc.returncode and not expect_returncode: |
---|
340 | if not self.verbose: |
---|
341 | print 'Running %s %s' % (cmd, ' '.join(args)) |
---|
342 | print 'Error (exit code: %s)' % proc.returncode |
---|
343 | if stderr: |
---|
344 | print stderr |
---|
345 | raise OSError("Error executing command %s" % cmd) |
---|
346 | if self.verbose > 2: |
---|
347 | if stderr: |
---|
348 | print 'Command error output:' |
---|
349 | print stderr |
---|
350 | if stdout: |
---|
351 | print 'Command output:' |
---|
352 | print stdout |
---|
353 | return stdout |
---|
354 | |
---|
355 | def popdefault(dict, name, default=None): |
---|
356 | if name not in dict: |
---|
357 | return default |
---|
358 | else: |
---|
359 | v = dict[name] |
---|
360 | del dict[name] |
---|
361 | return v |
---|
362 | |
---|