view makeitso/makeitso.py @ 55:b087a14a664b

fix several issues involving variable interpolation
author Jeff Hammel <jhammel@mozilla.com>
date Thu, 06 Jan 2011 14:15:19 -0800
parents d3e1e5745f24
children 728cae02a6ed
line wrap: on
line source

#!/usr/bin/env python

"""
filesystem template interpreter
"""

import inspect
import os
import re
import shutil
import subprocess
import sys
import tempfile
import urllib
# TODO: may have to use urllib2.urlopen to get reasonable timeouts

from optparse import OptionParser

# URL of -this file-
location = 'http://k0s.org/mozilla/hg/MakeItSo/raw-file/tip/makeitso/makeitso.py'

### import tempita

# URL of tempita
tempita_location = 'http://bitbucket.org/ianb/tempita/raw-file/tip/tempita/'

def cleanup():
    # remove temporary net module directory
    if 'tempdir' in globals():
        shutil.remove(tempdir)
try:
    import tempita
except ImportError:

    # Get tempita from the net
    # TODO: abstract this to get arbitrary modules from the net
    def getFiles(url, subdir, files):
        """
        fetch files from the internet
        - url : base url
        - subdirectory: to put things in
        - files : list of files to retrieve
        returns the location of a temporary directory
        """
        globals()['tempdir'] = tempfile.mkdtemp()
        os.mkdir(subdir)
        url = url.rstrip('/')
        for filename in files:
            f, headers = urllib.urlretrive('%s/%s' % (url, filename))
            content = file(f).read()
            outfile = os.path.join(globals()['tempdir'], subdir, filename)
            o = file(outfile, 'w')
            print >> o, content
        return globals()['tempdir']

    tempita_files = ['__init__.py', '_looper.py', 'compat3.py']

    try:
        t = getFiles(tempita_location, 'tempita', tempita_files)
        sys.path.append(t)
        import tempita
    except:
        cleanup()
        raise NotImplementedError('This should say something like youre not connected to the net')

# does tempita support delimeters?
has_delimeters = 'delimeters' in inspect.getargspec(tempita.Template.__init__).args

# regular expressions for finding the shebang
shebang_re = '#!.*makeitso.*'
shebang_re = re.compile(shebang_re)

### URIs

def parent_uri(uri):
    """parent resource of a URI"""
    
    if '://' in uri:
        return uri.rsplit('/', 1)[0] + '/'
    else:
        here = os.path.dirname(os.path.abspath(uri))
        here = here.rstrip(os.path.sep) + os.path.sep
        return here

def basename(uri):
    """return the basename of a URI"""
    if '://' in uri:
        return uri.rsplit('/', 1)[1]
    else:
        return os.path.basename(uri)

def include(uri):
    f, headers = urllib.urlretrieve(uri) # XXX -> urllib2 for timeout
    return file(f).read()

### things that deal with variables

class MissingVariablesException(Exception):
    """exception for (non-interactive) missing variables"""
    def __init__(self, missing):
        self.missing = missing
        message = 'Missing variables: %s' % ', '.join(missing)
        Exception.__init__(self, message)

def get_missing(name_error):
    """
    This is a horrible hack because python doesn't do the proper thing
    via eval and return the name of the variable;  instead, it just gives
    you a message:
    >>> try:
    ...   eval('2*foo')
    ... except Exception, e:
    ...   pass
    """
    message = name_error.args[0]
    varname = message.split("'")[1]
    return varname

### template classes

