Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/jinja2/bccache.py: 25%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""The optional bytecode cache system. This is useful if you have very
2complex template situations and the compilation of all those templates
3slows down your application too much.
5Situations where this is useful are often forking web applications that
6are initialized on the first request.
7"""
9import errno
10import fnmatch
11import marshal
12import os
13import pickle
14import stat
15import sys
16import tempfile
17import typing as t
18from hashlib import sha1
19from io import BytesIO
20from types import CodeType
22if t.TYPE_CHECKING:
23 import typing_extensions as te
25 from .environment import Environment
27 class _MemcachedClient(te.Protocol):
28 def get(self, key: str) -> bytes: ...
30 def set(self, key: str, value: bytes, timeout: int | None = None) -> None: ...
33bc_version = 5
34# Magic bytes to identify Jinja bytecode cache files. Contains the
35# Python major and minor version to avoid loading incompatible bytecode
36# if a project upgrades its Python version.
37bc_magic = (
38 b"j2"
39 + pickle.dumps(bc_version, 2)
40 + pickle.dumps((sys.version_info[0] << 24) | sys.version_info[1], 2)
41)
44class Bucket:
45 """Buckets are used to store the bytecode for one template. It's created
46 and initialized by the bytecode cache and passed to the loading functions.
48 The buckets get an internal checksum from the cache assigned and use this
49 to automatically reject outdated cache material. Individual bytecode
50 cache subclasses don't have to care about cache invalidation.
51 """
53 def __init__(self, environment: "Environment", key: str, checksum: str) -> None:
54 self.environment = environment
55 self.key = key
56 self.checksum = checksum
57 self.reset()
59 def reset(self) -> None:
60 """Resets the bucket (unloads the bytecode)."""
61 self.code: CodeType | None = None
63 def load_bytecode(self, f: t.BinaryIO) -> None:
64 """Loads bytecode from a file or file like object."""
65 # make sure the magic header is correct
66 magic = f.read(len(bc_magic))
67 if magic != bc_magic:
68 self.reset()
69 return
70 # the source code of the file changed, we need to reload
71 checksum = pickle.load(f)
72 if self.checksum != checksum:
73 self.reset()
74 return
75 # if marshal_load fails then we need to reload
76 try:
77 self.code = marshal.load(f)
78 except (EOFError, ValueError, TypeError):
79 self.reset()
80 return
82 def write_bytecode(self, f: t.IO[bytes]) -> None:
83 """Dump the bytecode into the file or file like object passed."""
84 if self.code is None:
85 raise TypeError("can't write empty bucket")
86 f.write(bc_magic)
87 pickle.dump(self.checksum, f, 2)
88 marshal.dump(self.code, f)
90 def bytecode_from_string(self, string: bytes) -> None:
91 """Load bytecode from bytes."""
92 self.load_bytecode(BytesIO(string))
94 def bytecode_to_string(self) -> bytes:
95 """Return the bytecode as bytes."""
96 out = BytesIO()
97 self.write_bytecode(out)
98 return out.getvalue()
101class BytecodeCache:
102 """To implement your own bytecode cache you have to subclass this class
103 and override :meth:`load_bytecode` and :meth:`dump_bytecode`. Both of
104 these methods are passed a :class:`~jinja2.bccache.Bucket`.
106 A very basic bytecode cache that saves the bytecode on the file system::
108 from os import path
110 class MyCache(BytecodeCache):
112 def __init__(self, directory):
113 self.directory = directory
115 def load_bytecode(self, bucket):
116 filename = path.join(self.directory, bucket.key)
117 if path.exists(filename):
118 with open(filename, 'rb') as f:
119 bucket.load_bytecode(f)
121 def dump_bytecode(self, bucket):
122 filename = path.join(self.directory, bucket.key)
123 with open(filename, 'wb') as f:
124 bucket.write_bytecode(f)
126 A more advanced version of a filesystem based bytecode cache is part of
127 Jinja.
128 """
130 def load_bytecode(self, bucket: Bucket) -> None:
131 """Subclasses have to override this method to load bytecode into a
132 bucket. If they are not able to find code in the cache for the
133 bucket, it must not do anything.
134 """
135 raise NotImplementedError()
137 def dump_bytecode(self, bucket: Bucket) -> None:
138 """Subclasses have to override this method to write the bytecode
139 from a bucket back to the cache. If it unable to do so it must not
140 fail silently but raise an exception.
141 """
142 raise NotImplementedError()
144 def clear(self) -> None:
145 """Clears the cache. This method is not used by Jinja but should be
146 implemented to allow applications to clear the bytecode cache used
147 by a particular environment.
148 """
150 def get_cache_key(self, name: str, filename: str | None = None) -> str:
151 """Returns the unique hash key for this template name."""
152 hash = sha1(name.encode("utf-8"))
154 if filename is not None:
155 hash.update(f"|{filename}".encode())
157 return hash.hexdigest()
159 def get_source_checksum(self, source: str) -> str:
160 """Returns a checksum for the source."""
161 return sha1(source.encode("utf-8")).hexdigest()
163 def get_bucket(
164 self,
165 environment: "Environment",
166 name: str,
167 filename: str | None,
168 source: str,
169 ) -> Bucket:
170 """Return a cache bucket for the given template. All arguments are
171 mandatory but filename may be `None`.
172 """
173 key = self.get_cache_key(name, filename)
174 checksum = self.get_source_checksum(source)
175 bucket = Bucket(environment, key, checksum)
176 self.load_bytecode(bucket)
177 return bucket
179 def set_bucket(self, bucket: Bucket) -> None:
180 """Put the bucket into the cache."""
181 self.dump_bytecode(bucket)
184class FileSystemBytecodeCache(BytecodeCache):
185 """A bytecode cache that stores bytecode on the filesystem. It accepts
186 two arguments: The directory where the cache items are stored and a
187 pattern string that is used to build the filename.
189 If no directory is specified a default cache directory is selected. On
190 Windows the user's temp directory is used, on UNIX systems a directory
191 is created for the user in the system temp directory.
193 The pattern can be used to have multiple separate caches operate on the
194 same directory. The default pattern is ``'__jinja2_%s.cache'``. ``%s``
195 is replaced with the cache key.
197 >>> bcc = FileSystemBytecodeCache('/tmp/jinja_cache', '%s.cache')
199 This bytecode cache supports clearing of the cache using the clear method.
200 """
202 def __init__(
203 self, directory: str | None = None, pattern: str = "__jinja2_%s.cache"
204 ) -> None:
205 if directory is None:
206 directory = self._get_default_cache_dir()
207 self.directory = directory
208 self.pattern = pattern
210 def _get_default_cache_dir(self) -> str:
211 def _unsafe_dir() -> "te.NoReturn":
212 raise RuntimeError(
213 "Cannot determine safe temp directory. You "
214 "need to explicitly provide one."
215 )
217 tmpdir = tempfile.gettempdir()
219 # On windows the temporary directory is used specific unless
220 # explicitly forced otherwise. We can just use that.
221 if os.name == "nt":
222 return tmpdir
223 if not hasattr(os, "getuid"):
224 _unsafe_dir()
226 dirname = f"_jinja2-cache-{os.getuid()}"
227 actual_dir = os.path.join(tmpdir, dirname)
229 try:
230 os.mkdir(actual_dir, stat.S_IRWXU)
231 except OSError as e:
232 if e.errno != errno.EEXIST:
233 raise
234 try:
235 os.chmod(actual_dir, stat.S_IRWXU)
236 actual_dir_stat = os.lstat(actual_dir)
237 if (
238 actual_dir_stat.st_uid != os.getuid()
239 or not stat.S_ISDIR(actual_dir_stat.st_mode)
240 or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU
241 ):
242 _unsafe_dir()
243 except OSError as e:
244 if e.errno != errno.EEXIST:
245 raise
247 actual_dir_stat = os.lstat(actual_dir)
248 if (
249 actual_dir_stat.st_uid != os.getuid()
250 or not stat.S_ISDIR(actual_dir_stat.st_mode)
251 or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU
252 ):
253 _unsafe_dir()
255 return actual_dir
257 def _get_cache_filename(self, bucket: Bucket) -> str:
258 return os.path.join(self.directory, self.pattern % (bucket.key,))
260 def load_bytecode(self, bucket: Bucket) -> None:
261 filename = self._get_cache_filename(bucket)
263 # Don't test for existence before opening the file, since the
264 # file could disappear after the test before the open.
265 try:
266 f = open(filename, "rb")
267 except (FileNotFoundError, IsADirectoryError, PermissionError):
268 # PermissionError can occur on Windows when an operation is
269 # in progress, such as calling clear().
270 return
272 with f:
273 bucket.load_bytecode(f)
275 def dump_bytecode(self, bucket: Bucket) -> None:
276 # Write to a temporary file, then rename to the real name after
277 # writing. This avoids another process reading the file before
278 # it is fully written.
279 name = self._get_cache_filename(bucket)
280 f = tempfile.NamedTemporaryFile(
281 mode="wb",
282 dir=os.path.dirname(name),
283 prefix=os.path.basename(name),
284 suffix=".tmp",
285 delete=False,
286 )
288 def remove_silent() -> None:
289 try:
290 os.remove(f.name)
291 except OSError:
292 # Another process may have called clear(). On Windows,
293 # another program may be holding the file open.
294 pass
296 try:
297 with f:
298 bucket.write_bytecode(f)
299 except BaseException:
300 remove_silent()
301 raise
303 try:
304 os.replace(f.name, name)
305 except OSError:
306 # Another process may have called clear(). On Windows,
307 # another program may be holding the file open.
308 remove_silent()
309 except BaseException:
310 remove_silent()
311 raise
313 def clear(self) -> None:
314 # imported lazily here because google app-engine doesn't support
315 # write access on the file system and the function does not exist
316 # normally.
317 from os import remove
319 files = fnmatch.filter(os.listdir(self.directory), self.pattern % ("*",))
320 for filename in files:
321 try:
322 remove(os.path.join(self.directory, filename))
323 except OSError:
324 pass
327class MemcachedBytecodeCache(BytecodeCache):
328 """This class implements a bytecode cache that uses a memcache cache for
329 storing the information. It does not enforce a specific memcache library
330 (tummy's memcache or cmemcache) but will accept any class that provides
331 the minimal interface required.
333 Libraries compatible with this class:
335 - `cachelib <https://github.com/pallets/cachelib>`_
336 - `python-memcached <https://pypi.org/project/python-memcached/>`_
338 (Unfortunately the django cache interface is not compatible because it
339 does not support storing binary data, only text. You can however pass
340 the underlying cache client to the bytecode cache which is available
341 as `django.core.cache.cache._client`.)
343 The minimal interface for the client passed to the constructor is this:
345 .. class:: MinimalClientInterface
347 .. method:: set(key, value[, timeout])
349 Stores the bytecode in the cache. `value` is a string and
350 `timeout` the timeout of the key. If timeout is not provided
351 a default timeout or no timeout should be assumed, if it's
352 provided it's an integer with the number of seconds the cache
353 item should exist.
355 .. method:: get(key)
357 Returns the value for the cache key. If the item does not
358 exist in the cache the return value must be `None`.
360 The other arguments to the constructor are the prefix for all keys that
361 is added before the actual cache key and the timeout for the bytecode in
362 the cache system. We recommend a high (or no) timeout.
364 This bytecode cache does not support clearing of used items in the cache.
365 The clear method is a no-operation function.
367 .. versionadded:: 2.7
368 Added support for ignoring memcache errors through the
369 `ignore_memcache_errors` parameter.
370 """
372 def __init__(
373 self,
374 client: "_MemcachedClient",
375 prefix: str = "jinja2/bytecode/",
376 timeout: int | None = None,
377 ignore_memcache_errors: bool = True,
378 ):
379 self.client = client
380 self.prefix = prefix
381 self.timeout = timeout
382 self.ignore_memcache_errors = ignore_memcache_errors
384 def load_bytecode(self, bucket: Bucket) -> None:
385 try:
386 code = self.client.get(self.prefix + bucket.key)
387 except Exception:
388 if not self.ignore_memcache_errors:
389 raise
390 else:
391 bucket.bytecode_from_string(code)
393 def dump_bytecode(self, bucket: Bucket) -> None:
394 key = self.prefix + bucket.key
395 value = bucket.bytecode_to_string()
397 try:
398 if self.timeout is not None:
399 self.client.set(key, value, self.timeout)
400 else:
401 self.client.set(key, value)
402 except Exception:
403 if not self.ignore_memcache_errors:
404 raise