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