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 }}
""") content = '' expected = Markup(f'
\n{content}\n
') 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( """
{{ content }}
""" ) (folder / "Page.jinja").write_text( """ {#def message #} """ ) html = catalog.render("Page", message="Hello") print(html) assert ( """
Hello
""".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 #}
{{ content }}
""" ) (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 ( """
Hello
""".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("""
{{content}} """) (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)