Added experimental and undocumented bytecode cache support

--HG--
branch : trunk
diff --git a/jinja2/bccache.py b/jinja2/bccache.py
new file mode 100644
index 0000000..50532cd
--- /dev/null
+++ b/jinja2/bccache.py
@@ -0,0 +1,156 @@
+# -*- coding: utf-8 -*-
+"""
+    jinja2.bccache
+    ~~~~~~~~~~~~~~
+
+    This module implements the bytecode cache system Jinja is optionally
+    using.  This is useful if you have very complex template situations and
+    the compiliation of all those templates slow down your application too
+    much.
+
+    Situations where this is useful are often forking web applications that
+    are initialized on the first request.
+
+    :copyright: Copyright 2008 by Armin Ronacher.
+    :license: BSD.
+"""
+from os import path
+import marshal
+import cPickle as pickle
+from cStringIO import StringIO
+try:
+    from hashlib import sha1
+except ImportError:
+    from sha import new as sha1
+
+
+bc_version = 1
+bc_magic = 'j2' + pickle.dumps(bc_version, 2)
+
+
+class Bucket(object):
+    """Buckets are used to store the bytecode for one template.  It's
+    initialized by the bytecode cache with the checksum for the code
+    as well as the unique key.
+
+    The bucket then provides method to load the bytecode from file(-like)
+    objects and strings or dump it again.
+    """
+
+    def __init__(self, cache, environment, key, checksum):
+        self._cache = cache
+        self.environment = environment
+        self.key = key
+        self.checksum = checksum
+        self.reset()
+
+    def reset(self):
+        """Resets the bucket (unloads the code)."""
+        self.code = None
+
+    def load(self, f):
+        """Loads bytecode from a f."""
+        # make sure the magic header is correct
+        magic = f.read(len(bc_magic))
+        if magic != bc_magic:
+            self.reset()
+            return
+        # the source code of the file changed, we need to reload
+        checksum = pickle.load(f)
+        if self.checksum != checksum:
+            self.reset()
+            return
+        # now load the code.  Because marshal is not able to load
+        # from arbitrary streams we have to work around that
+        if isinstance(f, file):
+            self.code = marshal.load(f)
+        else:
+            self.code = marshal.loads(f.read())
+
+    def dump(self, f):
+        """Dump the bytecode into f."""
+        if self.code is None:
+            raise TypeError('can\'t write empty bucket')
+        f.write(bc_magic)
+        pickle.dump(self.checksum, f, 2)
+        if isinstance(f, file):
+            marshal.dump(self.code, f)
+        else:
+            f.write(marshal.dumps(self.code))
+
+    def loads(self, string):
+        """Load bytecode from a string."""
+        self.load(StringIO(string))
+
+    def dumps(self):
+        """Return the bytecode as string."""
+        out = StringIO()
+        self.dump(out)
+        return out.getvalue()
+
+    def write_back(self):
+        """Write the bucket back to the cache."""
+        self._cache.dump_bucket(self)
+
+
+class BytecodeCache(object):
+    """To implement your own bytecode cache you have to subclass this class
+    and override :meth:`load_bucket` and :meth:`dump_bucket`.  Both of these
+    methods are passed a :class:`Bucket` that they have to load or dump.
+    """
+
+    def load_bucket(self, bucket):
+        """Subclasses have to override this method to load bytecode
+        into a bucket.
+        """
+        raise NotImplementedError()
+
+    def dump_bucket(self, bucket):
+        """Subclasses have to override this method to write the
+        bytecode from a bucket back to the cache.
+        """
+        raise NotImplementedError()
+
+    def get_cache_key(self, name):
+        """Return the unique hash key for this template name."""
+        return sha1(name.encode('utf-8')).hexdigest()
+
+    def get_source_checksum(self, source):
+        """Return a checksum for the source."""
+        return sha1(source.encode('utf-8')).hexdigest()
+
+    def get_bucket(self, environment, name, source):
+        """Return a cache bucket."""
+        key = self.get_cache_key(name)
+        checksum = self.get_source_checksum(source)
+        bucket = Bucket(self, environment, key, checksum)
+        self.load_bucket(bucket)
+        return bucket
+
+
+class FileSystemCache(BytecodeCache):
+    """A bytecode cache that stores bytecode on the filesystem."""
+
+    def __init__(self, directory, pattern='%s.jbc'):
+        self.directory = directory
+        self.pattern = pattern
+
+    def _get_cache_filename(self, bucket):
+        return path.join(self.directory, self.pattern % bucket.key)
+
+    def load_bucket(self, bucket):
+        filename = self._get_cache_filename(bucket)
+        if path.exists(filename):
+            f = file(filename, 'rb')
+            try:
+                bucket.load(f)
+            finally:
+                f.close()
+
+    def dump_bucket(self, bucket):
+        filename = self._get_cache_filename(bucket)
+        f = file(filename, 'wb')
+        try:
+            bucket.dump(f)
+        finally:
+            f.close()