1
0
Fork 0

Adding upstream version 0.55+dfsg.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-04-16 09:02:47 +02:00
parent 8090869090
commit 902976bf20
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
14 changed files with 197 additions and 66 deletions

View file

@ -19,3 +19,15 @@ coverage:
.PHONY: types .PHONY: types
types: types:
uv run pyright src/jinjax 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

View file

@ -4,7 +4,7 @@ requires = ["setuptools"]
[project] [project]
name = "jinjax" name = "jinjax"
version = "0.54" version = "0.55"
description = "Replace your HTML templates with Python server-Side components" description = "Replace your HTML templates with Python server-Side components"
authors = [ authors = [
{name = "Juan Pablo Scaletti", email = "juanpablo@jpscaletti.com"}, {name = "Juan Pablo Scaletti", email = "juanpablo@jpscaletti.com"},

View file

@ -1,3 +1,4 @@
from . import utils # noqa
from .catalog import Catalog from .catalog import Catalog
from .component import Component from .component import Component
from .exceptions import ( from .exceptions import (

View file

@ -9,10 +9,18 @@ import jinja2
from markupsafe import Markup from markupsafe import Markup
from .component import Component from .component import Component
from .exceptions import ComponentNotFound, InvalidArgument from .exceptions import ComponentNotFound, InvalidArgument, UnknownPrefix
from .html_attrs import HTMLAttrs from .html_attrs import HTMLAttrs
from .jinjax import JinjaX 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: if t.TYPE_CHECKING:
@ -25,6 +33,7 @@ DEFAULT_PREFIX = ""
DEFAULT_EXTENSION = ".jinja" DEFAULT_EXTENSION = ".jinja"
ARGS_ATTRS = "attrs" ARGS_ATTRS = "attrs"
ARGS_CONTENT = "content" ARGS_CONTENT = "content"
PREFIX_SEP = ":"
# Create ContextVars containers at module level # Create ContextVars containers at module level
collected_css: dict[int, ContextVar[list[str]]] = {} collected_css: dict[int, ContextVar[list[str]]] = {}
@ -488,7 +497,6 @@ class Catalog:
f"were parsed incorrectly as:\n {str(kw)}" f"were parsed incorrectly as:\n {str(kw)}"
) from exc ) from exc
args["__prefix"] = component.prefix
args[ARGS_CONTENT] = CallerWrapper(caller=caller, content=content) args[ARGS_CONTENT] = CallerWrapper(caller=caller, content=content)
return component.render(**args) return component.render(**args)
@ -605,7 +613,7 @@ class Catalog:
def _get_component(self, cname: str, **kw) -> Component: def _get_component(self, cname: str, **kw) -> Component:
source = kw.pop("_source", kw.pop("__source", "")) source = kw.pop("_source", kw.pop("__source", ""))
file_ext = kw.pop("_file_ext", kw.pop("__file_ext", "")) or self.file_ext 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) prefix, name = self._split_name(cname)
component = None component = None
@ -686,19 +694,19 @@ class Catalog:
return return
path, relpath = paths path, relpath = paths
component = Component(name=name, prefix=prefix, path=path, relpath=relpath) 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 return component
def _split_name(self, cname: str) -> tuple[str, str]: def _split_name(self, cname: str) -> tuple[str, str]:
cname = cname.strip().strip(DELIMITER) 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 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( def _get_component_path(
self, self,
prefix: str, prefix: str,

View file

@ -12,7 +12,7 @@ from .exceptions import (
InvalidArgument, InvalidArgument,
MissingRequiredArgument, MissingRequiredArgument,
) )
from .utils import get_url_prefix from .utils import ARGS_PREFIX, get_url_prefix
if t.TYPE_CHECKING: if t.TYPE_CHECKING:
@ -105,11 +105,11 @@ class Component:
self.load_metadata(source) self.load_metadata(source)
if path is not None and relpath is not None: 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(): if (path.with_suffix(".css")).is_file():
self.css.extend(self.parse_files_expr(default_css)) 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(): if (path.with_suffix(".js")).is_file():
self.js.extend(self.parse_files_expr(default_js)) self.js.extend(self.parse_files_expr(default_js))
@ -254,6 +254,7 @@ class Component:
def render(self, **kwargs): def render(self, **kwargs):
assert self.tmpl, f"Component {self.name} has no template" assert self.tmpl, f"Component {self.name} has no template"
kwargs.setdefault(ARGS_PREFIX, self.prefix)
html = self.tmpl.render(**kwargs).strip() html = self.tmpl.render(**kwargs).strip()
return Markup(html) return Markup(html)

View file

@ -35,3 +35,14 @@ class InvalidArgument(Exception):
Raised when the arguments passed to the component cannot be parsed Raised when the arguments passed to the component cannot be parsed
by JinjaX because of an invalid syntax. 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)

View file

@ -6,19 +6,23 @@ from jinja2.exceptions import TemplateSyntaxError
from jinja2.ext import Extension from jinja2.ext import Extension
from jinja2.filters import do_forceescape from jinja2.filters import do_forceescape
from .utils import logger from .utils import ARGS_PREFIX, logger
RENDER_CMD = "catalog.irender" RENDER_CMD = "catalog.irender"
BLOCK_CALL = '{% call(_slot="") [CMD]("[TAG]", __prefix=__prefix[ATTRS]) -%}[CONTENT]{%- endcall %}'
BLOCK_CALL = BLOCK_CALL.replace("[CMD]", RENDER_CMD) BLOCK_CALL = '{% call(_slot="") [CMD]("[TAG]", [ARGS_PREFIX]=[ARGS_PREFIX][ATTRS]) -%}[CONTENT]{%- endcall %}'
INLINE_CALL = '{{ [CMD]("[TAG]", __prefix=__prefix[ATTRS]) }}' BLOCK_CALL = BLOCK_CALL.replace("[CMD]", RENDER_CMD).replace("[ARGS_PREFIX]", ARGS_PREFIX)
INLINE_CALL = INLINE_CALL.replace("[CMD]", RENDER_CMD)
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*-?%\}" re_raw = r"\{%-?\s*raw\s*-?%\}.+?\{%-?\s*endraw\s*-?%\}"
RX_RAW = re.compile(re_raw, re.DOTALL) 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<attrs>[^\>]*)" re_raw_attrs = r"(?P<attrs>[^\>]*)"
re_tag = rf"<(?P<tag>{re_tag_name}){re_raw_attrs}\s*/?>" re_tag = rf"<(?P<tag>{re_tag_name}){re_raw_attrs}\s*/?>"
RX_TAG = re.compile(re_tag) RX_TAG = re.compile(re_tag)

