Update 2025-04-13_16:26:56
This commit is contained in:
408
venv/lib/python3.11/site-packages/jinja2/bccache.py
Normal file
408
venv/lib/python3.11/site-packages/jinja2/bccache.py
Normal file
@ -0,0 +1,408 @@
|
||||
"""The optional bytecode cache system. This is useful if you have very
|
||||
complex template situations and the compilation of all those templates
|
||||
slows down your application too much.
|
||||
|
||||
Situations where this is useful are often forking web applications that
|
||||
are initialized on the first request.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import fnmatch
|
||||
import marshal
|
||||
import os
|
||||
import pickle
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
import typing as t
|
||||
from hashlib import sha1
|
||||
from io import BytesIO
|
||||
from types import CodeType
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
import typing_extensions as te
|
||||
|
||||
from .environment import Environment
|
||||
|
||||
class _MemcachedClient(te.Protocol):
|
||||
def get(self, key: str) -> bytes: ...
|
||||
|
||||
def set(
|
||||
self, key: str, value: bytes, timeout: t.Optional[int] = None
|
||||
) -> None: ...
|
||||
|
||||
|
||||
bc_version = 5
|
||||
# Magic bytes to identify Jinja bytecode cache files. Contains the
|
||||
# Python major and minor version to avoid loading incompatible bytecode
|
||||
# if a project upgrades its Python version.
|
||||
bc_magic = (
|
||||
b"j2"
|
||||
+ pickle.dumps(bc_version, 2)
|
||||
+ pickle.dumps((sys.version_info[0] << 24) | sys.version_info[1], 2)
|
||||
)
|
||||
|
||||
|
||||
class Bucket:
|
||||
"""Buckets are used to store the bytecode for one template. It's created
|
||||
and initialized by the bytecode cache and passed to the loading functions.
|
||||
|
||||
The buckets get an internal checksum from the cache assigned and use this
|
||||
to automatically reject outdated cache material. Individual bytecode
|
||||
cache subclasses don't have to care about cache invalidation.
|
||||
"""
|
||||
|
||||
def __init__(self, environment: "Environment", key: str, checksum: str) -> None:
|
||||
self.environment = environment
|
||||
self.key = key
|
||||
self.checksum = checksum
|
||||
self.reset()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Resets the bucket (unloads the bytecode)."""
|
||||
self.code: t.Optional[CodeType] = None
|
||||
|
||||
def load_bytecode(self, f: t.BinaryIO) -> None:
|
||||
"""Loads bytecode from a file or file like object."""
|
||||
# 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
|
||||
# if marshal_load fails then we need to reload
|
||||
try:
|
||||
self.code = marshal.load(f)
|
||||
except (EOFError, ValueError, TypeError):
|
||||
self.reset()
|
||||
return
|
||||
|
||||
def write_bytecode(self, f: t.IO[bytes]) -> None:
|
||||
"""Dump the bytecode into the file or file like object passed."""
|
||||
if self.code is None:
|
||||
raise TypeError("can't write empty bucket")
|
||||
f.write(bc_magic)
|
||||
pickle.dump(self.checksum, f, 2)
|
||||
marshal.dump(self.code, f)
|
||||
|
||||
def bytecode_from_string(self, string: bytes) -> None:
|
||||
"""Load bytecode from bytes."""
|
||||
self.load_bytecode(BytesIO(string))
|
||||
|
||||
def bytecode_to_string(self) -> bytes:
|
||||
"""Return the bytecode as bytes."""
|
||||
out = BytesIO()
|
||||
self.write_bytecode(out)
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
class BytecodeCache:
|
||||
"""To implement your own bytecode cache you have to subclass this class
|
||||
and override :meth:`load_bytecode` and :meth:`dump_bytecode`. Both of
|
||||
these methods are passed a :class:`~jinja2.bccache.Bucket`.
|
||||
|
||||
A very basic bytecode cache that saves the bytecode on the file system::
|
||||
|
||||
from os import path
|
||||
|
||||
class MyCache(BytecodeCache):
|
||||
|
||||
def __init__(self, directory):
|
||||
self.directory = directory
|
||||
|
||||
def load_bytecode(self, bucket):
|
||||
filename = path.join(self.directory, bucket.key)
|
||||
if path.exists(filename):
|
||||
with open(filename, 'rb') as f:
|
||||
bucket.load_bytecode(f)
|
||||
|
||||
def dump_bytecode(self, bucket):
|
||||
filename = path.join(self.directory, bucket.key)
|
||||
with open(filename, 'wb') as f:
|
||||
bucket.write_bytecode(f)
|
||||
|
||||
A more advanced version of a filesystem based bytecode cache is part of
|
||||
Jinja.
|
||||
"""
|
||||
|
||||
def load_bytecode(self, bucket: Bucket) -> None:
|
||||
"""Subclasses have to override this method to load bytecode into a
|
||||
bucket. If they are not able to find code in the cache for the
|
||||
bucket, it must not do anything.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def dump_bytecode(self, bucket: Bucket) -> None:
|
||||
"""Subclasses have to override this method to write the bytecode
|
||||
from a bucket back to the cache. If it unable to do so it must not
|
||||
fail silently but raise an exception.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clears the cache. This method is not used by Jinja but should be
|
||||
implemented to allow applications to clear the bytecode cache used
|
||||
by a particular environment.
|
||||
"""
|
||||
|
||||
def get_cache_key(
|
||||
self, name: str, filename: t.Optional[t.Union[str]] = None
|
||||
) -> str:
|
||||
"""Returns the unique hash key for this template name."""
|
||||
hash = sha1(name.encode("utf-8"))
|
||||
|
||||
if filename is not None:
|
||||
hash.update(f"|{filename}".encode())
|
||||
|
||||
return hash.hexdigest()
|
||||
|
||||
def get_source_checksum(self, source: str) -> str:
|
||||
"""Returns a checksum for the source."""
|
||||
return sha1(source.encode("utf-8")).hexdigest()
|
||||
|
||||
def get_bucket(
|
||||
self,
|
||||
environment: "Environment",
|
||||
name: str,
|
||||
filename: t.Optional[str],
|
||||
source: str,
|
||||
) -> Bucket:
|
||||
"""Return a cache bucket for the given template. All arguments are
|
||||
mandatory but filename may be `None`.
|
||||
"""
|
||||
key = self.get_cache_key(name, filename)
|
||||
checksum = self.get_source_checksum(source)
|
||||
bucket = Bucket(environment, key, checksum)
|
||||
self.load_bytecode(bucket)
|
||||
return bucket
|
||||
|
||||
def set_bucket(self, bucket: Bucket) -> None:
|
||||
"""Put the bucket into the cache."""
|
||||
self.dump_bytecode(bucket)
|
||||
|
||||
|
||||
class FileSystemBytecodeCache(BytecodeCache):
|
||||
"""A bytecode cache that stores bytecode on the filesystem. It accepts
|
||||
two arguments: The directory where the cache items are stored and a
|
||||
pattern string that is used to build the filename.
|
||||
|
||||
If no directory is specified a default cache directory is selected. On
|
||||
Windows the user's temp directory is used, on UNIX systems a directory
|
||||
is created for the user in the system temp directory.
|
||||
|
||||
The pattern can be used to have multiple separate caches operate on the
|
||||
same directory. The default pattern is ``'__jinja2_%s.cache'``. ``%s``
|
||||
is replaced with the cache key.
|
||||
|
||||
>>> bcc = FileSystemBytecodeCache('/tmp/jinja_cache', '%s.cache')
|
||||
|
||||
This bytecode cache supports clearing of the cache using the clear method.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, directory: t.Optional[str] = None, pattern: str = "__jinja2_%s.cache"
|
||||
) -> None:
|
||||
if directory is None:
|
||||
directory = self._get_default_cache_dir()
|
||||
self.directory = directory
|
||||
self.pattern = pattern
|
||||
|
||||
def _get_default_cache_dir(self) -> str:
|
||||
def _unsafe_dir() -> "te.NoReturn":
|
||||
raise RuntimeError(
|
||||
"Cannot determine safe temp directory. You "
|
||||
"need to explicitly provide one."
|
||||
)
|
||||
|
||||
tmpdir = tempfile.gettempdir()
|
||||
|
||||
# On windows the temporary directory is used specific unless
|
||||
# explicitly forced otherwise. We can just use that.
|
||||
if os.name == "nt":
|
||||
return tmpdir
|
||||
if not hasattr(os, "getuid"):
|
||||
_unsafe_dir()
|
||||
|
||||
dirname = f"_jinja2-cache-{os.getuid()}"
|
||||
actual_dir = os.path.join(tmpdir, dirname)
|
||||
|
||||
try:
|
||||
os.mkdir(actual_dir, stat.S_IRWXU)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
try:
|
||||
os.chmod(actual_dir, stat.S_IRWXU)
|
||||
actual_dir_stat = os.lstat(actual_dir)
|
||||
if (
|
||||
actual_dir_stat.st_uid != os.getuid()
|
||||
or not stat.S_ISDIR(actual_dir_stat.st_mode)
|
||||
or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU
|
||||
):
|
||||
_unsafe_dir()
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
actual_dir_stat = os.lstat(actual_dir)
|
||||
if (
|
||||
actual_dir_stat.st_uid != os.getuid()
|
||||
or not stat.S_ISDIR(actual_dir_stat.st_mode)
|
||||
or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU
|
||||
):
|
||||
_unsafe_dir()
|
||||
|
||||
return actual_dir
|
||||
|
||||
def _get_cache_filename(self, bucket: Bucket) -> str:
|
||||
return os.path.join(self.directory, self.pattern % (bucket.key,))
|
||||
|
||||
def load_bytecode(self, bucket: Bucket) -> None:
|
||||
filename = self._get_cache_filename(bucket)
|
||||
|
||||
# Don't test for existence before opening the file, since the
|
||||
# file could disappear after the test before the open.
|
||||
try:
|
||||
f = open(filename, "rb")
|
||||
except (FileNotFoundError, IsADirectoryError, PermissionError):
|
||||
# PermissionError can occur on Windows when an operation is
|
||||
# in progress, such as calling clear().
|
||||
return
|
||||
|
||||
with f:
|
||||
bucket.load_bytecode(f)
|
||||
|
||||
def dump_bytecode(self, bucket: Bucket) -> None:
|
||||
# Write to a temporary file, then rename to the real name after
|
||||
# writing. This avoids another process reading the file before
|
||||
# it is fully written.
|
||||
name = self._get_cache_filename(bucket)
|
||||
f = tempfile.NamedTemporaryFile(
|
||||
mode="wb",
|
||||
dir=os.path.dirname(name),
|
||||
prefix=os.path.basename(name),
|
||||
suffix=".tmp",
|
||||
delete=False,
|
||||
)
|
||||
|
||||
def remove_silent() -> None:
|
||||
try:
|
||||
os.remove(f.name)
|
||||
except OSError:
|
||||
# Another process may have called clear(). On Windows,
|
||||
# another program may be holding the file open.
|
||||
pass
|
||||
|
||||
try:
|
||||
with f:
|
||||
bucket.write_bytecode(f)
|
||||
except BaseException:
|
||||
remove_silent()
|
||||
raise
|
||||
|
||||
try:
|
||||
os.replace(f.name, name)
|
||||
except OSError:
|
||||
# Another process may have called clear(). On Windows,
|
||||
# another program may be holding the file open.
|
||||
remove_silent()
|
||||
except BaseException:
|
||||
remove_silent()
|
||||
raise
|
||||
|
||||
def clear(self) -> None:
|
||||
# imported lazily here because google app-engine doesn't support
|
||||
# write access on the file system and the function does not exist
|
||||
# normally.
|
||||
from os import remove
|
||||
|
||||
files = fnmatch.filter(os.listdir(self.directory), self.pattern % ("*",))
|
||||
for filename in files:
|
||||
try:
|
||||
remove(os.path.join(self.directory, filename))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
class MemcachedBytecodeCache(BytecodeCache):
|
||||
"""This class implements a bytecode cache that uses a memcache cache for
|
||||
storing the information. It does not enforce a specific memcache library
|
||||
(tummy's memcache or cmemcache) but will accept any class that provides
|
||||
the minimal interface required.
|
||||
|
||||
Libraries compatible with this class:
|
||||
|
||||
- `cachelib <https://github.com/pallets/cachelib>`_
|
||||
- `python-memcached <https://pypi.org/project/python-memcached/>`_
|
||||
|
||||
(Unfortunately the django cache interface is not compatible because it
|
||||
does not support storing binary data, only text. You can however pass
|
||||
the underlying cache client to the bytecode cache which is available
|
||||
as `django.core.cache.cache._client`.)
|
||||
|
||||
The minimal interface for the client passed to the constructor is this:
|
||||
|
||||
.. class:: MinimalClientInterface
|
||||
|
||||
.. method:: set(key, value[, timeout])
|
||||
|
||||
Stores the bytecode in the cache. `value` is a string and
|
||||
`timeout` the timeout of the key. If timeout is not provided
|
||||
a default timeout or no timeout should be assumed, if it's
|
||||
provided it's an integer with the number of seconds the cache
|
||||
item should exist.
|
||||
|
||||
.. method:: get(key)
|
||||
|
||||
Returns the value for the cache key. If the item does not
|
||||
exist in the cache the return value must be `None`.
|
||||
|
||||
The other arguments to the constructor are the prefix for all keys that
|
||||
is added before the actual cache key and the timeout for the bytecode in
|
||||
the cache system. We recommend a high (or no) timeout.
|
||||
|
||||
This bytecode cache does not support clearing of used items in the cache.
|
||||
The clear method is a no-operation function.
|
||||
|
||||
.. versionadded:: 2.7
|
||||
Added support for ignoring memcache errors through the
|
||||
`ignore_memcache_errors` parameter.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: "_MemcachedClient",
|
||||
prefix: str = "jinja2/bytecode/",
|
||||
timeout: t.Optional[int] = None,
|
||||
ignore_memcache_errors: bool = True,
|
||||
):
|
||||
self.client = client
|
||||
self.prefix = prefix
|
||||
self.timeout = timeout
|
||||
self.ignore_memcache_errors = ignore_memcache_errors
|
||||
|
||||
def load_bytecode(self, bucket: Bucket) -> None:
|
||||
try:
|
||||
code = self.client.get(self.prefix + bucket.key)
|
||||
except Exception:
|
||||
if not self.ignore_memcache_errors:
|
||||
raise
|
||||
else:
|
||||
bucket.bytecode_from_string(code)
|
||||
|
||||
def dump_bytecode(self, bucket: Bucket) -> None:
|
||||
key = self.prefix + bucket.key
|
||||
value = bucket.bytecode_to_string()
|
||||
|
||||
try:
|
||||
if self.timeout is not None:
|
||||
self.client.set(key, value, self.timeout)
|
||||
else:
|
||||
self.client.set(key, value)
|
||||
except Exception:
|
||||
if not self.ignore_memcache_errors:
|
||||
raise
|
Reference in New Issue
Block a user