test_api.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. import shutil
  2. import tempfile
  3. from pathlib import Path
  4. import pytest
  5. from jinja2 import ChainableUndefined
  6. from jinja2 import DebugUndefined
  7. from jinja2 import DictLoader
  8. from jinja2 import Environment
  9. from jinja2 import is_undefined
  10. from jinja2 import make_logging_undefined
  11. from jinja2 import meta
  12. from jinja2 import StrictUndefined
  13. from jinja2 import Template
  14. from jinja2 import TemplatesNotFound
  15. from jinja2 import Undefined
  16. from jinja2 import UndefinedError
  17. from jinja2.compiler import CodeGenerator
  18. from jinja2.runtime import Context
  19. from jinja2.utils import Cycler
  20. from jinja2.utils import pass_context
  21. from jinja2.utils import pass_environment
  22. from jinja2.utils import pass_eval_context
  23. class TestExtendedAPI:
  24. def test_item_and_attribute(self, env):
  25. from jinja2.sandbox import SandboxedEnvironment
  26. for env in Environment(), SandboxedEnvironment():
  27. tmpl = env.from_string("{{ foo.items()|list }}")
  28. assert tmpl.render(foo={"items": 42}) == "[('items', 42)]"
  29. tmpl = env.from_string('{{ foo|attr("items")()|list }}')
  30. assert tmpl.render(foo={"items": 42}) == "[('items', 42)]"
  31. tmpl = env.from_string('{{ foo["items"] }}')
  32. assert tmpl.render(foo={"items": 42}) == "42"
  33. def test_finalize(self):
  34. e = Environment(finalize=lambda v: "" if v is None else v)
  35. t = e.from_string("{% for item in seq %}|{{ item }}{% endfor %}")
  36. assert t.render(seq=(None, 1, "foo")) == "||1|foo"
  37. def test_finalize_constant_expression(self):
  38. e = Environment(finalize=lambda v: "" if v is None else v)
  39. t = e.from_string("<{{ none }}>")
  40. assert t.render() == "<>"
  41. def test_no_finalize_template_data(self):
  42. e = Environment(finalize=lambda v: type(v).__name__)
  43. t = e.from_string("<{{ value }}>")
  44. # If template data was finalized, it would print "strintstr".
  45. assert t.render(value=123) == "<int>"
  46. def test_context_finalize(self):
  47. @pass_context
  48. def finalize(context, value):
  49. return value * context["scale"]
  50. e = Environment(finalize=finalize)
  51. t = e.from_string("{{ value }}")
  52. assert t.render(value=5, scale=3) == "15"
  53. def test_eval_finalize(self):
  54. @pass_eval_context
  55. def finalize(eval_ctx, value):
  56. return str(eval_ctx.autoescape) + value
  57. e = Environment(finalize=finalize, autoescape=True)
  58. t = e.from_string("{{ value }}")
  59. assert t.render(value="<script>") == "True&lt;script&gt;"
  60. def test_env_autoescape(self):
  61. @pass_environment
  62. def finalize(env, value):
  63. return " ".join(
  64. (env.variable_start_string, repr(value), env.variable_end_string)
  65. )
  66. e = Environment(finalize=finalize)
  67. t = e.from_string("{{ value }}")
  68. assert t.render(value="hello") == "{{ 'hello' }}"
  69. def test_cycler(self, env):
  70. items = 1, 2, 3
  71. c = Cycler(*items)
  72. for item in items + items:
  73. assert c.current == item
  74. assert next(c) == item
  75. next(c)
  76. assert c.current == 2
  77. c.reset()
  78. assert c.current == 1
  79. def test_expressions(self, env):
  80. expr = env.compile_expression("foo")
  81. assert expr() is None
  82. assert expr(foo=42) == 42
  83. expr2 = env.compile_expression("foo", undefined_to_none=False)
  84. assert is_undefined(expr2())
  85. expr = env.compile_expression("42 + foo")
  86. assert expr(foo=42) == 84
  87. def test_template_passthrough(self, env):
  88. t = Template("Content")
  89. assert env.get_template(t) is t
  90. assert env.select_template([t]) is t
  91. assert env.get_or_select_template([t]) is t
  92. assert env.get_or_select_template(t) is t
  93. def test_get_template_undefined(self, env):
  94. """Passing Undefined to get/select_template raises an
  95. UndefinedError or shows the undefined message in the list.
  96. """
  97. env.loader = DictLoader({})
  98. t = Undefined(name="no_name_1")
  99. with pytest.raises(UndefinedError):
  100. env.get_template(t)
  101. with pytest.raises(UndefinedError):
  102. env.get_or_select_template(t)
  103. with pytest.raises(UndefinedError):
  104. env.select_template(t)
  105. with pytest.raises(TemplatesNotFound) as exc_info:
  106. env.select_template([t, "no_name_2"])
  107. exc_message = str(exc_info.value)
  108. assert "'no_name_1' is undefined" in exc_message
  109. assert "no_name_2" in exc_message
  110. def test_autoescape_autoselect(self, env):
  111. def select_autoescape(name):
  112. if name is None or "." not in name:
  113. return False
  114. return name.endswith(".html")
  115. env = Environment(
  116. autoescape=select_autoescape,
  117. loader=DictLoader({"test.txt": "{{ foo }}", "test.html": "{{ foo }}"}),
  118. )
  119. t = env.get_template("test.txt")
  120. assert t.render(foo="<foo>") == "<foo>"
  121. t = env.get_template("test.html")
  122. assert t.render(foo="<foo>") == "&lt;foo&gt;"
  123. t = env.from_string("{{ foo }}")
  124. assert t.render(foo="<foo>") == "<foo>"
  125. def test_sandbox_max_range(self, env):
  126. from jinja2.sandbox import MAX_RANGE
  127. from jinja2.sandbox import SandboxedEnvironment
  128. env = SandboxedEnvironment()
  129. t = env.from_string("{% for item in range(total) %}{{ item }}{% endfor %}")
  130. with pytest.raises(OverflowError):
  131. t.render(total=MAX_RANGE + 1)
  132. class TestMeta:
  133. def test_find_undeclared_variables(self, env):
  134. ast = env.parse("{% set foo = 42 %}{{ bar + foo }}")
  135. x = meta.find_undeclared_variables(ast)
  136. assert x == {"bar"}
  137. ast = env.parse(
  138. "{% set foo = 42 %}{{ bar + foo }}"
  139. "{% macro meh(x) %}{{ x }}{% endmacro %}"
  140. "{% for item in seq %}{{ muh(item) + meh(seq) }}"
  141. "{% endfor %}"
  142. )
  143. x = meta.find_undeclared_variables(ast)
  144. assert x == {"bar", "seq", "muh"}
  145. ast = env.parse("{% for x in range(5) %}{{ x }}{% endfor %}{{ foo }}")
  146. x = meta.find_undeclared_variables(ast)
  147. assert x == {"foo"}
  148. def test_find_refererenced_templates(self, env):
  149. ast = env.parse('{% extends "layout.html" %}{% include helper %}')
  150. i = meta.find_referenced_templates(ast)
  151. assert next(i) == "layout.html"
  152. assert next(i) is None
  153. assert list(i) == []
  154. ast = env.parse(
  155. '{% extends "layout.html" %}'
  156. '{% from "test.html" import a, b as c %}'
  157. '{% import "meh.html" as meh %}'
  158. '{% include "muh.html" %}'
  159. )
  160. i = meta.find_referenced_templates(ast)
  161. assert list(i) == ["layout.html", "test.html", "meh.html", "muh.html"]
  162. def test_find_included_templates(self, env):
  163. ast = env.parse('{% include ["foo.html", "bar.html"] %}')
  164. i = meta.find_referenced_templates(ast)
  165. assert list(i) == ["foo.html", "bar.html"]
  166. ast = env.parse('{% include ("foo.html", "bar.html") %}')
  167. i = meta.find_referenced_templates(ast)
  168. assert list(i) == ["foo.html", "bar.html"]
  169. ast = env.parse('{% include ["foo.html", "bar.html", foo] %}')
  170. i = meta.find_referenced_templates(ast)
  171. assert list(i) == ["foo.html", "bar.html", None]
  172. ast = env.parse('{% include ("foo.html", "bar.html", foo) %}')
  173. i = meta.find_referenced_templates(ast)
  174. assert list(i) == ["foo.html", "bar.html", None]
  175. class TestStreaming:
  176. def test_basic_streaming(self, env):
  177. t = env.from_string(
  178. "<ul>{% for item in seq %}<li>{{ loop.index }} - {{ item }}</li>"
  179. "{%- endfor %}</ul>"
  180. )
  181. stream = t.stream(seq=list(range(3)))
  182. assert next(stream) == "<ul>"
  183. assert "".join(stream) == "<li>1 - 0</li><li>2 - 1</li><li>3 - 2</li></ul>"
  184. def test_buffered_streaming(self, env):
  185. tmpl = env.from_string(
  186. "<ul>{% for item in seq %}<li>{{ loop.index }} - {{ item }}</li>"
  187. "{%- endfor %}</ul>"
  188. )
  189. stream = tmpl.stream(seq=list(range(3)))
  190. stream.enable_buffering(size=3)
  191. assert next(stream) == "<ul><li>1"
  192. assert next(stream) == " - 0</li>"
  193. def test_streaming_behavior(self, env):
  194. tmpl = env.from_string("")
  195. stream = tmpl.stream()
  196. assert not stream.buffered
  197. stream.enable_buffering(20)
  198. assert stream.buffered
  199. stream.disable_buffering()
  200. assert not stream.buffered
  201. def test_dump_stream(self, env):
  202. tmp = Path(tempfile.mkdtemp())
  203. try:
  204. tmpl = env.from_string("\u2713")
  205. stream = tmpl.stream()
  206. stream.dump(str(tmp / "dump.txt"), "utf-8")
  207. assert (tmp / "dump.txt").read_bytes() == b"\xe2\x9c\x93"
  208. finally:
  209. shutil.rmtree(tmp)
  210. class TestUndefined:
  211. def test_stopiteration_is_undefined(self):
  212. def test():
  213. raise StopIteration()
  214. t = Template("A{{ test() }}B")
  215. assert t.render(test=test) == "AB"
  216. t = Template("A{{ test().missingattribute }}B")
  217. pytest.raises(UndefinedError, t.render, test=test)
  218. def test_undefined_and_special_attributes(self):
  219. with pytest.raises(AttributeError):
  220. Undefined("Foo").__dict__ # noqa B018
  221. def test_undefined_attribute_error(self):
  222. # Django's LazyObject turns the __class__ attribute into a
  223. # property that resolves the wrapped function. If that wrapped
  224. # function raises an AttributeError, printing the repr of the
  225. # object in the undefined message would cause a RecursionError.
  226. class Error:
  227. @property # type: ignore
  228. def __class__(self):
  229. raise AttributeError()
  230. u = Undefined(obj=Error(), name="hello")
  231. with pytest.raises(UndefinedError):
  232. getattr(u, "recursion", None)
  233. def test_logging_undefined(self):
  234. _messages = []
  235. class DebugLogger:
  236. def warning(self, msg, *args):
  237. _messages.append("W:" + msg % args)
  238. def error(self, msg, *args):
  239. _messages.append("E:" + msg % args)
  240. logging_undefined = make_logging_undefined(DebugLogger())
  241. env = Environment(undefined=logging_undefined)
  242. assert env.from_string("{{ missing }}").render() == ""
  243. pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render)
  244. assert env.from_string("{{ missing|list }}").render() == "[]"
  245. assert env.from_string("{{ missing is not defined }}").render() == "True"
  246. assert env.from_string("{{ foo.missing }}").render(foo=42) == ""
  247. assert env.from_string("{{ not missing }}").render() == "True"
  248. assert _messages == [
  249. "W:Template variable warning: 'missing' is undefined",
  250. "E:Template variable error: 'missing' is undefined",
  251. "W:Template variable warning: 'missing' is undefined",
  252. "W:Template variable warning: 'int object' has no attribute 'missing'",
  253. "W:Template variable warning: 'missing' is undefined",
  254. ]
  255. def test_default_undefined(self):
  256. env = Environment(undefined=Undefined)
  257. assert env.from_string("{{ missing }}").render() == ""
  258. pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render)
  259. assert env.from_string("{{ missing|list }}").render() == "[]"
  260. assert env.from_string("{{ missing is not defined }}").render() == "True"
  261. assert env.from_string("{{ foo.missing }}").render(foo=42) == ""
  262. assert env.from_string("{{ not missing }}").render() == "True"
  263. pytest.raises(UndefinedError, env.from_string("{{ missing - 1}}").render)
  264. assert env.from_string("{{ 'foo' in missing }}").render() == "False"
  265. und1 = Undefined(name="x")
  266. und2 = Undefined(name="y")
  267. assert und1 == und2
  268. assert und1 != 42
  269. assert hash(und1) == hash(und2) == hash(Undefined())
  270. with pytest.raises(AttributeError):
  271. getattr(Undefined, "__slots__") # noqa: B009
  272. def test_chainable_undefined(self):
  273. env = Environment(undefined=ChainableUndefined)
  274. # The following tests are copied from test_default_undefined
  275. assert env.from_string("{{ missing }}").render() == ""
  276. assert env.from_string("{{ missing|list }}").render() == "[]"
  277. assert env.from_string("{{ missing is not defined }}").render() == "True"
  278. assert env.from_string("{{ foo.missing }}").render(foo=42) == ""
  279. assert env.from_string("{{ not missing }}").render() == "True"
  280. pytest.raises(UndefinedError, env.from_string("{{ missing - 1}}").render)
  281. with pytest.raises(AttributeError):
  282. getattr(ChainableUndefined, "__slots__") # noqa: B009
  283. # The following tests ensure subclass functionality works as expected
  284. assert env.from_string('{{ missing.bar["baz"] }}').render() == ""
  285. assert env.from_string('{{ foo.bar["baz"]._undefined_name }}').render() == "foo"
  286. assert (
  287. env.from_string('{{ foo.bar["baz"]._undefined_name }}').render(foo=42)
  288. == "bar"
  289. )
  290. assert (
  291. env.from_string('{{ foo.bar["baz"]._undefined_name }}').render(
  292. foo={"bar": 42}
  293. )
  294. == "baz"
  295. )
  296. def test_debug_undefined(self):
  297. env = Environment(undefined=DebugUndefined)
  298. assert env.from_string("{{ missing }}").render() == "{{ missing }}"
  299. pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render)
  300. assert env.from_string("{{ missing|list }}").render() == "[]"
  301. assert env.from_string("{{ missing is not defined }}").render() == "True"
  302. assert (
  303. env.from_string("{{ foo.missing }}").render(foo=42)
  304. == "{{ no such element: int object['missing'] }}"
  305. )
  306. assert env.from_string("{{ not missing }}").render() == "True"
  307. undefined_hint = "this is testing undefined hint of DebugUndefined"
  308. assert (
  309. str(DebugUndefined(hint=undefined_hint))
  310. == f"{{{{ undefined value printed: {undefined_hint} }}}}"
  311. )
  312. with pytest.raises(AttributeError):
  313. getattr(DebugUndefined, "__slots__") # noqa: B009
  314. def test_strict_undefined(self):
  315. env = Environment(undefined=StrictUndefined)
  316. pytest.raises(UndefinedError, env.from_string("{{ missing }}").render)
  317. pytest.raises(UndefinedError, env.from_string("{{ missing.attribute }}").render)
  318. pytest.raises(UndefinedError, env.from_string("{{ missing|list }}").render)
  319. pytest.raises(UndefinedError, env.from_string("{{ 'foo' in missing }}").render)
  320. assert env.from_string("{{ missing is not defined }}").render() == "True"
  321. pytest.raises(
  322. UndefinedError, env.from_string("{{ foo.missing }}").render, foo=42
  323. )
  324. pytest.raises(UndefinedError, env.from_string("{{ not missing }}").render)
  325. assert (
  326. env.from_string('{{ missing|default("default", true) }}').render()
  327. == "default"
  328. )
  329. with pytest.raises(AttributeError):
  330. getattr(StrictUndefined, "__slots__") # noqa: B009
  331. assert env.from_string('{{ "foo" if false }}').render() == ""
  332. def test_indexing_gives_undefined(self):
  333. t = Template("{{ var[42].foo }}")
  334. pytest.raises(UndefinedError, t.render, var=0)
  335. def test_none_gives_proper_error(self):
  336. with pytest.raises(UndefinedError, match="'None' has no attribute 'split'"):
  337. Environment().getattr(None, "split")()
  338. def test_object_repr(self):
  339. with pytest.raises(
  340. UndefinedError, match="'int object' has no attribute 'upper'"
  341. ):
  342. Undefined(obj=42, name="upper")()
  343. class TestLowLevel:
  344. def test_custom_code_generator(self):
  345. class CustomCodeGenerator(CodeGenerator):
  346. def visit_Const(self, node, frame=None):
  347. # This method is pure nonsense, but works fine for testing...
  348. if node.value == "foo":
  349. self.write(repr("bar"))
  350. else:
  351. super().visit_Const(node, frame)
  352. class CustomEnvironment(Environment):
  353. code_generator_class = CustomCodeGenerator
  354. env = CustomEnvironment()
  355. tmpl = env.from_string('{% set foo = "foo" %}{{ foo }}')
  356. assert tmpl.render() == "bar"
  357. def test_custom_context(self):
  358. class CustomContext(Context):
  359. def resolve_or_missing(self, key):
  360. return "resolve-" + key
  361. class CustomEnvironment(Environment):
  362. context_class = CustomContext
  363. env = CustomEnvironment()
  364. tmpl = env.from_string("{{ foo }}")
  365. assert tmpl.render() == "resolve-foo"