0001
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
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
0042 if code.startswith('# -*- coding: '):
0043
0044
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
0056
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
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
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