diff --git a/docs/conf.py b/docs/conf.py index d5a6cf6676..89539db7b8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -104,6 +104,7 @@ (PY_CLASS, "abc.Collection"), (PY_CLASS, "advanced_alchemy.utils.dataclass.Empty"), (PY_CLASS, "jinja2.environment.Environment"), + (PY_CLASS, "minijinja.Environment"), (PY_CLASS, "pydantic.BaseModel"), (PY_CLASS, "pydantic.generics.GenericModel"), (PY_CLASS, "pydantic.main.BaseModel"), @@ -189,6 +190,8 @@ (PY_CLASS, "P.kwargs"), (PY_CLASS, "litestar.contrib.jinja.P"), (PY_CLASS, "litestar.contrib.mako.P"), + (PY_CLASS, "MiniJinjaTemplate"), + (PY_CLASS, "litestar.plugins.minijinja.MiniJinjaTemplate"), (PY_CLASS, "JWTDecodeOptions"), (PY_CLASS, "litestar.template.base.P"), (PY_CLASS, "typing.Self"), diff --git a/docs/examples/plugins/flash_messages/minijinja.py b/docs/examples/plugins/flash_messages/minijinja.py index 26ae24be13..b15d19fd07 100644 --- a/docs/examples/plugins/flash_messages/minijinja.py +++ b/docs/examples/plugins/flash_messages/minijinja.py @@ -1,7 +1,7 @@ from litestar import Litestar -from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.plugins.flash import FlashConfig, FlashPlugin +from litestar.plugins.minijinja import MiniJinjaTemplateEngine from litestar.template.config import TemplateConfig template_config = TemplateConfig(engine=MiniJinjaTemplateEngine, directory="templates") diff --git a/docs/examples/templating/engine_instance_minijinja.py b/docs/examples/templating/engine_instance_minijinja.py index 8a5f21dcc3..0375547f1f 100644 --- a/docs/examples/templating/engine_instance_minijinja.py +++ b/docs/examples/templating/engine_instance_minijinja.py @@ -1,4 +1,4 @@ -from litestar.contrib.minijinja import MiniJinjaTemplateEngine +from litestar.plugins.minijinja import MiniJinjaTemplateEngine from litestar.template.config import TemplateConfig template_config = TemplateConfig(engine=MiniJinjaTemplateEngine, directory="templates") diff --git a/docs/examples/templating/returning_templates_minijinja.py b/docs/examples/templating/returning_templates_minijinja.py index 8e0539a6d7..7db8032512 100644 --- a/docs/examples/templating/returning_templates_minijinja.py +++ b/docs/examples/templating/returning_templates_minijinja.py @@ -3,7 +3,7 @@ from pathlib import Path from litestar import Litestar, get -from litestar.contrib.minijinja import MiniJinjaTemplateEngine +from litestar.plugins.minijinja import MiniJinjaTemplateEngine from litestar.response import Template from litestar.template.config import TemplateConfig diff --git a/docs/examples/templating/template_engine_minijinja.py b/docs/examples/templating/template_engine_minijinja.py index 44775e769a..5d2b3946d9 100644 --- a/docs/examples/templating/template_engine_minijinja.py +++ b/docs/examples/templating/template_engine_minijinja.py @@ -1,7 +1,7 @@ from pathlib import Path from litestar import Litestar -from litestar.contrib.minijinja import MiniJinjaTemplateEngine +from litestar.plugins.minijinja import MiniJinjaTemplateEngine from litestar.template.config import TemplateConfig app = Litestar( diff --git a/docs/examples/templating/template_functions_minijinja.py b/docs/examples/templating/template_functions_minijinja.py index 0fccdccb3c..a3d4ca8fcf 100644 --- a/docs/examples/templating/template_functions_minijinja.py +++ b/docs/examples/templating/template_functions_minijinja.py @@ -1,7 +1,7 @@ from pathlib import Path from litestar import Litestar, get -from litestar.contrib.minijinja import MiniJinjaTemplateEngine, StateProtocol +from litestar.plugins.minijinja import MiniJinjaTemplateEngine, StateProtocol from litestar.response import Template from litestar.template.config import TemplateConfig diff --git a/docs/reference/plugins/index.rst b/docs/reference/plugins/index.rst index 1065957b49..8c6dcbdb3e 100644 --- a/docs/reference/plugins/index.rst +++ b/docs/reference/plugins/index.rst @@ -12,6 +12,7 @@ plugins attrs flash_messages htmx + minijinja opentelemetry problem_details prometheus diff --git a/docs/reference/plugins/minijinja.rst b/docs/reference/plugins/minijinja.rst new file mode 100644 index 0000000000..276e462643 --- /dev/null +++ b/docs/reference/plugins/minijinja.rst @@ -0,0 +1,6 @@ +========= +minijinja +========= + +.. automodule:: litestar.plugins.minijinja + :members: diff --git a/litestar/plugins/flash.py b/litestar/plugins/flash.py index 7c94f93d91..060890fb88 100644 --- a/litestar/plugins/flash.py +++ b/litestar/plugins/flash.py @@ -59,7 +59,7 @@ def on_app_init(self, app_config: AppConfig) -> AppConfig: raise litestar.exceptions.ImproperlyConfiguredException("Flash messages require a session middleware.") template_callable: Callable[[Any], Any] = get_flashes with suppress(MissingDependencyException): - from litestar.contrib.minijinja import MiniJinjaTemplateEngine, _transform_state + from litestar.plugins.minijinja import MiniJinjaTemplateEngine, _transform_state if isinstance(self.config.template_config.engine_instance, MiniJinjaTemplateEngine): template_callable = _transform_state(get_flashes) diff --git a/litestar/contrib/minijinja.py b/litestar/plugins/minijinja.py similarity index 89% rename from litestar/contrib/minijinja.py rename to litestar/plugins/minijinja.py index ca9abda96e..f4b51d9486 100644 --- a/litestar/contrib/minijinja.py +++ b/litestar/plugins/minijinja.py @@ -1,4 +1,4 @@ -from __future__ import annotations +"""MiniJinja template engine integration for Litestar.""" import functools from pathlib import Path @@ -41,15 +41,17 @@ def pass_state(func: C) -> C: ... class StateProtocol(Protocol): - auto_escape: str | None - current_block: str | None + auto_escape: "str | None" + current_block: "str | None" env: Environment name: str - def lookup(self, key: str) -> Any | None: ... + def lookup(self, key: str) -> "Any | None": ... -def _transform_state(func: TemplateCallableType[Mapping[str, Any], P, T]) -> TemplateCallableType[StateProtocol, P, T]: +def _transform_state( + func: TemplateCallableType["Mapping[str, Any]", P, T], +) -> TemplateCallableType[StateProtocol, P, T]: """Transform a template callable to receive a ``StateProtocol`` instance as first argument. This is for wrapping callables like ``url_for()`` that receive a mapping as first argument so they can be used @@ -96,7 +98,11 @@ def render(self, *args: Any, **kwargs: Any) -> str: class MiniJinjaTemplateEngine(TemplateEngineProtocol["MiniJinjaTemplate", StateProtocol]): """The engine instance.""" - def __init__(self, directory: Path | list[Path] | None = None, engine_instance: Environment | None = None) -> None: + def __init__( + self, + directory: "Path | list[Path] | None" = None, + engine_instance: "Environment | None" = None, + ) -> None: """Minijinja based TemplateEngine. Args: @@ -169,14 +175,14 @@ def register_template_callable( None """ - def is_decorated(func: Callable) -> bool: + def is_decorated(func: "Callable") -> bool: return hasattr(func, "__wrapped__") or func.__name__ not in globals() if not is_decorated(template_callable): template_callable = _transform_state(template_callable) # type: ignore[arg-type] # pragma: no cover self.engine.add_global(key, pass_state(template_callable)) - def render_string(self, template_string: str, context: Mapping[str, Any]) -> str: + def render_string(self, template_string: str, context: "Mapping[str, Any]") -> str: """Render a template from a string with the given context. Args: @@ -189,7 +195,7 @@ def render_string(self, template_string: str, context: Mapping[str, Any]) -> str return self.engine.render_str(template_string, **context) @classmethod - def from_environment(cls, minijinja_environment: Environment) -> MiniJinjaTemplateEngine: + def from_environment(cls, minijinja_environment: "Environment") -> "MiniJinjaTemplateEngine": """Create a MiniJinjaTemplateEngine from an existing minijinja Environment instance. Args: diff --git a/tests/unit/test_contrib/test_minijinja.py b/tests/unit/test_contrib/test_minijinja.py deleted file mode 100644 index f69723eef9..0000000000 --- a/tests/unit/test_contrib/test_minijinja.py +++ /dev/null @@ -1,46 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pytest -from minijinja import Environment - -from litestar.contrib.minijinja import MiniJinjaTemplateEngine -from litestar.exceptions import ImproperlyConfiguredException, TemplateNotFoundException - -if TYPE_CHECKING: - from pathlib import Path - - -def test_mini_jinja_template_engine_instantiation_error(tmp_path: Path) -> None: - with pytest.raises(ImproperlyConfiguredException): - MiniJinjaTemplateEngine(directory=tmp_path, engine_instance=Environment()) - - with pytest.raises(ImproperlyConfiguredException): - MiniJinjaTemplateEngine() - - -def test_mini_jinja_template_engine_instantiated_with_engine() -> None: - engine = Environment() - template_engine = MiniJinjaTemplateEngine(engine_instance=engine) - assert template_engine.engine is engine - - -def test_mini_jinja_template_render_raises_template_not_found(tmp_path: Path) -> None: - template_engine = MiniJinjaTemplateEngine(engine_instance=Environment()) - with pytest.raises(TemplateNotFoundException): - tmpl = template_engine.get_template("not_found.html") - tmpl.render() - - -def test_mini_jinja_template_render_string(tmp_path: Path) -> None: - template_engine = MiniJinjaTemplateEngine(engine_instance=Environment()) - - good_template = template_engine.render_string("template as a {{value}}", context={"value": "string"}) - assert good_template == "template as a string" - - -def test_from_environment() -> None: - engine = Environment() - template_engine = MiniJinjaTemplateEngine.from_environment(engine) - assert template_engine.engine is engine diff --git a/tests/unit/test_plugins/test_flash.py b/tests/unit/test_plugins/test_flash.py index e74db95ee0..9769ba4857 100644 --- a/tests/unit/test_plugins/test_flash.py +++ b/tests/unit/test_plugins/test_flash.py @@ -8,11 +8,11 @@ from litestar import Litestar, Request, get, post from litestar.contrib.jinja import JinjaTemplateEngine from litestar.contrib.mako import MakoTemplateEngine -from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.exceptions import ImproperlyConfiguredException from litestar.middleware.rate_limit import RateLimitConfig from litestar.middleware.session.server_side import ServerSideSessionConfig from litestar.plugins.flash import FlashConfig, FlashPlugin, flash +from litestar.plugins.minijinja import MiniJinjaTemplateEngine from litestar.response import Redirect, Template from litestar.template import TemplateConfig, TemplateEngineProtocol from litestar.testing import create_test_client diff --git a/tests/unit/test_plugins/test_minijinja.py b/tests/unit/test_plugins/test_minijinja.py new file mode 100644 index 0000000000..00b381ac49 --- /dev/null +++ b/tests/unit/test_plugins/test_minijinja.py @@ -0,0 +1,91 @@ +"""Functional tests for litestar.plugins.minijinja.""" + +from pathlib import Path + +import pytest +from minijinja import Environment + +from litestar.exceptions import ImproperlyConfiguredException, TemplateNotFoundException +from litestar.plugins.minijinja import MiniJinjaTemplateEngine, _transform_state + + +def test_minijinja_template_engine_instantiation_error(tmp_path: Path) -> None: + with pytest.raises(ImproperlyConfiguredException): + MiniJinjaTemplateEngine(directory=tmp_path, engine_instance=Environment()) + + with pytest.raises(ImproperlyConfiguredException): + MiniJinjaTemplateEngine() + + +def test_minijinja_template_engine_instantiated_with_engine() -> None: + engine = Environment() + template_engine = MiniJinjaTemplateEngine(engine_instance=engine) + assert template_engine.engine is engine + + +def test_minijinja_template_engine_instantiated_with_directory(tmp_path: Path) -> None: + template_dir = tmp_path / "templates" + template_dir.mkdir() + (template_dir / "hello.html").write_text("Hello {{ name }}!") + + template_engine = MiniJinjaTemplateEngine(directory=template_dir) + assert isinstance(template_engine.engine, Environment) + + +def test_minijinja_template_engine_instantiated_with_directory_list(tmp_path: Path) -> None: + first = tmp_path / "first" + second = tmp_path / "second" + first.mkdir() + second.mkdir() + (second / "hello.html").write_text("Hi {{ name }}!") + + template_engine = MiniJinjaTemplateEngine(directory=[first, second]) + template = template_engine.get_template("hello.html") + assert template.render(name="World") == "Hi World!" + + +def test_minijinja_template_render_raises_template_not_found(tmp_path: Path) -> None: + template_dir = tmp_path / "templates" + template_dir.mkdir() + template_engine = MiniJinjaTemplateEngine(directory=template_dir) + with pytest.raises(TemplateNotFoundException): + tmpl = template_engine.get_template("not_found.html") + tmpl.render() + + +def test_minijinja_template_render_string() -> None: + template_engine = MiniJinjaTemplateEngine(engine_instance=Environment()) + rendered = template_engine.render_string("template as a {{ value }}", context={"value": "string"}) + assert rendered == "template as a string" + + +def test_minijinja_from_environment() -> None: + engine = Environment() + template_engine = MiniJinjaTemplateEngine.from_environment(engine) + assert template_engine.engine is engine + + +def test_transform_state_callable_invoked_with_template_context() -> None: + captured: dict[str, object] = {} + + def callable_taking_context(context: object, value: str) -> str: + captured["context"] = context + return value.upper() + + wrapped = _transform_state(callable_taking_context) + engine = Environment() + engine.add_global("shout", wrapped) + rendered = engine.render_str("{{ shout('hi') }}") + assert rendered == "HI" + assert "context" in captured + + +def test_register_template_callable_wraps_undecorated_callable() -> None: + template_engine = MiniJinjaTemplateEngine(engine_instance=Environment()) + + def shout(_context: object, value: str) -> str: + return value.upper() + + template_engine.register_template_callable("shout", shout) + rendered = template_engine.render_string("{{ shout('hi') }}", context={}) + assert rendered == "HI" diff --git a/tests/unit/test_template/test_built_in.py b/tests/unit/test_template/test_built_in.py index 19d307092d..189f4ddf15 100644 --- a/tests/unit/test_template/test_built_in.py +++ b/tests/unit/test_template/test_built_in.py @@ -11,7 +11,7 @@ from litestar import get from litestar.contrib.jinja import JinjaTemplateEngine from litestar.contrib.mako import MakoTemplateEngine -from litestar.contrib.minijinja import MiniJinjaTemplateEngine +from litestar.plugins.minijinja import MiniJinjaTemplateEngine from litestar.response.template import Template from litestar.template.config import TemplateConfig from litestar.testing import create_test_client diff --git a/tests/unit/test_template/test_builtin_functions.py b/tests/unit/test_template/test_builtin_functions.py index d04705104f..bb59a864ce 100644 --- a/tests/unit/test_template/test_builtin_functions.py +++ b/tests/unit/test_template/test_builtin_functions.py @@ -7,7 +7,7 @@ from litestar import get from litestar.contrib.jinja import JinjaTemplateEngine from litestar.contrib.mako import MakoTemplateEngine -from litestar.contrib.minijinja import MiniJinjaTemplateEngine +from litestar.plugins.minijinja import MiniJinjaTemplateEngine from litestar.response.template import Template from litestar.status_codes import HTTP_200_OK, HTTP_500_INTERNAL_SERVER_ERROR from litestar.template.config import TemplateConfig diff --git a/tests/unit/test_template/test_context.py b/tests/unit/test_template/test_context.py index 822859600a..099cc154aa 100644 --- a/tests/unit/test_template/test_context.py +++ b/tests/unit/test_template/test_context.py @@ -6,7 +6,7 @@ from litestar import MediaType, get from litestar.contrib.jinja import JinjaTemplateEngine from litestar.contrib.mako import MakoTemplateEngine -from litestar.contrib.minijinja import MiniJinjaTemplateEngine +from litestar.plugins.minijinja import MiniJinjaTemplateEngine from litestar.response.template import Template from litestar.template.config import TemplateConfig from litestar.testing import create_test_client diff --git a/tests/unit/test_template/test_csrf_token.py b/tests/unit/test_template/test_csrf_token.py index f905c428a3..44d2f37623 100644 --- a/tests/unit/test_template/test_csrf_token.py +++ b/tests/unit/test_template/test_csrf_token.py @@ -8,8 +8,8 @@ from litestar.config.csrf import CSRFConfig from litestar.contrib.jinja import JinjaTemplateEngine from litestar.contrib.mako import MakoTemplateEngine -from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.middleware.csrf import generate_csrf_token +from litestar.plugins.minijinja import MiniJinjaTemplateEngine from litestar.response.template import Template from litestar.template.config import TemplateConfig from litestar.testing import create_test_client diff --git a/tests/unit/test_template/test_template.py b/tests/unit/test_template/test_template.py index 2faa8c4fea..c8336dd02b 100644 --- a/tests/unit/test_template/test_template.py +++ b/tests/unit/test_template/test_template.py @@ -11,8 +11,8 @@ from litestar import Litestar, MediaType, get from litestar.contrib.jinja import JinjaTemplateEngine from litestar.contrib.mako import MakoTemplateEngine -from litestar.contrib.minijinja import MiniJinjaTemplateEngine from litestar.exceptions import ImproperlyConfiguredException +from litestar.plugins.minijinja import MiniJinjaTemplateEngine from litestar.response.template import Template from litestar.template import TemplateEngineProtocol from litestar.template.config import TemplateConfig