Fix TemplateResponse calls to use Starlette 1.1.0+ keyword-argument style
This commit is contained in:
committed by
GitHub
parent
00f76f33fd
commit
1a5c6aca08
@@ -0,0 +1,99 @@
|
||||
"""Regression test for Starlette 1.1.0+ TemplateResponse keyword-argument style.
|
||||
|
||||
Prior to this fix, the three HTML routes called:
|
||||
templates.TemplateResponse("name.html", {"request": request, ...})
|
||||
which passes the context dict as the second positional argument. With the
|
||||
updated Starlette/FastAPI versions shipped in NixOS unstable (Starlette 1.1.0,
|
||||
FastAPI 0.136.3) that positional argument is the template name, causing Jinja2
|
||||
to receive a dict as a cache key and raise:
|
||||
TypeError: unhashable type: 'dict'
|
||||
|
||||
The fix updates every call to use keyword arguments:
|
||||
templates.TemplateResponse(request=request, name="name.html", context={...})
|
||||
"""
|
||||
|
||||
import ast
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SERVER_PY = Path(__file__).resolve().parents[1] / "sovran_systemsos_web" / "server.py"
|
||||
|
||||
|
||||
def _template_response_calls(source: str):
|
||||
"""Return a list of ast.Call nodes that are TemplateResponse calls."""
|
||||
tree = ast.parse(source)
|
||||
calls = []
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call):
|
||||
continue
|
||||
func = node.func
|
||||
if isinstance(func, ast.Attribute) and func.attr == "TemplateResponse":
|
||||
calls.append(node)
|
||||
return calls
|
||||
|
||||
|
||||
class TemplateResponseSignatureTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.source = SERVER_PY.read_text()
|
||||
self.calls = _template_response_calls(self.source)
|
||||
|
||||
def test_at_least_one_template_response_call_found(self):
|
||||
self.assertGreater(len(self.calls), 0, "No TemplateResponse calls found in server.py")
|
||||
|
||||
def test_no_old_style_positional_dict_context(self):
|
||||
"""No TemplateResponse call should pass a dict literal as its second positional arg.
|
||||
|
||||
The old style was:
|
||||
templates.TemplateResponse("name.html", {"request": request, ...})
|
||||
where args[0] is a string and args[1] is a Dict node. That pattern
|
||||
triggers the Starlette 1.1.0 bug.
|
||||
"""
|
||||
for call in self.calls:
|
||||
positional = call.args
|
||||
if len(positional) >= 2 and isinstance(positional[1], ast.Dict):
|
||||
self.fail(
|
||||
f"Found old-style TemplateResponse call at line {call.lineno}: "
|
||||
"second positional argument is a dict literal. "
|
||||
"Use keyword arguments (request=, name=, context=) instead."
|
||||
)
|
||||
|
||||
def test_request_not_duplicated_in_context(self):
|
||||
"""The 'request' key must not appear inside the context= dict when
|
||||
request= is already passed as a dedicated keyword argument."""
|
||||
for call in self.calls:
|
||||
kw_dict = {kw.arg: kw.value for kw in call.keywords if isinstance(kw, ast.keyword)}
|
||||
|
||||
if "request" not in kw_dict:
|
||||
continue # no request= kwarg, nothing to check
|
||||
|
||||
context_node = kw_dict.get("context")
|
||||
if not isinstance(context_node, ast.Dict):
|
||||
continue
|
||||
|
||||
for key_node in context_node.keys:
|
||||
if isinstance(key_node, ast.Constant) and key_node.value == "request":
|
||||
self.fail(
|
||||
f"TemplateResponse at line {call.lineno} passes 'request' both as "
|
||||
"request= keyword argument and inside the context dict."
|
||||
)
|
||||
|
||||
def test_all_calls_use_keyword_arguments(self):
|
||||
"""Every TemplateResponse call should use keyword arguments for request, name,
|
||||
and context rather than relying on positional ordering."""
|
||||
for call in self.calls:
|
||||
kw_args = {kw.arg for kw in call.keywords if isinstance(kw, ast.keyword)}
|
||||
self.assertIn(
|
||||
"request",
|
||||
kw_args,
|
||||
f"TemplateResponse at line {call.lineno} is missing keyword argument 'request='.",
|
||||
)
|
||||
self.assertIn(
|
||||
"name",
|
||||
kw_args,
|
||||
f"TemplateResponse at line {call.lineno} is missing keyword argument 'name='.",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user