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