blob: 50532cdf2b30b1d225f516bd2b3e9a4b99941e50 [file] [log] [blame]
# -*- 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()