[3] | 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 | |
---|