class ContentTemplate(tempita.Template):
    """MakeItSo's extension of tempita's Template class"""
    
    defaults = {'include': include}

    def __init__(self, content, name=None, interactive=True, variables=None):

        # default variables
        self.defaults = self.__class__.defaults.copy()
        self.defaults.update(variables or {})

        # TODO: automagically tell if the program is interactive or not
        self.interactive = interactive
        
        tempita.Template.__init__(self, content, name=name)

    def missing(self, **variables):
        """return additional variables needed"""
        vars = self.get_variables(**variables)
        missing = set([])
        while True:
            try:
                tempita.Template.substitute(self, **vars)
                return missing
            except NameError, e:
                missed = get_missing(e)
                missing.add(missed)
                vars[missed] = ''
        return missing

    def get_variables(self, **variables):
        vars = self.defaults.copy()
        vars.update(variables)
        return vars

    def check_missing(self, vars):
        """
        check for missing variables and, if applicable,
        update them from the command line
        """
        missing = self.missing(**vars)
        if missing:
            if self.interactive:
                vars.update(self.read_variables(missing))
            else:
                raise MissingVariablesException(missing)
        
    
    def variables(self):
        """return the variables needed for a template"""
        return self.missing()

    def substitute(self, **variables):
        """interactive (for now) substitution"""
        vars = self.get_variables(**variables)
        self.check_missing(vars)
        return self._substitute(**vars)

    def _substitute(self, **variables):
        return tempita.Template.substitute(self, **variables)

    def read_variables(self, variables):
        """read variables from stdin"""
        # TODO: variables should (optionally) be richer objects
        retval = {}
        for i in variables:
            print 'Enter %s: ' % i,
            retval[i] = raw_input()
        return retval


class URITemplate(ContentTemplate):
    """template for a file or URL"""

    def __init__(self, uri, output=None, interactive=True, variables=None):
        self.output = output or sys.stdout        
        content = include(uri)
        
        # remove makeitso shebang if it has one
        if shebang_re.match(content):
            content = os.linesep.join(content.splitlines()[1:])

        variables['here'] = parent_uri(uri)
        ContentTemplate.__init__(self, content, name=uri,
                                 interactive=interactive,
                                 variables=variables)

    def substitute(self, **variables):
        output = ContentTemplate.substitute(self, **variables)
        f = self.output
        
        if isinstance(f, basestring):
            if os.path.isdir(f):
                f = os.path.join(f, basename(self.name))
            f = file(f, 'w')
            print >> f, output
            f.close()
        else:
            print >> f, output

class DirectoryTemplate(ContentTemplate):
    """template for a directory structure"""
    
    def __init__(self, directory, output=None, interactive=True, variables=None):
        """
        - output : output directory; if None will render in place
        """
        assert os.path.isdir(directory)
        self.name = directory
        self.interactive = interactive
        self.output = output
        if output is not None:
            if os.path.exists(output):
                assert os.path.isdir(output), "%s: Must be a directory" % self.name
        self.defaults = ContentTemplate.defaults.copy()
        self.defaults.update(variables or {})


    def missing(self, **variables):
        vars = self.defaults.copy()
        vars.update(variables)
        missing = set([])
        for dirpath, dirnames, filenames in os.walk(self.name):

            # find variables from directory names
            for d in dirnames:
                missed = ContentTemplate(d).missing(**vars)
                missing.update(missed)
                variables.update(dict([(i, '') for i in missed]))

            # find variables from files
            for f in filenames:
                path = os.path.join(dirpath, f)
                template = URITemplate(path, interactive=self.interactive)
                missed = template.missing(**vars)
                missing.update(missed)
                variables.update(dict([(i, '') for i in missed]))

        return missing

    def _substitute(self, **variables):
        # TODO: do this with recursion instead of os.walk so that
        # per-directory control may be asserted

        # make output directory if necessary
        output = self.output
        if output and not os.path.exists(output):
            os.makedirs(output)
            
        for dirname, dirnames, filenames in os.walk(self.name):
            
            # interpolate directory names
            for d in dirnames:
                path = os.path.join(dirname, d)
                interpolated = ContentTemplate(path).substitute(**variables)
                target = os.path.join(self.output, interpolated.split(self.name, 1)[-1].strip(os.path.sep))
                
                if os.path.exists(target):
                    # ensure its a directory
                    # TODO: check this first before interpolation is in progress
                    assert os.path.isdir(target), "Can't substitute a directory on top of the file"
                else:
                    os.makedirs(target)

            # interpolate files
            for filename in filenames:
                path = os.path.join(dirname, filename)
                interpolated = ContentTemplate(path).substitute(**variables)
                target = os.path.join(self.output, interpolated.split(self.name, 1)[-1].strip(os.path.sep))
                
                if os.path.exists(target):
                    # ensure its a directory
                    # TODO: check this first before interpolation is in progress
                    assert os.path.isfile(target), "Can't substitute a file on top of a directory"
                template = URITemplate(path, output=target, interactive=False)
                template.substitute(**variables)


