import time
from pathlib import Path
from textwrap import dedent
from threading import Thread
import jinja2
import pytest
from jinja2.exceptions import TemplateSyntaxError
from markupsafe import Markup
import jinjax
@pytest.mark.parametrize("autoescape", [True, False])
def test_render_simple(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Greeting.jinja").write_text(
"""
{#def message #}
{{ message }}
"""
)
html = catalog.render("Greeting", message="Hello world!")
assert html == Markup('Hello world!
')
@pytest.mark.parametrize("autoescape", [True, False])
def test_render_source(catalog, autoescape):
catalog.jinja_env.autoescape = autoescape
source = '{#def message #}\n{{ message }}
'
expected = Markup('Hello world!
')
html = catalog.render("Greeting", message="Hello world!", _source=source)
assert expected == html
# Legacy
html = catalog.render("Greeting", message="Hello world!", __source=source)
assert expected == html
@pytest.mark.parametrize("autoescape", [True, False])
def test_render_content(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Card.jinja").write_text("""
""")
content = ''
expected = Markup(f'')
html = catalog.render("Card", _content=content)
print(html)
assert expected == html
# Legacy
html = catalog.render("Card", __content=content)
assert expected == html
@pytest.mark.parametrize("autoescape", [True, False])
@pytest.mark.parametrize(
"source, expected",
[
("HiHi", "Hi
Hi
"),
("", ''),
("Hi", 'Hi
'),
("Hi", 'Hi
'),
],
)
def test_render_mix_of_contentful_and_contentless_components(
catalog,
folder,
source,
expected,
autoescape,
):
catalog.jinja_env.autoescape = autoescape
(folder / "Icon.jinja").write_text('')
(folder / "Title.jinja").write_text("{{ content }}
")
(folder / "Page.jinja").write_text(source)
html = catalog.render("Page")
assert html == Markup(expected)
@pytest.mark.parametrize("autoescape", [True, False])
def test_composition(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Greeting.jinja").write_text(
"""
{#def message #}
{{ message }}
"""
)
(folder / "CloseBtn.jinja").write_text(
"""
{#def disabled=False -#}
"""
)
(folder / "Card.jinja").write_text(
"""
"""
)
(folder / "Page.jinja").write_text(
"""
{#def message #}
"""
)
html = catalog.render("Page", message="Hello")
print(html)
assert (
"""
""".strip()
in html
)
@pytest.mark.parametrize("autoescape", [True, False])
def test_just_properties(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Lorem.jinja").write_text(
"""
{#def ipsum=False #}
lorem {{ "ipsum" if ipsum else "lorem" }}
"""
)
(folder / "Layout.jinja").write_text(
"""
{{ content }}
"""
)
(folder / "Page.jinja").write_text(
"""
meh
"""
)
html = catalog.render("Page")
print(html)
assert (
"""
lorem ipsum
meh
lorem lorem
""".strip()
in html
)
@pytest.mark.parametrize("autoescape", [True, False])
def test_render_assets(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Greeting.jinja").write_text(
"""
{#def message #}
{#css greeting.css, http://example.com/super.css #}
{#js greeting.js #}
{{ message }}
"""
)
(folder / "Card.jinja").write_text(
"""
{#css https://somewhere.com/style.css, card.css #}
{#js card.js, shared.js #}
"""
)
(folder / "Layout.jinja").write_text(
"""
{{ catalog.render_assets() }}
{{ content }}
"""
)
(folder / "Page.jinja").write_text(
"""
{#def message #}
{#js https://somewhere.com/blabla.js, shared.js #}
"""
)
html = catalog.render("Page", message="Hello")
print(html)
assert (
"""
""".strip()
in html
)
@pytest.mark.parametrize("autoescape", [True, False])
def test_global_values(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Global.jinja").write_text("""{{ globalvar }}""")
message = "Hello world!"
catalog.jinja_env.globals["globalvar"] = message
html = catalog.render("Global")
print(html)
assert message in html
@pytest.mark.parametrize("autoescape", [True, False])
def test_required_attr_are_required(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Greeting.jinja").write_text(
"""
{#def message #}
{{ message }}
"""
)
with pytest.raises(jinjax.MissingRequiredArgument):
catalog.render("Greeting")
@pytest.mark.parametrize("autoescape", [True, False])
def test_subfolder(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
sub = folder / "UI"
sub.mkdir()
(folder / "Meh.jinja").write_text("Meh")
(sub / "Tab.jinja").write_text('{{ content }}
')
html = catalog.render("Meh")
assert html == Markup('Meh
')
@pytest.mark.parametrize("autoescape", [True, False])
def test_default_attr(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Greeting.jinja").write_text(
"""
{#def message="Hello", world=False #}
{{ message }}{% if world %} World{% endif %}
"""
)
(folder / "Page.jinja").write_text(
"""
"""
)
html = catalog.render("Page", message="Hello")
print(html)
assert (
"""
Hello
Hi
Hello
Hello World
Hello World
""".strip()
in html
)
@pytest.mark.parametrize("autoescape", [True, False])
def test_raw_content(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Code.jinja").write_text("""
{{ content|e }}
""")
(folder / "Page.jinja").write_text("""
{% raw -%}
{#def message="Hello", world=False #}
{{ message }}{% if world %} World{% endif %}
{%- endraw %}
""")
html = catalog.render("Page")
print(html)
assert (
"""
{#def message="Hello", world=False #}
<Header />
<div>{{ message }}{% if world %} World{% endif %}</div>
""".strip()
in html
)
@pytest.mark.parametrize("autoescape", [True, False])
def test_multiple_raw(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "C.jinja").write_text("""
""")
(folder / "Page.jinja").write_text("""
{% raw -%}
{%- endraw %}
{% raw %}{% endraw %}
""")
html = catalog.render("Page", message="Hello")
print(html)
assert (
"""
<C id="2" />
<C id="4" />
""".strip()
in html
)
@pytest.mark.parametrize("autoescape", [True, False])
def test_check_for_unclosed(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Lorem.jinja").write_text("""
{#def ipsum=False #}
lorem {{ "ipsum" if ipsum else "lorem" }}
""")
(folder / "Page.jinja").write_text("""
""")
with pytest.raises(TemplateSyntaxError):
try:
catalog.render("Page")
except TemplateSyntaxError as err:
print(err)
raise
@pytest.mark.parametrize("autoescape", [True, False])
def test_dict_as_attr(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "CitiesList.jinja").write_text("""
{#def cities #}
{% for city, country in cities.items() -%}
{{ city }}, {{ country }}
{%- endfor %}
""")
(folder / "Page.jinja").write_text("""
""")
html = catalog.render("Page")
assert html == Markup("Lima, Peru
New York, USA
")
@pytest.mark.parametrize("autoescape", [True, False])
def test_cleanup_assets(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Layout.jinja").write_text("""
{{ catalog.render_assets() }}
{{ content }}
""")
(folder / "Foo.jinja").write_text("""
{#js foo.js #}
Foo
""")
(folder / "Bar.jinja").write_text("""
{#js bar.js #}
Bar
""")
html = catalog.render("Foo")
print(html, "\n")
assert (
"""
Foo
""".strip()
in html
)
html = catalog.render("Bar")
print(html)
assert (
"""
Bar
""".strip()
in html
)
@pytest.mark.parametrize("autoescape", [True, False])
def test_do_not_mess_with_external_jinja_env(folder_t, folder, autoescape):
"""https://github.com/jpsca/jinjax/issues/19"""
(folder_t / "greeting.html").write_text("Jinja still works")
(folder / "Greeting.jinja").write_text("JinjaX works")
jinja_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(folder_t),
extensions=["jinja2.ext.i18n"],
)
jinja_env.globals = {"glo": "bar"}
jinja_env.filters = {"fil": lambda x: x}
jinja_env.tests = {"tes": lambda x: x}
jinja_env.autoescape = autoescape
catalog = jinjax.Catalog(
jinja_env=jinja_env,
extensions=["jinja2.ext.debug"],
globals={"xglo": "foo"},
filters={"xfil": lambda x: x},
tests={"xtes": lambda x: x},
)
catalog.add_folder(folder)
html = catalog.render("Greeting")
assert html == Markup("JinjaX works")
assert catalog.jinja_env.globals["catalog"] == catalog
assert catalog.jinja_env.globals["glo"] == "bar"
assert catalog.jinja_env.globals["xglo"] == "foo"
assert catalog.jinja_env.filters["fil"]
assert catalog.jinja_env.filters["xfil"]
assert catalog.jinja_env.tests["tes"]
assert catalog.jinja_env.tests["xtes"]
assert "jinja2.ext.InternationalizationExtension" in catalog.jinja_env.extensions
assert "jinja2.ext.DebugExtension" in catalog.jinja_env.extensions
assert "jinja2.ext.ExprStmtExtension" in catalog.jinja_env.extensions
tmpl = jinja_env.get_template("greeting.html")
assert tmpl.render() == "Jinja still works"
assert jinja_env.globals["catalog"] == catalog
assert jinja_env.globals["glo"] == "bar"
assert "xglo" not in jinja_env.globals
assert jinja_env.filters["fil"]
assert "xfil" not in jinja_env.filters
assert jinja_env.tests["tes"]
assert "xtes" not in jinja_env.tests
assert "jinja2.ext.InternationalizationExtension" in jinja_env.extensions
assert "jinja2.ext.DebugExtension" not in jinja_env.extensions
@pytest.mark.parametrize("autoescape", [True, False])
def test_auto_reload(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Layout.jinja").write_text("""
{{ content }}
""")
(folder / "Foo.jinja").write_text("""
Foo
""")
bar_file = folder / "Bar.jinja"
bar_file.write_text("Bar
")
html1 = catalog.render("Foo")
print(bar_file.stat().st_mtime)
print(html1, "\n")
assert (
"""
Foo
Bar
""".strip()
in html1
)
# Give it some time so the st_mtime are different
time.sleep(0.1)
catalog.auto_reload = False
bar_file.write_text("Ignored
")
print(bar_file.stat().st_mtime)
html2 = catalog.render("Foo")
print(html2, "\n")
catalog.auto_reload = True
bar_file.write_text("Updated
")
print(bar_file.stat().st_mtime)
html3 = catalog.render("Foo")
print(html3, "\n")
assert html1 == html2
assert (
"""
Foo
Updated
""".strip()
in html3
)
@pytest.mark.parametrize("autoescape", [True, False])
def test_subcomponents(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
"""Issue https://github.com/jpsca/jinjax/issues/32"""
(folder / "Page.jinja").write_text("""
{#def message #}
lorem ipsum
{{ message }}
""")
(folder / "Subcomponent.jinja").write_text("""
foo bar
""")
html = catalog.render("Page", message="<3")
if autoescape:
expected = """
lorem ipsum
foo bar
<3
"""
else:
expected = """
lorem ipsum
foo bar
<3
"""
assert html == Markup(expected.strip())
@pytest.mark.parametrize("autoescape", [True, False])
def test_fingerprint_assets(catalog, folder: Path, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Layout.jinja").write_text("""
{{ catalog.render_assets() }}
{{ content }}
""")
(folder / "Page.jinja").write_text("""
{#css app.css, http://example.com/super.css #}
{#js app.js #}
Hi
""")
(folder / "app.css").write_text("...")
catalog.fingerprint = True
html = catalog.render("Page", message="Hello")
print(html)
assert 'src="/static/components/app.js"' in html
assert 'href="/static/components/app-' in html
assert 'href="http://example.com/super.css' in html
@pytest.mark.parametrize("autoescape", [True, False])
def test_colon_in_attrs(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "C.jinja").write_text("""
""")
(folder / "Page.jinja").write_text("""
""")
html = catalog.render("Page", message="Hello")
print(html)
assert """""" in html
@pytest.mark.parametrize("autoescape", [True, False])
def test_template_globals(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Input.jinja").write_text("""
{# def name, value #}
""")
(folder / "CsrfToken.jinja").write_text("""
""")
(folder / "Form.jinja").write_text("""
""")
(folder / "Page.jinja").write_text("""
{# def value #}
""")
html = catalog.render("Page", value="bar", __globals={"csrf_token": "abc"})
print(html)
assert """""" in html
@pytest.mark.parametrize("autoescape", [True, False])
def test_template_globals_update_cache(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "CsrfToken.jinja").write_text(
""""""
)
(folder / "Page.jinja").write_text("""""")
html = catalog.render("Page", __globals={"csrf_token": "abc"})
print(html)
assert """""" in html
html = catalog.render("Page", __globals={"csrf_token": "xyz"})
print(html)
assert """""" in html
@pytest.mark.parametrize("autoescape", [True, False])
def test_alpine_sintax(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Greeting.jinja").write_text("""
{#def message #}
""")
html = catalog.render("Greeting", message="Hello world!")
print(html)
expected = """"""
assert html == Markup(expected)
@pytest.mark.parametrize("autoescape", [True, False])
def test_alpine_sintax_in_component(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Button.jinja").write_text(
""""""
)
(folder / "Greeting.jinja").write_text(
""""""
)
html = catalog.render("Greeting")
print(html)
expected = """"""
assert html == Markup(expected)
@pytest.mark.parametrize("autoescape", [True, False])
def test_autoescaped_attrs(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "CheckboxItem.jinja").write_text(
""""""
)
(folder / "Page.jinja").write_text(
""""""
)
html = catalog.render("Page")
print(html)
expected = """"""
assert html == Markup(expected)
@pytest.mark.parametrize(
"template",
[
pytest.param(
dedent(
"""
{# def
href,
hx_target="#maincontent",
hx_swap="innerHTML show:body:top",
hx_push_url=true,
#}
{{- content -}}
"""
),
id="no comment",
),
pytest.param(
dedent(
"""
{# def
href,
hx_target="#maincontent", # css selector
hx_swap="innerHTML show:body:top",
hx_push_url=true,
#}
{{- content -}}
"""
),
id="comment with # on line",
),
pytest.param(
dedent(
"""
{# def
href, # url of the target page
hx_target="#maincontent", # css selector
hx_swap="innerHTML show:body:top", # browse on top of the page
hx_push_url=true, # replace the url of the browser
#}
{{- content -}}
"""
),
id="many comments",
),
pytest.param(
dedent(
"""
{# def
href: str, # url of the target page
hx_target: str = "#maincontent", # css selector
hx_swap: str = "innerHTML show:body:top", # browse on top of the page
hx_push_url: bool = true, # replace the url
#}
{{- content -}}
"""
),
id="many comments and typing",
),
],
)
@pytest.mark.parametrize("autoescape", [True, False])
def test_strip_comment(catalog, folder, autoescape, template):
catalog.jinja_env.autoescape = autoescape
(folder / "A.jinja").write_text(template)
(folder / "Page.jinja").write_text("""Yolo""")
html = catalog.render("Page")
print(html)
expected = """
Yolo""".strip()
assert html == Markup(expected)
@pytest.mark.parametrize("autoescape", [True, False])
def test_auto_load_assets_with_same_name(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Layout.jinja").write_text(
"""{{ catalog.render_assets() }}\n{{ content }}"""
)
(folder / "FooBar.css").touch()
(folder / "common").mkdir()
(folder / "common" / "Form.jinja").write_text(
"""
{#js "shared.js" #}
"""
)
(folder / "common" / "Form.css").touch()
(folder / "common" / "Form.js").touch()
(folder / "Page.jinja").write_text(
"""
{#css "Page.css" #}
"""
)
(folder / "Page.css").touch()
(folder / "Page.js").touch()
html = catalog.render("Page")
print(html)
expected = """
""".strip()
assert html == Markup(expected)
def test_vue_like_syntax(catalog, folder):
(folder / "Test.jinja").write_text("""
{#def a, b, c, d #}
{{ a }} {{ b }} {{ c }} {{ d }}
""")
(folder / "Caller.jinja").write_text(
""""""
)
html = catalog.render("Caller")
print(html)
expected = """4 2+2 {'lorem': 'ipsum'} False""".strip()
assert html == Markup(expected)
def test_jinja_like_syntax(catalog, folder):
(folder / "Test.jinja").write_text("""
{#def a, b, c, d #}
{{ a }} {{ b }} {{ c }} {{ d }}
""")
(folder / "Caller.jinja").write_text(
""""""
)
html = catalog.render("Caller")
print(html)
expected = """4 2+2 {'lorem': 'ipsum'} False""".strip()
assert html == Markup(expected)
def test_mixed_syntax(catalog, folder):
(folder / "Test.jinja").write_text("""
{#def a, b, c, d #}
{{ a }} {{ b }} {{ c }} {{ d }}
""")
(folder / "Caller.jinja").write_text(
""""""
)
html = catalog.render("Caller")
print(html)
expected = """4 {{2+2}} {'lorem': 'ipsum'} False""".strip()
assert html == Markup(expected)
@pytest.mark.parametrize("autoescape", [True, False])
def test_slots(catalog, folder, autoescape):
catalog.jinja_env.autoescape = autoescape
(folder / "Component.jinja").write_text(
"""
{{ content }}
{{ content("first") }}
{{ content("second") }}
{{ content("antoher") }}
{{ content() }}
""".strip()
)
(folder / "Messages.jinja").write_text(
"""
{% if _slot == "first" %}Hello World
{%- elif _slot == "second" %}Lorem Ipsum
{%- elif _slot == "meh" %}QWERTYUIOP
{%- else %}Default{% endif %}
""".strip()
)
html = catalog.render("Messages")
print(html)
expected = """
Default
Hello World
Lorem Ipsum
Default
Default
""".strip()
assert html == Markup(expected)
class ThreadWithReturnValue(Thread):
def __init__(self, group=None, target=None, name=None, args=None, kwargs=None):
args = args or ()
kwargs = kwargs or {}
Thread.__init__(
self,
group=group,
target=target,
name=name,
args=args,
kwargs=kwargs,
)
self._target = target
self._args = args
self._kwargs = kwargs
self._return = None
def run(self):
if self._target is not None:
self._return = self._target(*self._args, **self._kwargs)
def join(self, *args):
Thread.join(self, *args)
return self._return
def test_thread_safety_of_render_assets(catalog, folder):
NUM_THREADS = 5
child_tmpl = """
{#css "c{i}.css" #}
{#js "c{i}.js" #}
Child {i}
""".strip()
parent_tmpl = """
{{ catalog.render_assets() }}
{{ content }}""".strip()
comp_tmpl = """
{#css "a{i}.css", "b{i}.css" #}
{#js "a{i}.js", "b{i}.js" #}
""".strip()
expected_tmpl = """
Child {i}
""".strip()
def render(i):
return catalog.render(f"Page{i}")
for i in range(NUM_THREADS):
si = str(i)
child_name = f"Child{i}.jinja"
child_src = child_tmpl.replace("{i}", si)
parent_name = f"Parent{i}.jinja"
parent_src = parent_tmpl.replace("{i}", si)
comp_name = f"Page{i}.jinja"
comp_src = comp_tmpl.replace("{i}", si)
(folder / child_name).write_text(child_src)
(folder / comp_name).write_text(comp_src)
(folder / parent_name).write_text(parent_src)
threads = []
for i in range(NUM_THREADS):
thread = ThreadWithReturnValue(target=render, args=(i,))
threads.append(thread)
thread.start()
results = [thread.join() for thread in threads]
for i, result in enumerate(results):
expected = expected_tmpl.replace("{i}", str(i))
print(f"---- EXPECTED {i}----")
print(expected)
print(f"---- RESULT {i}----")
print(result)
assert result == Markup(expected)
def test_same_thread_assets_independence(catalog, folder):
catalog2 = jinjax.Catalog()
catalog2.add_folder(folder)
print(catalog._key)
print(catalog2._key)
(folder / "Parent.jinja").write_text("""
{{ catalog.render_assets() }}
{{ content }}""".strip())
(folder / "Comp1.jinja").write_text("""
{#css "a.css" #}
{#js "a.js" #}
""".strip())
(folder / "Comp2.jinja").write_text("""
{#css "b.css" #}
{#js "b.js" #}
""".strip())
expected_1 = """
""".strip()
expected_2 = """
""".strip()
html1 = catalog.render("Comp1")
# `irender` instead of `render` so the assets are not cleared
html2 = catalog2.irender("Comp2")
print(html1)
print(html2)
assert html1 == Markup(expected_1)
assert html2 == Markup(expected_2)