Files
responder/tests/test_templates_context.py
kennethreitz ea5ba0d26b Add standalone Router composition; fix lifespan/dispatch/header correctness; 8.0 behavior changes
The first 8.0 sweep. Flagship: responder.Router records route declarations
without an API instance (Flask-blueprint style) and api.include_router()
replays them with prefix/tags/dependencies/auth composition, nesting, and
conflict guards.

Correctness: lifespan= now composes with on_event (background draining runs
again), view-style default routes dispatch instead of 500ing, CBV HEAD falls
back to on_get, tolerant cookie parsing, case-insensitive resp.headers,
async background tasks, async GraphQL resolvers, Jinja builtins survive
context assignment, identity-based dependency cycle detection, WSGI mounts
wrap once, WebSocket crashes send a 1011 close frame, and dependency
metadata matches actual imports.

Security: Content-Disposition filename escaping in resp.download(); bounded,
lock-guarded MemorySessionBackend with expiry-first eviction.

8.0 behavior changes: no implicit static/ mkdir, url_for raises
RouteNotFoundError and percent-encodes values, redirect() defaults to 307
(permanent=True for 308), charset= on encoded text responses (no more fake
'Encoding' header), and RFC 9512 application/yaml.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 17:05:08 -04:00

103 lines
3.9 KiB
Python

"""Setting Templates.context must not wipe Jinja2's built-in globals."""
import asyncio
from responder.templates import Templates
def make_templates(tmp_path, **kwargs):
return Templates(directory=str(tmp_path), **kwargs)
def test_context_setter_preserves_jinja_builtins(tmp_path):
"""range/namespace/cycler/joiner/lipsum/dict survive a context assignment."""
templates = make_templates(tmp_path)
templates.context = {"greeting": "hi"}
for builtin in ("range", "namespace", "cycler", "joiner", "lipsum", "dict"):
assert builtin in templates.context
# A template using range() renders instead of raising UndefinedError.
out = templates.render_string("{% for i in range(3) %}{{ i }}{% endfor %}")
assert out == "012"
def test_context_setter_applies_values(tmp_path):
templates = make_templates(tmp_path)
templates.context = {"greeting": "hi"}
assert templates.render_string("{{ greeting }}") == "hi"
def test_context_setter_keeps_default_context(tmp_path):
templates = make_templates(tmp_path, context={"site": "responder"})
templates.context = {"greeting": "hi"}
assert templates.render_string("{{ site }}:{{ greeting }}") == "responder:hi"
def test_context_setter_replaces_previous_custom_keys(tmp_path):
"""Assignment semantics: stale custom keys from a prior set are dropped."""
templates = make_templates(tmp_path)
templates.context = {"old": "value"}
templates.context = {"new": "value"}
assert "new" in templates.context
assert "old" not in templates.context
# ...but the built-ins are still there.
assert "range" in templates.context
def test_context_setter_restores_shadowed_builtin(tmp_path):
"""Regression: a user key shadowing a Jinja built-in (e.g. ``range``) is
removed — and the built-in restored — when the context is reassigned."""
templates = make_templates(tmp_path)
templates.context = {"range": lambda *args: ["x"]}
out = templates.render_string("{% for i in range(2) %}{{ i }}{% endfor %}")
assert out == "x" # the fake is in effect
templates.context = {}
out = templates.render_string("{% for i in range(2) %}{{ i }}{% endfor %}")
assert out == "01" # the real built-in is back
assert templates.context["range"] is range
def test_context_setter_reshadow_then_unshadow_builtin(tmp_path):
templates = make_templates(tmp_path)
templates.context = {"range": lambda *args: ["x"]}
templates.context = {"range": lambda *args: ["y", "y"]} # still shadowed
out = templates.render_string("{% for i in range(2) %}{{ i }}{% endfor %}")
assert out == "yy"
templates.context = {"greeting": "hi"}
out = templates.render_string("{% for i in range(2) %}{{ i }}{% endfor %}")
assert out == "01"
def test_context_setter_override_wins_over_default(tmp_path):
templates = make_templates(tmp_path, context={"who": "default"})
templates.context = {"who": "override"}
assert templates.render_string("{{ who }}") == "override"
def test_context_setter_shares_globals_with_async_env(tmp_path):
"""The sync and async environments keep sharing one globals object."""
templates = make_templates(tmp_path)
templates.context = {"greeting": "hi"}
assert templates._env.globals is templates._async_env.globals
def test_render_async_sees_updated_context_and_builtins(tmp_path):
(tmp_path / "loop.html").write_text(
"{{ greeting }}{% for i in range(2) %}{{ i }}{% endfor %}"
)
templates = make_templates(tmp_path)
templates.context = {"greeting": "hi"}
out = asyncio.run(templates.render_async("loop.html"))
assert out == "hi01"
def test_render_with_builtins_without_context_set(tmp_path):
"""Pre-existing happy path: built-ins work when context is never assigned."""
templates = make_templates(tmp_path)
out = templates.render_string("{% for i in range(2) %}{{ i }}{% endfor %}")
assert out == "01"