class PolyTemplate(ContentTemplate):
    """template for several files/directories"""

    def __init__(self, templates, output=None, interactive=True, variables=None):

        assert templates, "No templates given!"

        self.interactive = interactive
        self._templates = templates[:]
        self.templates = []
        self.output = output
        for template in templates:
            # TODO: check if the template is a [e.g] PasteScript.template entry point
            if os.path.isdir(template):
                self.templates.append(DirectoryTemplate(template, interactive=self.interactive, output=output, variables=variables))
            else:
                self.templates.append(URITemplate(template, interactive=self.interactive, output=output, variables=variables))

    def missing(self, **variables):
        vars = variables.copy()
        missing = set([])
        for template in self.templates:
            missed = template.missing(**vars)
            missing.update(missed)
            vars.update(dict([(i, '') for i in missed]))
        return missing

    def _substitute(self, **variables):

        # determine where the hell to put these things
        if self.output is None:
            dirs = [i for i in self._templates if os.path.isdir(i)]
            if not ((len(dirs) == 0) or len(dirs) == len(self.templates)):
                raise AssertionError("Must specify output when mixing directories and URIs")
        
        # TODO: check for missing
        if len(self.templates) > 1 and not os.path.exists(self.output):
            os.makedirs(self.output)
        for template in self.templates:
            template.substitute(**variables)
        
### command line interface

def invocation(url, **variables):
    """returns a string appropriate for TTW invocation"""
    variables_string = ' '.join(['%s=%s' % (i,j) for i,j in variables.items()])
    return 'python <(curl %s) %s %s' % (location, url, variables_string)

def main(args=sys.argv[1:]):

    # create option parser
    usage = '%prog [options] template <template> <...>'
    parser = OptionParser(usage, description=__doc__)

    # delimeters
    # XXX needs tempita trunk
    if has_delimeters:
        parser.add_option('-[', '--start-braces', dest='start_braces',
                          help='starting delimeter')
        parser.add_option('-]', '--end-braces', dest='end_braces',
                          help='starting delimeter')

    # options about where to put things
    parser.add_option('-o', '--output', dest='output',
                      help='where to put the output (filename or directory)')

    # options for getting information
    parser.add_option('--commandline', dest='commandline',
                      action='store_true', default=False,
                      help="print the commandline to invoke this script TTW")
    parser.add_option('--variables', dest='variables',
                      action='store_true', default=False,
                      help='print the variables in a template')
    
    options, args = parser.parse_args(args)

    # print the variables for the templates
    if options.variables:

        # makes no sense without a template
        if not args:
            parser.print_usage()
            parser.exit()

        # find all variables
        template = PolyTemplate(templates=args)
        variables = template.variables()

        # print them
        for variable in sorted(variables):
            print variable
        return

    # template variables
    variables = {}
    _args = []
    for arg in args:
        if '=' in arg:
            key, value = arg.split('=')
            variables[key] = value
        else:
            _args.append(arg)
    args = _args

    # print TTW commandline for invocation
    if options.commandline:
        if args:
            for arg in args:
                print invocation(arg, **variables)
        else:
            print invocation('[URI]', **variables)
        return

    # get the content
    if args:
        template = PolyTemplate(templates=args,
                                output=options.output,
                                variables=variables)
    else:
        template = ContentTemplate(sys.stdin.read(), variables=variables)
    template.substitute()

    # cleanup
    cleanup()
        
if __name__ == '__main__':
    main()