0001# -*- coding: utf-8 -*-
0002
0003"""Kid Compiler
0004
0005Compile XML to Python byte-code.
0006
0007"""
0008
0009__revision__ = "$Rev: 492 $"
0010__date__ = "$Date: 2007-07-06 21:38:45 -0400 (Fri, 06 Jul 2007) $"
0011__author__ = "Ryan Tomayko (rtomayko@gmail.com)"
0012__copyright__ = "Copyright 2004-2005, Ryan Tomayko"
0013__license__ = "MIT <http://www.opensource.org/licenses/mit-license.php>"
0014
0015import os
0016import os.path
0017import imp
0018import stat
0019import struct
0020import marshal
0021
0022import kid
0023from kid.codewriter import raise_template_error
0024
0025__all__ = ['KID_EXT', 'compile', 'compile_file', 'compile_dir']
0026
0027# kid filename extension
0028KID_EXT = ".kid"
0029
0030def actualize(code, dict=None):
0031    """Run code with variables in dict, updating the dict."""
0032    if dict is None:
0033        dict = {}
0034    exec code in dict
0035    return dict
0036
0037_py_compile = compile
0038def py_compile(code, filename='<string>', kind='exec'):
0039    """The Python built-in compile function with safeguard."""
0040    if type(code) == unicode:
0041        # unicode strings may not have a PEP 0263 encoding declaration
0042        if code.startswith('# -*- coding: '):
0043            # we want the line numbering to match with the source file,
0044            # so we only remove the magic word in the comment line:
0045            code = '# -*-' + code[13:]
0046    return _py_compile(code, filename, 'exec')
0047
0048def compile(source, filename='<string>', encoding=None, entity_map=None):
0049    """Compiles Kid XML source to a Python code object.
0050
0051    source   -- A file like object - must support read.
0052    filename -- An optional filename that is used
0053
0054    """
0055    # XXX all kinds of work to do here catching syntax errors and
0056    #     adjusting line numbers...
0057    py = kid.codewriter.parse(source, encoding, filename, entity_map)
0058    return py_compile(py, filename)
0059
0060
0061_timestamp = lambda filename : os.stat(filename)[stat.ST_MTIME]
0062
0063class KidFile(object):
0064    magic = imp.get_magic()
0065
0066    def __init__(self, kid_file, force=False,
0067            encoding=None, strip_dest_dir=None, entity_map=None):
0068        self.kid_file = kid_file
0069        self.py_file = os.path.splitext(kid_file)[0] + '.py'
0070        self.strip_dest_dir = strip_dest_dir
0071        self.pyc_file = self.py_file + 'c'
0072        self.encoding = encoding
0073        self.entity_map = entity_map
0074        fp = None
0075        if force:
0076            stale = True
0077        else:
0078            stale = False
0079            try:
0080                fp = open(self.pyc_file, "rb")
0081            except IOError:
0082                stale = True
0083            else:
0084                if fp.read(4) != self.magic:
0085                    stale = True
0086                else:
0087                    mtime = struct.unpack('<I', fp.read(4))[0]
0088                    kid_mtime = _timestamp(kid_file)
0089                    if kid_mtime is None or mtime < kid_mtime:
0090                        stale = True
0091        self.stale = stale
0092        self._pyc_fp = fp
0093        self._python = None
0094        self._code = None
0095
0096    def compile(self, dump_code=True, dump_source=False):
0097        if dump_source:
0098            self.dump_source()
0099        code = self.code
0100        if dump_code and self.stale:
0101            self.dump_code()
0102        return code
0103
0104    def code(self):
0105        """Get the compiled Python code for the template."""
0106        if self._code is None:
0107            if self.stale:
0108                pyfile = self.py_file
0109                if self.strip_dest_dir and                      self.py_file.startswith(self.strip_dest_dir):
0111                    pyfile = os.path.normpath(
0112                        self.py_file[len(self.strip_dest_dir):])
0113                try:
0114                    self._code = py_compile(self.python, pyfile)
0115                except Exception:
0116                    raise_template_error(filename=self.kid_file,
0117                        encoding=self.encoding)
0118            else:
0119                self._code = marshal.load(self._pyc_fp)
0120        return self._code
0121    code = property(code)
0122
0123    def python(self):
0124        """Get the Python source for the template."""
0125        if self._python is None:
0126            py = kid.codewriter.parse_file(self.kid_file,
0127                self.encoding, self.entity_map)
0128            self._python = py
0129        return self._python
0130    python = property(python)
0131
0132    def dump_source(self, file=None):
0133        py = self.python
0134        encoding = self.encoding or 'utf-8'
0135        file = file or self.py_file
0136        fp = _maybe_open(file, 'wb')
0137        if fp:
0138            try:
0139                try:
0140                    fp.write(py.encode(encoding))
0141                finally:
0142                    fp.close()
0143            except IOError:
0144                _maybe_remove(file)
0145            else:
0146                return True
0147        return False
0148
0149    def dump_code(self, file=None):
0150        code = self.code
0151        file = file or self.pyc_file
0152        fp = _maybe_open(file, 'wb')
0153        if fp:
0154            try:
0155                try:
0156                    if self.kid_file:
0157                        mtime = os.stat(self.kid_file)[stat.ST_MTIME]
0158                    else:
0159                        mtime = 0
0160                    fp.write('\0\0\0\0')
0161                    fp.write(struct.pack('<I', mtime))
0162                    marshal.dump(code, fp)
0163                    fp.flush()
0164                    fp.seek(0)
0165                    fp.write(self.magic)
0166                finally:
0167                    fp.close()
0168            except IOError:
0169                _maybe_remove(file)
0170            else:
0171                return True
0172        return False
0173
0174def _maybe_open(f, mode):
0175    if isinstance(f, basestring):
0176        try:
0177            f = open(f, mode)
0178        except IOError:
0179            f = None
0180    return f
0181
0182def _maybe_remove(f):
0183    if isinstance(f, basestring):
0184        try:
0185            os.remove(f)
0186        except OSError:
0187            pass
0188
0189#
0190# functions for compiling files directly and the kidc utility
0191#
0192
0193def compile_file(file, force=False, source=False, encoding=None,
0194        strip_dest_dir=None, entity_map=None):
0195    """Compile the file specified.
0196
0197    Return True if the file was compiled, False if the compiled file already
0198    exists and is up-to-date.
0199
0200    """
0201    template = KidFile(file, force, encoding, strip_dest_dir, entity_map)
0202    if template.stale:
0203        template.compile(dump_source=source)
0204        return True
0205    else:
0206        return False
0207
0208def compile_dir(dir, maxlevels=10, force=False, source=False,
0209        encoding=None, strip_dest_dir=None, entity_map=None):
0210    """Byte-compile all kid modules in the given directory tree.
0211
0212    Keyword Arguments: (only dir is required)
0213    dir       -- the directory to byte-compile
0214    maxlevels -- maximum recursion level (default 10)
0215    force     -- if True, force compilation, even if timestamps are up-to-date.
0216    source    -- if True, dump python source (.py) files along with .pyc files.
0217
0218    Yields tuples (stat, filename) where stat is either an error message,
0219    True if the file was compiled or False if the file did not need to be compiled.
0220
0221    """
0222    names = os.listdir(dir)
0223    names.sort()
0224    ext_len = len(KID_EXT)
0225    for name in names:
0226        fullname = os.path.join(dir, name)
0227        if os.path.isfile(fullname):
0228            ext = name[-ext_len:]
0229            if ext == KID_EXT:
0230                try:
0231                    stat = compile_file(fullname, force, source,
0232                        encoding, strip_dest_dir, entity_map)
0233                except Exception, e:
0234                    # TODO: grab the traceback and yield it with the other stuff
0235                    stat = e
0236                yield stat, fullname
0237        elif maxlevels > 0 and name != os.curdir and name != os.pardir                   and os.path.isdir(fullname) and not os.path.islink(fullname):
0239            for res in compile_dir(fullname, maxlevels - 1, force, source,
0240                    encoding, strip_dest_dir, entity_map):
0241                yield res