Cache compiled templates in render_template_string

This commit is contained in:
Optifiner AI 2026-01-24 16:26:46 +08:00 committed by GitHub
parent 2579ce9f18
commit 77b42e9e81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 147 additions and 2 deletions

View file

@ -1,6 +1,7 @@
from __future__ import annotations
import typing as t
from collections import OrderedDict
from jinja2 import BaseLoader
from jinja2 import Environment as BaseEnvironment
@ -38,11 +39,46 @@ class Environment(BaseEnvironment):
name of the blueprint to referenced templates if necessary.
"""
#: Maximum number of string templates to cache. When the cache is full,
#: the least recently used templates are evicted.
string_template_cache_size: t.ClassVar[int] = 100
#: Maximum length of a template source string to cache. Strings longer
#: than this are compiled but not cached.
string_template_cache_max_len: t.ClassVar[int] = 100_000
def __init__(self, app: App, **options: t.Any) -> None:
if "loader" not in options:
options["loader"] = app.create_global_jinja_loader()
BaseEnvironment.__init__(self, **options)
self.app = app
self._string_template_cache: OrderedDict[str, Template] = OrderedDict()
def get_or_compile_string(self, source: str) -> Template:
"""Get a compiled template from a source string, using a cache to
avoid recompiling the same template multiple times.
.. versionadded:: 3.2
"""
# Skip caching for very large templates
if len(source) > self.string_template_cache_max_len:
return self.from_string(source)
cache = self._string_template_cache
if source in cache:
# Move to end to mark as recently used
cache.move_to_end(source)
return cache[source]
template = self.from_string(source)
# Evict oldest entries if cache is full
while len(cache) >= self.string_template_cache_size:
cache.popitem(last=False)
cache[source] = template
return template
class DispatchingJinjaLoader(BaseLoader):
@ -153,9 +189,12 @@ def render_template_string(source: str, **context: t.Any) -> str:
:param source: The source code of the template to render.
:param context: The variables to make available in the template.
.. versionchanged:: 3.2
Templates are cached to avoid recompiling the same source string.
"""
ctx = app_ctx._get_current_object()
template = ctx.app.jinja_env.from_string(source)
template = ctx.app.jinja_env.get_or_compile_string(source)
return _render(ctx, template, context)
@ -205,7 +244,10 @@ def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]:
:param context: The variables to make available in the template.
.. versionadded:: 2.2
.. versionchanged:: 3.2
Templates are cached to avoid recompiling the same source string.
"""
ctx = app_ctx._get_current_object()
template = ctx.app.jinja_env.from_string(source)
template = ctx.app.jinja_env.get_or_compile_string(source)
return _stream(ctx, template, context)

View file

@ -530,3 +530,106 @@ def test_custom_jinja_env():
app = CustomFlask(__name__)
assert isinstance(app.jinja_env, CustomEnvironment)
def test_template_string_caching(app, app_ctx):
"""Test that render_template_string caches compiled templates."""
source = "Hello {{ name }}!"
cache = app.jinja_env._string_template_cache
# Cache should be empty initially
assert len(cache) == 0
# First render should add to cache
result1 = flask.render_template_string(source, name="World")
assert result1 == "Hello World!"
assert len(cache) == 1
assert source in cache
# Second render should use cached template
cached_template = cache[source]
result2 = flask.render_template_string(source, name="Flask")
assert result2 == "Hello Flask!"
assert len(cache) == 1
assert cache[source] is cached_template # Same object
def test_stream_template_string_caching(app, app_ctx):
"""Test that stream_template_string uses the same cache."""
source = "Hello {{ name }}!"
cache = app.jinja_env._string_template_cache
# Render with stream_template_string
result = "".join(flask.stream_template_string(source, name="World"))
assert result == "Hello World!"
assert source in cache
# Render same source with render_template_string should hit cache
cached_template = cache[source]
result2 = flask.render_template_string(source, name="Flask")
assert result2 == "Hello Flask!"
assert cache[source] is cached_template
def test_template_string_cache_per_app(app_ctx):
"""Test that each app has its own template string cache."""
app1 = flask.Flask(__name__)
app2 = flask.Flask(__name__)
source = "Hello {{ name }}!"
with app1.app_context():
flask.render_template_string(source, name="App1")
with app2.app_context():
flask.render_template_string(source, name="App2")
# Each app should have its own cache with the template
assert source in app1.jinja_env._string_template_cache
assert source in app2.jinja_env._string_template_cache
# But they should be different template objects (different environments)
assert (
app1.jinja_env._string_template_cache[source]
is not app2.jinja_env._string_template_cache[source]
)
def test_template_string_cache_lru_eviction(app, app_ctx):
"""Test that the cache evicts least recently used templates."""
# Set a small cache size for testing
app.jinja_env.string_template_cache_size = 3
cache = app.jinja_env._string_template_cache
# Fill the cache
flask.render_template_string("{{ a }}", a=1)
flask.render_template_string("{{ b }}", b=2)
flask.render_template_string("{{ c }}", c=3)
assert len(cache) == 3
assert list(cache.keys()) == ["{{ a }}", "{{ b }}", "{{ c }}"]
# Access the first one to make it recently used
flask.render_template_string("{{ a }}", a=1)
assert list(cache.keys()) == ["{{ b }}", "{{ c }}", "{{ a }}"]
# Add a new template, should evict "{{ b }}" (least recently used)
flask.render_template_string("{{ d }}", d=4)
assert len(cache) == 3
assert "{{ b }}" not in cache
assert list(cache.keys()) == ["{{ c }}", "{{ a }}", "{{ d }}"]
def test_template_string_cache_max_len(app, app_ctx):
"""Test that templates exceeding max length are not cached."""
# Set a small max length for testing
app.jinja_env.string_template_cache_max_len = 20
cache = app.jinja_env._string_template_cache
# Short template should be cached
short_source = "{{ x }}"
flask.render_template_string(short_source, x=1)
assert short_source in cache
# Long template should not be cached
long_source = "{{ x }}" + " " * 20 # exceeds max_len of 20
result = flask.render_template_string(long_source, x=2)
assert result == "2" + " " * 20
assert long_source not in cache