0001# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
0002# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
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            # @@: Why is it a default to add a .py extension? 
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        # @@: This should actually be implemented
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        # @@: Should give error about egg_info when top_level.txt missing
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        # @@: This doesn't support deeper servlet directories,
0120        # or packages not kept at the top level.
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            # Erase extensions
0149            name = name[:-3]
0150        if '.' in name:
0151            # Turn into directory name:
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            # we either reached the parent-most directory, or we got
0181            # a relative directory
0182            # @@: Should we make sure we resolve relative directories
0183            # first?  Though presumably the current directory always
0184            # exists.
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                # File not found
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