From 902976bf20d4bd527e9e0ba4e49960081e4b4686 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 16 Apr 2025 09:02:47 +0200 Subject: [PATCH] Adding upstream version 0.55+dfsg. Signed-off-by: Daniel Baumann --- Makefile | 12 ++++ pyproject.toml | 2 +- src/jinjax/__init__.py | 1 + src/jinjax/catalog.py | 30 ++++++---- src/jinjax/component.py | 7 ++- src/jinjax/exceptions.py | 11 ++++ src/jinjax/jinjax.py | 16 ++++-- src/jinjax/middleware.py | 2 +- src/jinjax/utils.py | 2 + tests/test_prefix.py | 47 ++++++++++++---- tests/test_render.py | 106 +++++++++++++++++++++++++++--------- tests/test_render_assets.py | 21 +++++-- tests/test_thread_safety.py | 4 +- uv.lock | 2 +- 14 files changed, 197 insertions(+), 66 deletions(-) diff --git a/Makefile b/Makefile index 5df4df6..de11d21 100644 --- a/Makefile +++ b/Makefile @@ -19,3 +19,15 @@ coverage: .PHONY: types types: uv run pyright src/jinjax + +.PHONY: docs +docs: + cd docs && uv run python docs.py + +.PHONY: docs-build +docs-build: + cd docs && uv run python docs.py build + +.PHONY: docs-deploy +docs-deploy: + cd docs && uv run sh deploy.sh diff --git a/pyproject.toml b/pyproject.toml index f865827..8e56b9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["setuptools"] [project] name = "jinjax" -version = "0.54" +version = "0.55" description = "Replace your HTML templates with Python server-Side components" authors = [ {name = "Juan Pablo Scaletti", email = "juanpablo@jpscaletti.com"}, diff --git a/src/jinjax/__init__.py b/src/jinjax/__init__.py index b8fc6cf..390ccd6 100644 --- a/src/jinjax/__init__.py +++ b/src/jinjax/__init__.py @@ -1,3 +1,4 @@ +from . import utils # noqa from .catalog import Catalog from .component import Component from .exceptions import ( diff --git a/src/jinjax/catalog.py b/src/jinjax/catalog.py index 949fa43..514576f 100644 --- a/src/jinjax/catalog.py +++ b/src/jinjax/catalog.py @@ -9,10 +9,18 @@ import jinja2 from markupsafe import Markup from .component import Component -from .exceptions import ComponentNotFound, InvalidArgument +from .exceptions import ComponentNotFound, InvalidArgument, UnknownPrefix from .html_attrs import HTMLAttrs from .jinjax import JinjaX -from .utils import DELIMITER, SLASH, get_random_id, get_url_prefix, kebab_case, logger +from .utils import ( + ARGS_PREFIX, + DELIMITER, + SLASH, + get_random_id, + get_url_prefix, + kebab_case, + logger, +) if t.TYPE_CHECKING: @@ -25,6 +33,7 @@ DEFAULT_PREFIX = "" DEFAULT_EXTENSION = ".jinja" ARGS_ATTRS = "attrs" ARGS_CONTENT = "content" +PREFIX_SEP = ":" # Create ContextVars containers at module level collected_css: dict[int, ContextVar[list[str]]] = {} @@ -488,7 +497,6 @@ class Catalog: f"were parsed incorrectly as:\n {str(kw)}" ) from exc - args["__prefix"] = component.prefix args[ARGS_CONTENT] = CallerWrapper(caller=caller, content=content) return component.render(**args) @@ -605,7 +613,7 @@ class Catalog: def _get_component(self, cname: str, **kw) -> Component: source = kw.pop("_source", kw.pop("__source", "")) file_ext = kw.pop("_file_ext", kw.pop("__file_ext", "")) or self.file_ext - caller_prefix = kw.pop("__prefix", "") + caller_prefix = kw.pop(ARGS_PREFIX, "") prefix, name = self._split_name(cname) component = None @@ -686,18 +694,18 @@ class Catalog: return path, relpath = paths component = Component(name=name, prefix=prefix, path=path, relpath=relpath) - component.tmpl = self.jinja_env.get_template(str(relpath), globals=self.tmpl_globals) + component.tmpl = self.jinja_env.get_template(str(relpath.as_posix()), globals=self.tmpl_globals) return component def _split_name(self, cname: str) -> tuple[str, str]: cname = cname.strip().strip(DELIMITER) - if DELIMITER not in cname: + if PREFIX_SEP not in cname: return DEFAULT_PREFIX, cname - for prefix in self.prefixes.keys(): - _prefix = f"{prefix}{DELIMITER}" - if cname.startswith(_prefix): - return prefix, cname.removeprefix(_prefix) - return DEFAULT_PREFIX, cname + + prefix, cname = cname.split(PREFIX_SEP, 1) + if prefix not in self.prefixes: + raise UnknownPrefix(prefix) + return prefix, cname def _get_component_path( self, diff --git a/src/jinjax/component.py b/src/jinjax/component.py index 78795bd..b2b5afd 100644 --- a/src/jinjax/component.py +++ b/src/jinjax/component.py @@ -12,7 +12,7 @@ from .exceptions import ( InvalidArgument, MissingRequiredArgument, ) -from .utils import get_url_prefix +from .utils import ARGS_PREFIX, get_url_prefix if t.TYPE_CHECKING: @@ -105,11 +105,11 @@ class Component: self.load_metadata(source) if path is not None and relpath is not None: - default_css = str(relpath.with_suffix(".css")) + default_css = str(relpath.with_suffix(".css").as_posix()) if (path.with_suffix(".css")).is_file(): self.css.extend(self.parse_files_expr(default_css)) - default_js = str(relpath.with_suffix(".js")) + default_js = str(relpath.with_suffix(".js").as_posix()) if (path.with_suffix(".js")).is_file(): self.js.extend(self.parse_files_expr(default_js)) @@ -254,6 +254,7 @@ class Component: def render(self, **kwargs): assert self.tmpl, f"Component {self.name} has no template" + kwargs.setdefault(ARGS_PREFIX, self.prefix) html = self.tmpl.render(**kwargs).strip() return Markup(html) diff --git a/src/jinjax/exceptions.py b/src/jinjax/exceptions.py index 9624076..7f0fffe 100644 --- a/src/jinjax/exceptions.py +++ b/src/jinjax/exceptions.py @@ -35,3 +35,14 @@ class InvalidArgument(Exception): Raised when the arguments passed to the component cannot be parsed by JinjaX because of an invalid syntax. """ + + +class UnknownPrefix(Exception): + """ + Raised when a component is used/invoked with a prefix that is + not registered. + """ + + def __init__(self, name: str) -> None: + msg = f"The prefix `{name}` is not registered" + super().__init__(msg) diff --git a/src/jinjax/jinjax.py b/src/jinjax/jinjax.py index c4966ae..65e225d 100644 --- a/src/jinjax/jinjax.py +++ b/src/jinjax/jinjax.py @@ -6,19 +6,23 @@ from jinja2.exceptions import TemplateSyntaxError from jinja2.ext import Extension from jinja2.filters import do_forceescape -from .utils import logger +from .utils import ARGS_PREFIX, logger RENDER_CMD = "catalog.irender" -BLOCK_CALL = '{% call(_slot="") [CMD]("[TAG]", __prefix=__prefix[ATTRS]) -%}[CONTENT]{%- endcall %}' -BLOCK_CALL = BLOCK_CALL.replace("[CMD]", RENDER_CMD) -INLINE_CALL = '{{ [CMD]("[TAG]", __prefix=__prefix[ATTRS]) }}' -INLINE_CALL = INLINE_CALL.replace("[CMD]", RENDER_CMD) + +BLOCK_CALL = '{% call(_slot="") [CMD]("[TAG]", [ARGS_PREFIX]=[ARGS_PREFIX][ATTRS]) -%}[CONTENT]{%- endcall %}' +BLOCK_CALL = BLOCK_CALL.replace("[CMD]", RENDER_CMD).replace("[ARGS_PREFIX]", ARGS_PREFIX) + +INLINE_CALL = '{{ [CMD]("[TAG]", [ARGS_PREFIX]=[ARGS_PREFIX][ATTRS]) }}' +INLINE_CALL = INLINE_CALL.replace("[CMD]", RENDER_CMD).replace("[ARGS_PREFIX]", ARGS_PREFIX) re_raw = r"\{%-?\s*raw\s*-?%\}.+?\{%-?\s*endraw\s*-?%\}" RX_RAW = re.compile(re_raw, re.DOTALL) -re_tag_name = r"([0-9A-Za-z_-]+\.)*[A-Z][0-9A-Za-z_-]*" +re_tag_prefix = r"([0-9A-Za-z_-]+\:)?" +re_tag_path = r"([0-9A-Za-z_-]+\.)*[A-Z][0-9A-Za-z_-]*" +re_tag_name = rf"{re_tag_prefix}{re_tag_path}" re_raw_attrs = r"(?P[^\>]*)" re_tag = rf"<(?P{re_tag_name}){re_raw_attrs}\s*/?>" RX_TAG = re.compile(re_tag) diff --git a/src/jinjax/middleware.py b/src/jinjax/middleware.py index 4e8c52e..d83e60b 100644 --- a/src/jinjax/middleware.py +++ b/src/jinjax/middleware.py @@ -40,7 +40,7 @@ class ComponentsMiddleware(WhiteNoise): # type: ignore stem = fingerprinted.group(1) relpath = relpath.with_name(f"{stem}{ext}") - return super().find_file(str(relpath)) + return super().find_file(str(relpath.as_posix())) def add_file_to_dictionary( self, url: str, path: str, stat_cache: t.Any = None diff --git a/src/jinjax/utils.py b/src/jinjax/utils.py index 1748844..3bfe148 100644 --- a/src/jinjax/utils.py +++ b/src/jinjax/utils.py @@ -8,6 +8,8 @@ logger = logging.getLogger("jinjax") DELIMITER = "." SLASH = "/" +ARGS_PREFIX = "__prefix" + def get_url_prefix(prefix: str) -> str: url_prefix = prefix.strip().strip(f"{DELIMITER}{SLASH}").replace(DELIMITER, SLASH) diff --git a/tests/test_prefix.py b/tests/test_prefix.py index 379174f..a4b6cf8 100644 --- a/tests/test_prefix.py +++ b/tests/test_prefix.py @@ -1,13 +1,34 @@ +import jinja2 import pytest from markupsafe import Markup +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_prefix_namespace(catalog, folder, folder_t, autoescape): +def test_render_prefixed(catalog, folder, folder_t, autoescape, undefined): """Components mounted with a prefix should be able to import other components from the same folder without specifying the prefix. """ catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined + catalog.add_folder(folder_t, prefix="ui") + + (folder / "Test.jinja").write_text("") + + (folder_t / "Title.jinja").write_text("prefix") + + html = catalog.render("Test") + assert html == Markup("prefix") + + +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) +@pytest.mark.parametrize("autoescape", [True, False]) +def test_prefix_namespace(catalog, folder, folder_t, autoescape, undefined): + """Components mounted with a prefix should be able to import other components + from the same folder without specifying the prefix. + """ + catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined catalog.add_folder(folder_t, prefix="ui") (folder / "Title.jinja").write_text("parent") @@ -15,54 +36,60 @@ def test_prefix_namespace(catalog, folder, folder_t, autoescape): (folder_t / "Title.jinja").write_text("prefix") (folder_t / "Alert.jinja").write_text("") - html = catalog.render("ui.Alert") + html = catalog.render("ui:Alert") assert html == Markup("prefix") +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_prefix_namespace_sub(catalog, folder, folder_t, autoescape): +def test_prefix_namespace_sub(catalog, folder, folder_t, autoescape, undefined): """Components mounted with a prefix should be able to import other components from the same folder without specifying the prefix, even if those components are in a subfolder. """ catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined catalog.add_folder(folder_t, prefix="ui") - (folder / "sub").mkdir() - (folder_t / "sub").mkdir() (folder / "Title.jinja").write_text("parent") + (folder / "sub").mkdir() (folder / "sub" / "Title.jinja").write_text("sub/parent") (folder_t / "Title.jinja").write_text("sub") + (folder_t / "sub").mkdir() (folder_t / "sub" / "Title.jinja").write_text("sub/prefix") (folder_t / "Alert.jinja").write_text("<sub.Title />") - html = catalog.render("ui.Alert") + html = catalog.render("ui:Alert") assert html == Markup("sub/prefix") +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_prefix_fallback(catalog, folder, folder_t, autoescape): +def test_prefix_fallback(catalog, folder, folder_t, autoescape, undefined): """If a component is not found in the folder with the prefix, it should fallback to the no-prefix folders. """ catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined catalog.add_folder(folder_t, prefix="ui") (folder / "Title.jinja").write_text("parent") (folder_t / "Alert.jinja").write_text("<Title />") - html = catalog.render("ui.Alert") + html = catalog.render("ui:Alert") assert html == Markup("parent") +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_prefix_namespace_assets(catalog, folder, folder_t, autoescape): +def test_prefix_namespace_assets(catalog, folder, folder_t, autoescape, undefined): """Components import without specifying the prefix should also be able to auto-import their assets. """ catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined catalog.add_folder(folder_t, prefix="ui") (folder_t / "Title.jinja").write_text("prefix") @@ -73,7 +100,7 @@ def test_prefix_namespace_assets(catalog, folder, folder_t, autoescape): """) (folder_t / "Alert.jinja").write_text("<Layout><Title /></Layout>") - html = catalog.render("ui.Alert") + html = catalog.render("ui:Alert") assert html == Markup(""" <link rel="stylesheet" href="/static/components/ui/Title.css"> prefix diff --git a/tests/test_render.py b/tests/test_render.py index a9ec75c..0550028 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -9,9 +9,11 @@ from markupsafe import Markup import jinjax +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_render_simple(catalog, folder, autoescape): +def test_render_simple(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Greeting.jinja").write_text( """ @@ -23,9 +25,11 @@ def test_render_simple(catalog, folder, autoescape): assert html == Markup('<div class="greeting [&_a]:flex">Hello world!</div>') +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_render_source(catalog, autoescape): +def test_render_source(catalog, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined source = '{#def message #}\n<div class="greeting [&_a]:flex">{{ message }}</div>' expected = Markup('<div class="greeting [&_a]:flex">Hello world!</div>') @@ -38,9 +42,11 @@ def test_render_source(catalog, autoescape): assert expected == html +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_render_content(catalog, folder, autoescape): +def test_render_content(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Card.jinja").write_text(""" <section class="card"> @@ -60,6 +66,7 @@ def test_render_content(catalog, folder, autoescape): assert expected == html +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) @pytest.mark.parametrize( "source, expected", @@ -76,8 +83,10 @@ def test_render_mix_of_contentful_and_contentless_components( source, expected, autoescape, + undefined, ): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Icon.jinja").write_text('<i class="icon"></i>') (folder / "Title.jinja").write_text("<h1>{{ content }}</h1>") @@ -87,9 +96,11 @@ def test_render_mix_of_contentful_and_contentless_components( assert html == Markup(expected) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_composition(catalog, folder, autoescape): +def test_composition(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Greeting.jinja").write_text( """ @@ -138,9 +149,11 @@ def test_composition(catalog, folder, autoescape): ) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_just_properties(catalog, folder, autoescape): +def test_just_properties(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Lorem.jinja").write_text( """ @@ -181,9 +194,11 @@ def test_just_properties(catalog, folder, autoescape): ) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_global_values(catalog, folder, autoescape): +def test_global_values(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Global.jinja").write_text("""{{ globalvar }}""") message = "Hello world!" @@ -193,9 +208,11 @@ def test_global_values(catalog, folder, autoescape): assert message in html +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_required_attr_are_required(catalog, folder, autoescape): +def test_required_attr_are_required(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Greeting.jinja").write_text( """ @@ -208,12 +225,14 @@ def test_required_attr_are_required(catalog, folder, autoescape): catalog.render("Greeting") +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_subfolder(catalog, folder, autoescape): +def test_subfolder(catalog, folder, autoescape, undefined): """Components can be organized in subfolders and called using the dot notation. """ catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined sub = folder / "ui" sub.mkdir() @@ -224,9 +243,11 @@ def test_subfolder(catalog, folder, autoescape): assert html == Markup('<div class="tab">Meh</div>') +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_default_attr(catalog, folder, autoescape): +def test_default_attr(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Greeting.jinja").write_text( """ @@ -259,9 +280,11 @@ def test_default_attr(catalog, folder, autoescape): ) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_raw_content(catalog, folder, autoescape): +def test_raw_content(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Code.jinja").write_text(""" <pre class="code"> @@ -293,9 +316,11 @@ def test_raw_content(catalog, folder, autoescape): ) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_multiple_raw(catalog, folder, autoescape): +def test_multiple_raw(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "C.jinja").write_text(""" <div {{ attrs.render() }}></div> @@ -325,9 +350,11 @@ def test_multiple_raw(catalog, folder, autoescape): ) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_check_for_unclosed(catalog, folder, autoescape): +def test_check_for_unclosed(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Lorem.jinja").write_text(""" {#def ipsum=False #} @@ -348,9 +375,11 @@ def test_check_for_unclosed(catalog, folder, autoescape): raise +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_dict_as_attr(catalog, folder, autoescape): +def test_dict_as_attr(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "CitiesList.jinja").write_text(""" {#def cities #} @@ -370,9 +399,11 @@ def test_dict_as_attr(catalog, folder, autoescape): assert html == Markup("<p>Lima, Peru</p><p>New York, USA</p>") +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @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""" +def test_do_not_mess_with_external_jinja_env(folder_t, folder, autoescape, undefined): + """Fix https://github.com/jpsca/jinjax/issues/19 + """ (folder_t / "greeting.html").write_text("Jinja still works") (folder / "Greeting.jinja").write_text("JinjaX works") @@ -384,6 +415,7 @@ def test_do_not_mess_with_external_jinja_env(folder_t, folder, autoescape): jinja_env.filters = {"fil": lambda x: x} jinja_env.tests = {"tes": lambda x: x} jinja_env.autoescape = autoescape + jinja_env.undefined = undefined catalog = jinjax.Catalog( jinja_env=jinja_env, @@ -422,9 +454,11 @@ def test_do_not_mess_with_external_jinja_env(folder_t, folder, autoescape): assert "jinja2.ext.DebugExtension" not in jinja_env.extensions +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_auto_reload(catalog, folder, autoescape): +def test_auto_reload(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Layout.jinja").write_text(""" <html> @@ -482,9 +516,11 @@ def test_auto_reload(catalog, folder, autoescape): ) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_subcomponents(catalog, folder, autoescape): +def test_subcomponents(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined """Issue https://github.com/jpsca/jinjax/issues/32""" (folder / "Page.jinja").write_text(""" @@ -520,9 +556,11 @@ def test_subcomponents(catalog, folder, autoescape): assert html == Markup(expected.strip()) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_colon_in_attrs(catalog, folder, autoescape): +def test_colon_in_attrs(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "C.jinja").write_text(""" <div {{ attrs.render() }}></div> @@ -537,9 +575,11 @@ def test_colon_in_attrs(catalog, folder, autoescape): assert """<div hx-on:click="show = !show"></div>""" in html +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_template_globals(catalog, folder, autoescape): +def test_template_globals(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Input.jinja").write_text(""" {# def name, value #}<input type="text" name="{{name}}" value="{{value}}"> @@ -563,9 +603,11 @@ def test_template_globals(catalog, folder, autoescape): assert """<input type="hidden" name="csrft" value="abc">""" in html +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_template_globals_update_cache(catalog, folder, autoescape): +def test_template_globals_update_cache(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "CsrfToken.jinja").write_text( """<input type="hidden" name="csrft" value="{{csrf_token}}">""" @@ -581,9 +623,11 @@ def test_template_globals_update_cache(catalog, folder, autoescape): assert """<input type="hidden" name="csrft" value="xyz">""" in html +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_alpine_sintax(catalog, folder, autoescape): +def test_alpine_sintax(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Greeting.jinja").write_text(""" {#def message #} @@ -595,9 +639,11 @@ def test_alpine_sintax(catalog, folder, autoescape): assert html == Markup(expected) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_alpine_sintax_in_component(catalog, folder, autoescape): +def test_alpine_sintax_in_component(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Button.jinja").write_text( """<button {{ attrs.render() }}>{{ content }}</button>""" @@ -613,9 +659,11 @@ def test_alpine_sintax_in_component(catalog, folder, autoescape): assert html == Markup(expected) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_autoescaped_attrs(catalog, folder, autoescape): +def test_autoescaped_attrs(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "CheckboxItem.jinja").write_text( """<div {{ attrs.render(class="relative") }}></div>""" @@ -708,9 +756,11 @@ def test_autoescaped_attrs(catalog, folder, autoescape): ), ], ) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_strip_comment(catalog, folder, autoescape, template): +def test_strip_comment(catalog, folder, autoescape, template, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "A.jinja").write_text(template) @@ -767,9 +817,11 @@ def test_mixed_syntax(catalog, folder): assert html == Markup(expected) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_slots(catalog, folder, autoescape): +def test_slots(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Component.jinja").write_text( """ @@ -804,9 +856,11 @@ def test_slots(catalog, folder, autoescape): assert html == Markup(expected) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_kebab_cased_component_names(catalog, folder, autoescape): +def test_kebab_cased_component_names(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "a_tricky-FOLDER").mkdir() (folder / "kebab-folder").mkdir() diff --git a/tests/test_render_assets.py b/tests/test_render_assets.py index e4730d1..af1f862 100644 --- a/tests/test_render_assets.py +++ b/tests/test_render_assets.py @@ -1,12 +1,15 @@ from pathlib import Path +import jinja2 import pytest from markupsafe import Markup +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_render_assets(catalog, folder, autoescape): +def test_render_assets(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Greeting.jinja").write_text( """ @@ -72,9 +75,11 @@ def test_render_assets(catalog, folder, autoescape): ) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_cleanup_assets(catalog, folder, autoescape): +def test_cleanup_assets(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Layout.jinja").write_text(""" <html> @@ -122,9 +127,11 @@ def test_cleanup_assets(catalog, folder, autoescape): ) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_fingerprint_assets(catalog, folder: Path, autoescape): +def test_fingerprint_assets(catalog, folder: Path, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Layout.jinja").write_text(""" <html> @@ -150,9 +157,11 @@ def test_fingerprint_assets(catalog, folder: Path, autoescape): assert 'href="http://example.com/super.css' in html +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_auto_load_assets_with_same_name(catalog, folder, autoescape): +def test_auto_load_assets_with_same_name(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Layout.jinja").write_text( """{{ catalog.render_assets() }}\n{{ content }}""" @@ -194,9 +203,11 @@ def test_auto_load_assets_with_same_name(catalog, folder, autoescape): assert html == Markup(expected) +@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined]) @pytest.mark.parametrize("autoescape", [True, False]) -def test_auto_load_assets_for_kebab_cased_names(catalog, folder, autoescape): +def test_auto_load_assets_for_kebab_cased_names(catalog, folder, autoescape, undefined): catalog.jinja_env.autoescape = autoescape + catalog.jinja_env.undefined = undefined (folder / "Layout.jinja").write_text( """{{ catalog.render_assets() }}\n{{ content }}""" diff --git a/tests/test_thread_safety.py b/tests/test_thread_safety.py index 5443e8c..d210be0 100644 --- a/tests/test_thread_safety.py +++ b/tests/test_thread_safety.py @@ -26,8 +26,8 @@ class ThreadWithReturnValue(Thread): if self._target is not None: self._return = self._target(*self._args, **self._kwargs) - def join(self, *args): - Thread.join(self, *args) + def join(self, *args, **kwargs): + Thread.join(self, *args, **kwargs) return self._return diff --git a/uv.lock b/uv.lock index 64a29ce..fcff473 100644 --- a/uv.lock +++ b/uv.lock @@ -215,7 +215,7 @@ wheels = [ [[package]] name = "jinjax" -version = "0.54" +version = "0.55" source = { editable = "." } dependencies = [ { name = "jinja2" },