0001
0002
0003import os
0004import glob
0005from paste.script import pluginlib, copydir
0006from paste.script.command import BadCommand
0007try:
0008 import subprocess
0009except ImportError:
0010 from paste.script.util import subprocess24 as subprocess
0011
0012class FileOp(object):
0013 """
0014 Enhance the ease of file copying/processing from a package into a target
0015 project
0016 """
0017
0018 def __init__(self, simulate=False,
0019 verbose=True,
0020 interactive=True,
0021 source_dir=None,
0022 template_vars=None):
0023 """
0024 Initialize our File operation helper object
0025
0026 source_dir
0027 Should refer to the directory within the package
0028 that contains the templates to be used for the other copy
0029 operations. It is assumed that packages will keep all their
0030 templates under a hierarchy starting here.
0031
0032 This should be an absolute path passed in, for example::
0033
0034 FileOp(source_dir=os.path.dirname(__file__) + '/templates')
0035 """
0036 self.simulate = simulate
0037 self.verbose = verbose
0038 self.interactive = interactive
0039 if template_vars is None:
0040 template_vars = {}
0041 self.template_vars = template_vars
0042 self.source_dir = source_dir
0043
0044 def copy_file(self, template, dest, filename=None, add_py=True, package=True):
0045 """
0046 Copy a file from the source location to somewhere in the
0047 destination.
0048
0049 template
0050 The filename underneath self.source_dir to copy/process
0051 dest
0052 The destination directory in the project relative to where
0053 this command is being run
0054 filename
0055 What to name the file in the target project, use the same name
0056 as the template if not provided
0057 add_py
0058 Add a .py extension to all files copied
0059 package
0060 Whether or not this file is part of a Python package, and any
0061 directories created should contain a __init__.py file as well.
0062
0063 """
0064 if not filename:
0065 filename = template.split('/')[0]
0066 if filename.endswith('_tmpl'):
0067 filename = filename[:-5]
0068 base_package, cdir = self.find_dir(dest, package)
0069 self.template_vars['base_package'] = base_package
0070 content = self.load_content(base_package, cdir, filename, template)
0071 if add_py:
0072
0073 filename = '%s.py' % filename
0074 dest = os.path.join(cdir, filename)
0075 self.ensure_file(dest, content, package)
0076
0077 def copy_dir(self, template_dir, dest, destname=None, package=True):
0078 """
0079 Copy a directory recursively, processing any files within it
0080 that need to be processed (end in _tmpl).
0081
0082 template_dir
0083 Directory under self.source_dir to copy/process
0084 dest
0085 Destination directory into which this directory will be copied
0086 to.
0087 destname
0088 Use this name instead of the original template_dir name for
0089 creating the directory
0090 package
0091 This directory will be a Python package and needs to have a
0092 __init__.py file.
0093 """
0094
0095 raise NotImplementedError
0096
0097 def load_content(self, base_package, base, name, template):
0098 blank = os.path.join(base, name + '.py')
0099 if not os.path.exists(blank):
0100 blank = os.path.join(self.source_dir,
0101 template)
0102 f = open(blank, 'r')
0103 content = f.read()
0104 f.close()
0105 if blank.endswith('_tmpl'):
0106 content = copydir.substitute_content(content, self.template_vars,
0107 filename=blank)
0108 return content
0109
0110 def find_dir(self, dirname, package=False):
0111 egg_info = pluginlib.find_egg_info_dir(os.getcwd())
0112
0113 f = open(os.path.join(egg_info, 'top_level.txt'))
0114 packages = [l.strip() for l in f.readlines()
0115 if l.strip() and not l.strip().startswith('#')]
0116 f.close()
0117 if not len(packages):
0118 raise BadCommand("No top level dir found for %s" % dirname)
0119
0120
0121 base = os.path.dirname(egg_info)
0122 possible = []
0123 for pkg in packages:
0124 d = os.path.join(base, pkg, dirname)
0125 if os.path.exists(d):
0126 possible.append((pkg, d))
0127 if not possible:
0128 self.ensure_dir(os.path.join(base, packages[0], dirname),
0129 package=package)
0130 return self.find_dir(dirname)
0131 if len(possible) > 1:
0132 raise BadCommand(
0133 "Multiple %s dirs found (%s)" % (dirname, possible))
0134 return possible[0]
0135
0136 def parse_path_name_args(self, name):
0137 """
0138 Given the name, assume that the first argument is a path/filename
0139 combination. Return the name and dir of this. If the name ends with
0140 '.py' that will be erased.
0141
0142 Examples:
0143 comments -> comments, ''
0144 admin/comments -> comments, 'admin'
0145 h/ab/fred -> fred, 'h/ab'
0146 """
0147 if name.endswith('.py'):
0148
0149 name = name[:-3]
0150 if '.' in name:
0151
0152 name = name.replace('.', os.path.sep)
0153 if '/' != os.path.sep:
0154 name = name.replace('/', os.path.sep)
0155 parts = name.split(os.path.sep)
0156 name = parts[-1]
0157 if not parts[:-1]:
0158 dir = ''
0159 elif len(parts[:-1]) == 1:
0160 dir = parts[0]
0161 else:
0162 dir = os.path.join(*parts[:-1])
0163 return name, dir
0164
0165 def ensure_dir(self, dir, svn_add=True, package=False):
0166 """
0167 Ensure that the directory exists, creating it if necessary.
0168 Respects verbosity and simulation.
0169
0170 Adds directory to subversion if ``.svn/`` directory exists in
0171 parent, and directory was created.
0172
0173 package
0174 If package is True, any directories created will contain a
0175 __init__.py file.
0176
0177 """
0178 dir = dir.rstrip(os.sep)
0179 if not dir:
0180
0181
0182
0183
0184
0185 return
0186 if not os.path.exists(dir):
0187 self.ensure_dir(os.path.dirname(dir), svn_add=svn_add, package=package)
0188 if self.verbose:
0189 print 'Creating %s' % self.shorten(dir)
0190 if not self.simulate:
0191 os.mkdir(dir)
0192 if (svn_add and
0193 os.path.exists(os.path.join(os.path.dirname(dir), '.svn'))):
0194 self.svn_command('add', dir)
0195 if package:
0196 initfile = os.path.join(dir, '__init__.py')
0197 f = open(initfile, 'wb')
0198 f.write("#\n")
0199 f.close()
0200 print 'Creating %s' % self.shorten(initfile)
0201 if (svn_add and
0202 os.path.exists(os.path.join(os.path.dirname(dir), '.svn'))):
0203 self.svn_command('add', initfile)
0204 else:
0205 if self.verbose > 1:
0206 print "Directory already exists: %s" % self.shorten(dir)
0207
0208 def ensure_file(self, filename, content, svn_add=True, package=False):
0209 """
0210 Ensure a file named ``filename`` exists with the given
0211 content. If ``--interactive`` has been enabled, this will ask
0212 the user what to do if a file exists with different content.
0213 """
0214 global difflib
0215 self.ensure_dir(os.path.dirname(filename), svn_add=svn_add, package=package)
0216 if not os.path.exists(filename):
0217 if self.verbose:
0218 print 'Creating %s' % filename
0219 if not self.simulate:
0220 f = open(filename, 'wb')
0221 f.write(content)
0222 f.close()
0223 if svn_add and os.path.exists(os.path.join(os.path.dirname(filename), '.svn')):
0224 self.svn_command('add', filename)
0225 return
0226 f = open(filename, 'rb')
0227 old_content = f.read()
0228 f.close()
0229 if content == old_content:
0230 if self.verbose > 1:
0231 print 'File %s matches expected content' % filename
0232 return
0233 if not self.options.overwrite:
0234 print 'Warning: file %s does not match expected content' % filename
0235 if difflib is None:
0236 import difflib
0237 diff = difflib.context_diff(
0238 content.splitlines(),
0239 old_content.splitlines(),
0240 'expected ' + filename,
0241 filename)
0242 print '\n'.join(diff)
0243 if self.interactive:
0244 while 1:
0245 s = raw_input(
0246 'Overwrite file with new content? [y/N] ').strip().lower()
0247 if not s:
0248 s = 'n'
0249 if s.startswith('y'):
0250 break
0251 if s.startswith('n'):
0252 return
0253 print 'Unknown response; Y or N please'
0254 else:
0255 return
0256
0257 if self.verbose:
0258 print 'Overwriting %s with new content' % filename
0259 if not self.simulate:
0260 f = open(filename, 'wb')
0261 f.write(content)
0262 f.close()
0263
0264 def shorten(self, fn, *paths):
0265 """
0266 Return a shorted form of the filename (relative to the current
0267 directory), typically for displaying in messages. If
0268 ``*paths`` are present, then use os.path.join to create the
0269 full filename before shortening.
0270 """
0271 if paths:
0272 fn = os.path.join(fn, *paths)
0273 if fn.startswith(os.getcwd()):
0274 return fn[len(os.getcwd()):].lstrip(os.path.sep)
0275 else:
0276 return fn
0277
0278 _svn_failed = False
0279
0280 def svn_command(self, *args, **kw):
0281 """
0282 Run an svn command, but don't raise an exception if it fails.
0283 """
0284 try:
0285 return self.run_command('svn', *args, **kw)
0286 except OSError, e:
0287 if not self._svn_failed:
0288 print 'Unable to run svn command (%s); proceeding anyway' % e
0289 self._svn_failed = True
0290
0291 def run_command(self, cmd, *args, **kw):
0292 """
0293 Runs the command, respecting verbosity and simulation.
0294 Returns stdout, or None if simulating.
0295 """
0296 cwd = popdefault(kw, 'cwd', os.getcwd())
0297 capture_stderr = popdefault(kw, 'capture_stderr', False)
0298 expect_returncode = popdefault(kw, 'expect_returncode', False)
0299 assert not kw, ("Arguments not expected: %s" % kw)
0300 if capture_stderr:
0301 stderr_pipe = subprocess.STDOUT
0302 else:
0303 stderr_pipe = subprocess.PIPE
0304 try:
0305 proc = subprocess.Popen([cmd] + list(args),
0306 cwd=cwd,
0307 stderr=stderr_pipe,
0308 stdout=subprocess.PIPE)
0309 except OSError, e:
0310 if e.errno != 2:
0311
0312 raise
0313 raise OSError(
0314 "The expected executable %s was not found (%s)"
0315 % (cmd, e))
0316 if self.verbose:
0317 print 'Running %s %s' % (cmd, ' '.join(args))
0318 if self.simulate:
0319 return None
0320 stdout, stderr = proc.communicate()
0321 if proc.returncode and not expect_returncode:
0322 if not self.verbose:
0323 print 'Running %s %s' % (cmd, ' '.join(args))
0324 print 'Error (exit code: %s)' % proc.returncode
0325 if stderr:
0326 print stderr
0327 raise OSError("Error executing command %s" % cmd)
0328 if self.verbose > 2:
0329 if stderr:
0330 print 'Command error output:'
0331 print stderr
0332 if stdout:
0333 print 'Command output:'
0334 print stdout
0335 return stdout
0336
0337def popdefault(dict, name, default=None):
0338 if name not in dict:
0339 return default
0340 else:
0341 v = dict[name]
0342 del dict[name]
0343 return v