View file

@ -40,7 +40,7 @@ class ComponentsMiddleware(WhiteNoise): # type: ignore
stem = fingerprinted.group(1) stem = fingerprinted.group(1)
relpath = relpath.with_name(f"{stem}{ext}") 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( def add_file_to_dictionary(
self, url: str, path: str, stat_cache: t.Any = None self, url: str, path: str, stat_cache: t.Any = None

View file

@ -8,6 +8,8 @@ logger = logging.getLogger("jinjax")
DELIMITER = "." DELIMITER = "."
SLASH = "/" SLASH = "/"
ARGS_PREFIX = "__prefix"
def get_url_prefix(prefix: str) -> str: def get_url_prefix(prefix: str) -> str:
url_prefix = prefix.strip().strip(f"{DELIMITER}{SLASH}").replace(DELIMITER, SLASH) url_prefix = prefix.strip().strip(f"{DELIMITER}{SLASH}").replace(DELIMITER, SLASH)

View file

@ -1,13 +1,34 @@
import jinja2
import pytest import pytest
from markupsafe import Markup from markupsafe import Markup
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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 """Components mounted with a prefix should be able to import other components
from the same folder without specifying the prefix. from the same folder without specifying the prefix.
""" """
catalog.jinja_env.autoescape = autoescape catalog.jinja_env.autoescape = autoescape
catalog.jinja_env.undefined = undefined
catalog.add_folder(folder_t, prefix="ui")
(folder / "Test.jinja").write_text("<ui:Title />")
(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") catalog.add_folder(folder_t, prefix="ui")
(folder / "Title.jinja").write_text("parent") (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 / "Title.jinja").write_text("prefix")
(folder_t / "Alert.jinja").write_text("<Title />") (folder_t / "Alert.jinja").write_text("<Title />")
html = catalog.render("ui.Alert") html = catalog.render("ui:Alert")
assert html == Markup("prefix") assert html == Markup("prefix")
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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 """Components mounted with a prefix should be able to import other components
from the same folder without specifying the prefix, even if those components from the same folder without specifying the prefix, even if those components
are in a subfolder. are in a subfolder.
""" """
catalog.jinja_env.autoescape = autoescape catalog.jinja_env.autoescape = autoescape
catalog.jinja_env.undefined = undefined
catalog.add_folder(folder_t, prefix="ui") catalog.add_folder(folder_t, prefix="ui")
(folder / "sub").mkdir()
(folder_t / "sub").mkdir()
(folder / "Title.jinja").write_text("parent") (folder / "Title.jinja").write_text("parent")
(folder / "sub").mkdir()
(folder / "sub" / "Title.jinja").write_text("sub/parent") (folder / "sub" / "Title.jinja").write_text("sub/parent")
(folder_t / "Title.jinja").write_text("sub") (folder_t / "Title.jinja").write_text("sub")
(folder_t / "sub").mkdir()
(folder_t / "sub" / "Title.jinja").write_text("sub/prefix") (folder_t / "sub" / "Title.jinja").write_text("sub/prefix")
(folder_t / "Alert.jinja").write_text("<sub.Title />") (folder_t / "Alert.jinja").write_text("<sub.Title />")
html = catalog.render("ui.Alert") html = catalog.render("ui:Alert")
assert html == Markup("sub/prefix") assert html == Markup("sub/prefix")
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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 """If a component is not found in the folder with the prefix, it should
fallback to the no-prefix folders. fallback to the no-prefix folders.
""" """
catalog.jinja_env.autoescape = autoescape catalog.jinja_env.autoescape = autoescape
catalog.jinja_env.undefined = undefined
catalog.add_folder(folder_t, prefix="ui") catalog.add_folder(folder_t, prefix="ui")
(folder / "Title.jinja").write_text("parent") (folder / "Title.jinja").write_text("parent")
(folder_t / "Alert.jinja").write_text("<Title />") (folder_t / "Alert.jinja").write_text("<Title />")
html = catalog.render("ui.Alert") html = catalog.render("ui:Alert")
assert html == Markup("parent") assert html == Markup("parent")
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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 """Components import without specifying the prefix should also be
able to auto-import their assets. able to auto-import their assets.
""" """
catalog.jinja_env.autoescape = autoescape catalog.jinja_env.autoescape = autoescape
catalog.jinja_env.undefined = undefined
catalog.add_folder(folder_t, prefix="ui") catalog.add_folder(folder_t, prefix="ui")
(folder_t / "Title.jinja").write_text("prefix") (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>") (folder_t / "Alert.jinja").write_text("<Layout><Title /></Layout>")
html = catalog.render("ui.Alert") html = catalog.render("ui:Alert")
assert html == Markup(""" assert html == Markup("""
<link rel="stylesheet" href="/static/components/ui/Title.css"> <link rel="stylesheet" href="/static/components/ui/Title.css">
prefix prefix

View file

@ -9,9 +9,11 @@ from markupsafe import Markup
import jinjax import jinjax
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Greeting.jinja").write_text( (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>') 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]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
source = '{#def message #}\n<div class="greeting [&_a]:flex">{{ message }}</div>' source = '{#def message #}\n<div class="greeting [&_a]:flex">{{ message }}</div>'
expected = Markup('<div class="greeting [&_a]:flex">Hello world!</div>') expected = Markup('<div class="greeting [&_a]:flex">Hello world!</div>')
@ -38,9 +42,11 @@ def test_render_source(catalog, autoescape):
assert expected == html assert expected == html
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Card.jinja").write_text(""" (folder / "Card.jinja").write_text("""
<section class="card"> <section class="card">
@ -60,6 +66,7 @@ def test_render_content(catalog, folder, autoescape):
assert expected == html assert expected == html
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @pytest.mark.parametrize("autoescape", [True, False])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"source, expected", "source, expected",
@ -76,8 +83,10 @@ def test_render_mix_of_contentful_and_contentless_components(
source, source,
expected, expected,
autoescape, autoescape,
undefined,
): ):
catalog.jinja_env.autoescape = autoescape catalog.jinja_env.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Icon.jinja").write_text('<i class="icon"></i>') (folder / "Icon.jinja").write_text('<i class="icon"></i>')
(folder / "Title.jinja").write_text("<h1>{{ content }}</h1>") (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) assert html == Markup(expected)
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Greeting.jinja").write_text( (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]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Lorem.jinja").write_text( (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]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Global.jinja").write_text("""{{ globalvar }}""") (folder / "Global.jinja").write_text("""{{ globalvar }}""")
message = "Hello world!" message = "Hello world!"
@ -193,9 +208,11 @@ def test_global_values(catalog, folder, autoescape):
assert message in html assert message in html
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Greeting.jinja").write_text( (folder / "Greeting.jinja").write_text(
""" """
@ -208,12 +225,14 @@ def test_required_attr_are_required(catalog, folder, autoescape):
catalog.render("Greeting") catalog.render("Greeting")
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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 """Components can be organized in subfolders and called
using the dot notation. using the dot notation.
""" """
catalog.jinja_env.autoescape = autoescape catalog.jinja_env.autoescape = autoescape
catalog.jinja_env.undefined = undefined
sub = folder / "ui" sub = folder / "ui"
sub.mkdir() sub.mkdir()
@ -224,9 +243,11 @@ def test_subfolder(catalog, folder, autoescape):
assert html == Markup('<div class="tab">Meh</div>') assert html == Markup('<div class="tab">Meh</div>')
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Greeting.jinja").write_text( (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]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Code.jinja").write_text(""" (folder / "Code.jinja").write_text("""
<pre class="code"> <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]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "C.jinja").write_text(""" (folder / "C.jinja").write_text("""
<div {{ attrs.render() }}></div> <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]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Lorem.jinja").write_text(""" (folder / "Lorem.jinja").write_text("""
{#def ipsum=False #} {#def ipsum=False #}
@ -348,9 +375,11 @@ def test_check_for_unclosed(catalog, folder, autoescape):
raise raise
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "CitiesList.jinja").write_text(""" (folder / "CitiesList.jinja").write_text("""
{#def cities #} {#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>") 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]) @pytest.mark.parametrize("autoescape", [True, False])
def test_do_not_mess_with_external_jinja_env(folder_t, folder, autoescape): def test_do_not_mess_with_external_jinja_env(folder_t, folder, autoescape, undefined):
"""https://github.com/jpsca/jinjax/issues/19""" """Fix https://github.com/jpsca/jinjax/issues/19
"""
(folder_t / "greeting.html").write_text("Jinja still works") (folder_t / "greeting.html").write_text("Jinja still works")
(folder / "Greeting.jinja").write_text("JinjaX 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.filters = {"fil": lambda x: x}
jinja_env.tests = {"tes": lambda x: x} jinja_env.tests = {"tes": lambda x: x}
jinja_env.autoescape = autoescape jinja_env.autoescape = autoescape
jinja_env.undefined = undefined
catalog = jinjax.Catalog( catalog = jinjax.Catalog(
jinja_env=jinja_env, 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 assert "jinja2.ext.DebugExtension" not in jinja_env.extensions
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Layout.jinja").write_text(""" (folder / "Layout.jinja").write_text("""
<html> <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]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
"""Issue https://github.com/jpsca/jinjax/issues/32""" """Issue https://github.com/jpsca/jinjax/issues/32"""
(folder / "Page.jinja").write_text(""" (folder / "Page.jinja").write_text("""
@ -520,9 +556,11 @@ def test_subcomponents(catalog, folder, autoescape):
assert html == Markup(expected.strip()) assert html == Markup(expected.strip())
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "C.jinja").write_text(""" (folder / "C.jinja").write_text("""
<div {{ attrs.render() }}></div> <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 assert """<div hx-on:click="show = !show"></div>""" in html
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Input.jinja").write_text(""" (folder / "Input.jinja").write_text("""
{# def name, value #}<input type="text" name="{{name}}" value="{{value}}"> {# 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 assert """<input type="hidden" name="csrft" value="abc">""" in html
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "CsrfToken.jinja").write_text( (folder / "CsrfToken.jinja").write_text(
"""<input type="hidden" name="csrft" value="{{csrf_token}}">""" """<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 assert """<input type="hidden" name="csrft" value="xyz">""" in html
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Greeting.jinja").write_text(""" (folder / "Greeting.jinja").write_text("""
{#def message #} {#def message #}
@ -595,9 +639,11 @@ def test_alpine_sintax(catalog, folder, autoescape):
assert html == Markup(expected) assert html == Markup(expected)
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Button.jinja").write_text( (folder / "Button.jinja").write_text(
"""<button {{ attrs.render() }}>{{ content }}</button>""" """<button {{ attrs.render() }}>{{ content }}</button>"""
@ -613,9 +659,11 @@ def test_alpine_sintax_in_component(catalog, folder, autoescape):
assert html == Markup(expected) assert html == Markup(expected)
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "CheckboxItem.jinja").write_text( (folder / "CheckboxItem.jinja").write_text(
"""<div {{ attrs.render(class="relative") }}></div>""" """<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]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "A.jinja").write_text(template) (folder / "A.jinja").write_text(template)
@ -767,9 +817,11 @@ def test_mixed_syntax(catalog, folder):
assert html == Markup(expected) assert html == Markup(expected)
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Component.jinja").write_text( (folder / "Component.jinja").write_text(
""" """
@ -804,9 +856,11 @@ def test_slots(catalog, folder, autoescape):
assert html == Markup(expected) assert html == Markup(expected)
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "a_tricky-FOLDER").mkdir() (folder / "a_tricky-FOLDER").mkdir()
(folder / "kebab-folder").mkdir() (folder / "kebab-folder").mkdir()

View file

@ -1,12 +1,15 @@
from pathlib import Path from pathlib import Path
import jinja2
import pytest import pytest
from markupsafe import Markup from markupsafe import Markup
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Greeting.jinja").write_text( (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]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Layout.jinja").write_text(""" (folder / "Layout.jinja").write_text("""
<html> <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]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Layout.jinja").write_text(""" (folder / "Layout.jinja").write_text("""
<html> <html>
@ -150,9 +157,11 @@ def test_fingerprint_assets(catalog, folder: Path, autoescape):
assert 'href="http://example.com/super.css' in html assert 'href="http://example.com/super.css' in html
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Layout.jinja").write_text( (folder / "Layout.jinja").write_text(
"""{{ catalog.render_assets() }}\n{{ content }}""" """{{ catalog.render_assets() }}\n{{ content }}"""
@ -194,9 +203,11 @@ def test_auto_load_assets_with_same_name(catalog, folder, autoescape):
assert html == Markup(expected) assert html == Markup(expected)
@pytest.mark.parametrize("undefined", [jinja2.Undefined, jinja2.StrictUndefined])
@pytest.mark.parametrize("autoescape", [True, False]) @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.autoescape = autoescape
catalog.jinja_env.undefined = undefined
(folder / "Layout.jinja").write_text( (folder / "Layout.jinja").write_text(
"""{{ catalog.render_assets() }}\n{{ content }}""" """{{ catalog.render_assets() }}\n{{ content }}"""

View file

@ -26,8 +26,8 @@ class ThreadWithReturnValue(Thread):
if self._target is not None: if self._target is not None:
self._return = self._target(*self._args, **self._kwargs) self._return = self._target(*self._args, **self._kwargs)
def join(self, *args): def join(self, *args, **kwargs):
Thread.join(self, *args) Thread.join(self, *args, **kwargs)
return self._return return self._return

2
uv.lock generated
View file

@ -215,7 +215,7 @@ wheels = [
[[package]] [[package]]
name = "jinjax" name = "jinjax"
version = "0.54" version = "0.55"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "jinja2" }, { name = "jinja2" },