| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | """ |
| 3 | jinja2.bccache |
| 4 | ~~~~~~~~~~~~~~ |
| 5 | |
| 6 | This module implements the bytecode cache system Jinja is optionally |
| 7 | using. This is useful if you have very complex template situations and |
| 8 | the compiliation of all those templates slow down your application too |
| 9 | much. |
| 10 | |
| 11 | Situations where this is useful are often forking web applications that |
| 12 | are initialized on the first request. |
| 13 | |
| Armin Ronacher | 55494e4 | 2010-01-22 09:41:48 +0100 | [diff] [blame] | 14 | :copyright: (c) 2010 by the Jinja Team. |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 15 | :license: BSD. |
| 16 | """ |
| Max Ischenko | 03f8823 | 2008-09-18 16:23:33 +0200 | [diff] [blame] | 17 | from os import path, listdir |
| Armin Ronacher | 51db6c9 | 2011-01-11 20:53:42 +0100 | [diff] [blame] | 18 | import sys |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 19 | import marshal |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 20 | import tempfile |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 21 | import cPickle as pickle |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 22 | import fnmatch |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 23 | from cStringIO import StringIO |
| 24 | try: |
| 25 | from hashlib import sha1 |
| 26 | except ImportError: |
| 27 | from sha import new as sha1 |
| Armin Ronacher | ccae055 | 2008-10-05 23:08:58 +0200 | [diff] [blame] | 28 | from jinja2.utils import open_if_exists |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 29 | |
| 30 | |
| Armin Ronacher | 51db6c9 | 2011-01-11 20:53:42 +0100 | [diff] [blame] | 31 | bc_version = 2 |
| 32 | |
| 33 | # magic version used to only change with new jinja versions. With 2.6 |
| 34 | # we change this to also take Python version changes into account. The |
| 35 | # reason for this is that Python tends to segfault if fed earlier bytecode |
| 36 | # versions because someone thought it would be a good idea to reuse opcodes |
| 37 | # or make Python incompatible with earlier versions. |
| 38 | bc_magic = 'j2'.encode('ascii') + \ |
| 39 | pickle.dumps(bc_version, 2) + \ |
| 40 | pickle.dumps((sys.version_info[0] << 24) | sys.version_info[1]) |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 41 | |
| 42 | |
| 43 | class Bucket(object): |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 44 | """Buckets are used to store the bytecode for one template. It's created |
| 45 | and initialized by the bytecode cache and passed to the loading functions. |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 46 | |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 47 | The buckets get an internal checksum from the cache assigned and use this |
| 48 | to automatically reject outdated cache material. Individual bytecode |
| 49 | cache subclasses don't have to care about cache invalidation. |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 50 | """ |
| 51 | |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 52 | def __init__(self, environment, key, checksum): |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 53 | self.environment = environment |
| 54 | self.key = key |
| 55 | self.checksum = checksum |
| 56 | self.reset() |
| 57 | |
| 58 | def reset(self): |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 59 | """Resets the bucket (unloads the bytecode).""" |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 60 | self.code = None |
| 61 | |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 62 | def load_bytecode(self, f): |
| 63 | """Loads bytecode from a file or file like object.""" |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 64 | # make sure the magic header is correct |
| 65 | magic = f.read(len(bc_magic)) |
| 66 | if magic != bc_magic: |
| 67 | self.reset() |
| 68 | return |
| 69 | # the source code of the file changed, we need to reload |
| Armin Ronacher | aa1d17d | 2008-09-18 18:09:06 +0200 | [diff] [blame] | 70 | checksum = pickle.load(f) |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 71 | if self.checksum != checksum: |
| 72 | self.reset() |
| 73 | return |
| Armin Ronacher | aa1d17d | 2008-09-18 18:09:06 +0200 | [diff] [blame] | 74 | # now load the code. Because marshal is not able to load |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 75 | # from arbitrary streams we have to work around that |
| 76 | if isinstance(f, file): |
| Armin Ronacher | aa1d17d | 2008-09-18 18:09:06 +0200 | [diff] [blame] | 77 | self.code = marshal.load(f) |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 78 | else: |
| 79 | self.code = marshal.loads(f.read()) |
| 80 | |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 81 | def write_bytecode(self, f): |
| 82 | """Dump the bytecode into the file or file like object passed.""" |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 83 | if self.code is None: |
| 84 | raise TypeError('can\'t write empty bucket') |
| 85 | f.write(bc_magic) |
| Armin Ronacher | aa1d17d | 2008-09-18 18:09:06 +0200 | [diff] [blame] | 86 | pickle.dump(self.checksum, f, 2) |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 87 | if isinstance(f, file): |
| Armin Ronacher | aa1d17d | 2008-09-18 18:09:06 +0200 | [diff] [blame] | 88 | marshal.dump(self.code, f) |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 89 | else: |
| 90 | f.write(marshal.dumps(self.code)) |
| 91 | |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 92 | def bytecode_from_string(self, string): |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 93 | """Load bytecode from a string.""" |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 94 | self.load_bytecode(StringIO(string)) |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 95 | |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 96 | def bytecode_to_string(self): |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 97 | """Return the bytecode as string.""" |
| 98 | out = StringIO() |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 99 | self.write_bytecode(out) |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 100 | return out.getvalue() |
| 101 | |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 102 | |
| 103 | class BytecodeCache(object): |
| 104 | """To implement your own bytecode cache you have to subclass this class |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 105 | and override :meth:`load_bytecode` and :meth:`dump_bytecode`. Both of |
| 106 | these methods are passed a :class:`~jinja2.bccache.Bucket`. |
| 107 | |
| 108 | A very basic bytecode cache that saves the bytecode on the file system:: |
| 109 | |
| 110 | from os import path |
| 111 | |
| 112 | class MyCache(BytecodeCache): |
| 113 | |
| 114 | def __init__(self, directory): |
| 115 | self.directory = directory |
| 116 | |
| 117 | def load_bytecode(self, bucket): |
| 118 | filename = path.join(self.directory, bucket.key) |
| 119 | if path.exists(filename): |
| Armin Ronacher | 0faa861 | 2010-02-09 15:04:51 +0100 | [diff] [blame] | 120 | with open(filename, 'rb') as f: |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 121 | bucket.load_bytecode(f) |
| 122 | |
| 123 | def dump_bytecode(self, bucket): |
| 124 | filename = path.join(self.directory, bucket.key) |
| Armin Ronacher | 0faa861 | 2010-02-09 15:04:51 +0100 | [diff] [blame] | 125 | with open(filename, 'wb') as f: |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 126 | bucket.write_bytecode(f) |
| 127 | |
| 128 | A more advanced version of a filesystem based bytecode cache is part of |
| 129 | Jinja2. |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 130 | """ |
| 131 | |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 132 | def load_bytecode(self, bucket): |
| 133 | """Subclasses have to override this method to load bytecode into a |
| 134 | bucket. If they are not able to find code in the cache for the |
| 135 | bucket, it must not do anything. |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 136 | """ |
| 137 | raise NotImplementedError() |
| 138 | |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 139 | def dump_bytecode(self, bucket): |
| 140 | """Subclasses have to override this method to write the bytecode |
| 141 | from a bucket back to the cache. If it unable to do so it must not |
| 142 | fail silently but raise an exception. |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 143 | """ |
| 144 | raise NotImplementedError() |
| 145 | |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 146 | def clear(self): |
| 147 | """Clears the cache. This method is not used by Jinja2 but should be |
| 148 | implemented to allow applications to clear the bytecode cache used |
| 149 | by a particular environment. |
| 150 | """ |
| 151 | |
| 152 | def get_cache_key(self, name, filename=None): |
| 153 | """Returns the unique hash key for this template name.""" |
| 154 | hash = sha1(name.encode('utf-8')) |
| 155 | if filename is not None: |
| 156 | if isinstance(filename, unicode): |
| 157 | filename = filename.encode('utf-8') |
| 158 | hash.update('|' + filename) |
| 159 | return hash.hexdigest() |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 160 | |
| 161 | def get_source_checksum(self, source): |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 162 | """Returns a checksum for the source.""" |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 163 | return sha1(source.encode('utf-8')).hexdigest() |
| 164 | |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 165 | def get_bucket(self, environment, name, filename, source): |
| 166 | """Return a cache bucket for the given template. All arguments are |
| 167 | mandatory but filename may be `None`. |
| 168 | """ |
| 169 | key = self.get_cache_key(name, filename) |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 170 | checksum = self.get_source_checksum(source) |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 171 | bucket = Bucket(environment, key, checksum) |
| 172 | self.load_bytecode(bucket) |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 173 | return bucket |
| 174 | |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 175 | def set_bucket(self, bucket): |
| 176 | """Put the bucket into the cache.""" |
| 177 | self.dump_bytecode(bucket) |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 178 | |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 179 | |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 180 | class FileSystemBytecodeCache(BytecodeCache): |
| 181 | """A bytecode cache that stores bytecode on the filesystem. It accepts |
| 182 | two arguments: The directory where the cache items are stored and a |
| 183 | pattern string that is used to build the filename. |
| 184 | |
| 185 | If no directory is specified the system temporary items folder is used. |
| 186 | |
| 187 | The pattern can be used to have multiple separate caches operate on the |
| 188 | same directory. The default pattern is ``'__jinja2_%s.cache'``. ``%s`` |
| 189 | is replaced with the cache key. |
| 190 | |
| 191 | >>> bcc = FileSystemBytecodeCache('/tmp/jinja_cache', '%s.cache') |
| Armin Ronacher | aa1d17d | 2008-09-18 18:09:06 +0200 | [diff] [blame] | 192 | |
| 193 | This bytecode cache supports clearing of the cache using the clear method. |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 194 | """ |
| 195 | |
| 196 | def __init__(self, directory=None, pattern='__jinja2_%s.cache'): |
| 197 | if directory is None: |
| 198 | directory = tempfile.gettempdir() |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 199 | self.directory = directory |
| 200 | self.pattern = pattern |
| 201 | |
| 202 | def _get_cache_filename(self, bucket): |
| 203 | return path.join(self.directory, self.pattern % bucket.key) |
| 204 | |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 205 | def load_bytecode(self, bucket): |
| Armin Ronacher | ccae055 | 2008-10-05 23:08:58 +0200 | [diff] [blame] | 206 | f = open_if_exists(self._get_cache_filename(bucket), 'rb') |
| 207 | if f is not None: |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 208 | try: |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 209 | bucket.load_bytecode(f) |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 210 | finally: |
| 211 | f.close() |
| 212 | |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 213 | def dump_bytecode(self, bucket): |
| Armin Ronacher | 0faa861 | 2010-02-09 15:04:51 +0100 | [diff] [blame] | 214 | f = open(self._get_cache_filename(bucket), 'wb') |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 215 | try: |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 216 | bucket.write_bytecode(f) |
| Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 217 | finally: |
| 218 | f.close() |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 219 | |
| 220 | def clear(self): |
| Max Ischenko | 03f8823 | 2008-09-18 16:23:33 +0200 | [diff] [blame] | 221 | # imported lazily here because google app-engine doesn't support |
| 222 | # write access on the file system and the function does not exist |
| 223 | # normally. |
| 224 | from os import remove |
| Armin Ronacher | 2e46a5c | 2008-09-17 22:25:04 +0200 | [diff] [blame] | 225 | files = fnmatch.filter(listdir(self.directory), self.pattern % '*') |
| 226 | for filename in files: |
| Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 227 | try: |
| 228 | remove(path.join(self.directory, filename)) |
| 229 | except OSError: |
| 230 | pass |
| Armin Ronacher | aa1d17d | 2008-09-18 18:09:06 +0200 | [diff] [blame] | 231 | |
| 232 | |
| 233 | class MemcachedBytecodeCache(BytecodeCache): |
| 234 | """This class implements a bytecode cache that uses a memcache cache for |
| 235 | storing the information. It does not enforce a specific memcache library |
| 236 | (tummy's memcache or cmemcache) but will accept any class that provides |
| 237 | the minimal interface required. |
| 238 | |
| 239 | Libraries compatible with this class: |
| 240 | |
| 241 | - `werkzeug <http://werkzeug.pocoo.org/>`_.contrib.cache |
| 242 | - `python-memcached <http://www.tummy.com/Community/software/python-memcached/>`_ |
| 243 | - `cmemcache <http://gijsbert.org/cmemcache/>`_ |
| 244 | |
| 245 | (Unfortunately the django cache interface is not compatible because it |
| 246 | does not support storing binary data, only unicode. You can however pass |
| Georg Brandl | 3e497b7 | 2008-09-19 09:55:17 +0000 | [diff] [blame] | 247 | the underlying cache client to the bytecode cache which is available |
| Armin Ronacher | aa1d17d | 2008-09-18 18:09:06 +0200 | [diff] [blame] | 248 | as `django.core.cache.cache._client`.) |
| 249 | |
| 250 | The minimal interface for the client passed to the constructor is this: |
| 251 | |
| 252 | .. class:: MinimalClientInterface |
| 253 | |
| 254 | .. method:: set(key, value[, timeout]) |
| 255 | |
| 256 | Stores the bytecode in the cache. `value` is a string and |
| 257 | `timeout` the timeout of the key. If timeout is not provided |
| 258 | a default timeout or no timeout should be assumed, if it's |
| 259 | provided it's an integer with the number of seconds the cache |
| 260 | item should exist. |
| 261 | |
| 262 | .. method:: get(key) |
| 263 | |
| 264 | Returns the value for the cache key. If the item does not |
| 265 | exist in the cache the return value must be `None`. |
| 266 | |
| 267 | The other arguments to the constructor are the prefix for all keys that |
| 268 | is added before the actual cache key and the timeout for the bytecode in |
| 269 | the cache system. We recommend a high (or no) timeout. |
| 270 | |
| 271 | This bytecode cache does not support clearing of used items in the cache. |
| 272 | The clear method is a no-operation function. |
| 273 | """ |
| 274 | |
| 275 | def __init__(self, client, prefix='jinja2/bytecode/', timeout=None): |
| 276 | self.client = client |
| 277 | self.prefix = prefix |
| 278 | self.timeout = timeout |
| 279 | |
| 280 | def load_bytecode(self, bucket): |
| 281 | code = self.client.get(self.prefix + bucket.key) |
| 282 | if code is not None: |
| 283 | bucket.bytecode_from_string(code) |
| 284 | |
| 285 | def dump_bytecode(self, bucket): |
| 286 | args = (self.prefix + bucket.key, bucket.bytecode_to_string()) |
| 287 | if self.timeout is not None: |
| 288 | args += (self.timeout,) |
| 289 | self.client.set(*args) |