Repository: litestar-org/litestar

Issue:
template additional config
How can this be achieved with the current way the template config is used?

```python
templates = Jinja2Templates(directory="templates")
templates.env.globals[’flashed_messages’] = flashed_messages
```


Discussion:
I cannot see an obvious way, nor any mention of setting globals in the docs, so hopefully I haven't missed anything.

The jinja `Environment` is constructed in `starlite.template.jinja.JinjaTemplateEngine`:

```python
class JinjaTemplateEngine(TemplateEngineProtocol[JinjaTemplate]):
    """Template engine using the jinja templating library"""

    def __init__(self, directory: Union[DirectoryPath, List[DirectoryPath]]) -> None:
        super().__init__(directory)
        loader = FileSystemLoader(searchpath=directory)
        self.engine = Environment(loader=loader, autoescape=True)
```
According to [jinja docs](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment.globals) that `Environment` object has a `globals` attribute which is a dict for storing those variables in.

So maybe we could do something like:

```python
templates = TemplateConfig(
    directory="templates", engine=JinjaTemplateEngine, globals={"flashed_messages": flashed_messages}
)
```

@peterschutt are you saying that you can implement into the code base or it should already work?

I have tried that but errors with `UndefinedError: 'flashed_messages' is undefined` when trying to call it in the template.
That is a suggestion of a potential api design to solve the problem you have, it would need to be implemented.
+1, We can also expose and propagate `**kwargs`

Resolving patch:
diff --git a/starlite/app.py b/starlite/app.py
--- a/starlite/app.py
+++ b/starlite/app.py
@@ -36,7 +36,6 @@
 from starlite.router import Router
 from starlite.routes import ASGIRoute, BaseRoute, HTTPRoute, WebSocketRoute
 from starlite.signature import model_function_signature
-from starlite.template import TemplateEngineProtocol
 from starlite.types import (
     AfterRequestHandler,
     BeforeRequestHandler,
@@ -49,6 +48,7 @@
 )
 from starlite.utils import normalize_path
 from starlite.utils.exception import get_exception_handler
+from starlite.utils.templates import create_template_engine
 
 DEFAULT_OPENAPI_CONFIG = OpenAPIConfig(title="Starlite API", version="1.0.0")
 DEFAULT_CACHE_CONFIG = CacheConfig()
@@ -134,10 +134,7 @@ def __init__(
                 static_files = StaticFiles(html=config.html_mode, check_dir=False)
                 static_files.all_directories = config.directories  # type: ignore
                 self.register(asgi(path=path)(static_files))
-        if template_config:
-            self.template_engine: Optional[TemplateEngineProtocol] = template_config.engine(template_config.directory)
-        else:
-            self.template_engine = None
+        self.template_engine = create_template_engine(template_config)
 
     async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
         scope["app"] = self
diff --git a/starlite/config.py b/starlite/config.py
--- a/starlite/config.py
+++ b/starlite/config.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict, List, Optional, Tuple, Union
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
 from urllib.parse import urlencode
 
 from openapi_schema_pydantic import (
@@ -84,6 +84,7 @@ class Config:
 
     directory: Union[DirectoryPath, List[DirectoryPath]]
     engine: Type[TemplateEngineProtocol]
+    engine_callback: Optional[Callable[[Any], Any]]
 
 
 def default_cache_key_builder(request: Request) -> str:
diff --git a/starlite/utils/templates.py b/starlite/utils/templates.py
new file mode 100644
--- /dev/null
+++ b/starlite/utils/templates.py
@@ -0,0 +1,27 @@
+from typing import TYPE_CHECKING, Optional
+
+if TYPE_CHECKING:  # pragma: no cover
+    from starlite.config import TemplateConfig
+    from starlite.template import TemplateEngineProtocol
+
+
+def create_template_engine(template_config: Optional["TemplateConfig"]) -> Optional["TemplateEngineProtocol"]:
+    """
+    Construct a template engine if `template_config` is provided.
+
+    Parameters
+    ----------
+    template_config : TemplateConfig | None
+
+    Returns
+    -------
+    TemplateEngineProtocol | None
+    """
+    template_engine: Optional["TemplateEngineProtocol"]
+    if template_config:
+        template_engine = template_config.engine(template_config.directory)
+        if template_config.engine_callback is not None:
+            template_config.engine_callback(template_engine)
+    else:
+        template_engine = None
+    return template_engine


Test patch:
diff --git a/tests/template/__init__.py b/tests/template/__init__.py
new file mode 100644
diff --git a/tests/template/conftest.py b/tests/template/conftest.py
new file mode 100644
--- /dev/null
+++ b/tests/template/conftest.py
@@ -0,0 +1,8 @@
+import pathlib
+
+import pytest
+
+
+@pytest.fixture
+def template_dir(tmp_path: pathlib.Path) -> pathlib.Path:
+    return tmp_path
diff --git a/tests/template/test_built_in.py b/tests/template/test_built_in.py
new file mode 100644
--- /dev/null
+++ b/tests/template/test_built_in.py
@@ -0,0 +1,108 @@
+import pathlib
+from dataclasses import dataclass
+from typing import Any, Type, Union
+
+import pytest
+
+from starlite import HTTPRouteHandler, Template, TemplateConfig, get
+from starlite.template.jinja import JinjaTemplateEngine
+from starlite.template.mako import MakoTemplateEngine
+from starlite.testing import create_test_client
+
+
+@dataclass
+class EngineTest:
+    engine: Type[Union[JinjaTemplateEngine, MakoTemplateEngine]]
+    index_template: str
+    nested_template: str
+
+
+@pytest.fixture(
+    params=[
+        EngineTest(
+            engine=JinjaTemplateEngine,
+            index_template="<html>Injected? {{test}}</html>",
+            nested_template="<html>Does nested dirs work? {{test}}</html>",
+        ),
+        EngineTest(
+            engine=MakoTemplateEngine,
+            index_template="<html>Injected? ${test}</html>",
+            nested_template="<html>Does nested dirs work? ${test}</html>",
+        ),
+    ]
+)
+def engine_test(request: Any) -> EngineTest:
+    return request.param  # type:ignore[no-any-return]
+
+
+@pytest.fixture
+def index_handler(engine_test: EngineTest, template_dir: pathlib.Path) -> HTTPRouteHandler:
+    with open(template_dir / "index.html", "w") as f:
+        f.write(engine_test.index_template)
+
+    @get(path="/")
+    def index_handler() -> Template:
+        return Template(name="index.html", context={"test": "yep"})
+
+    return index_handler
+
+
+@pytest.fixture
+def nested_path_handler(engine_test: EngineTest, template_dir: pathlib.Path) -> HTTPRouteHandler:
+    nested_path = template_dir / "nested-dir"
+    nested_path.mkdir()
+    with open(nested_path / "nested.html", "w") as f:
+        f.write(engine_test.nested_template)
+
+    @get(path="/nested")
+    def nested_path_handler() -> Template:
+        return Template(name="nested-dir/nested.html", context={"test": "yep"})
+
+    return nested_path_handler
+
+
+@pytest.fixture
+def template_config(engine_test: EngineTest, template_dir: pathlib.Path) -> TemplateConfig:
+    return TemplateConfig(engine=engine_test.engine, directory=template_dir)
+
+
+def test_template(index_handler: HTTPRouteHandler, template_config: TemplateConfig) -> None:
+    with create_test_client(route_handlers=[index_handler], template_config=template_config) as client:
+        response = client.request("GET", "/")
+        assert response.status_code == 200, response.text
+        assert response.text == "<html>Injected? yep</html>"
+        assert response.headers["Content-Type"] == "text/html; charset=utf-8"
+
+
+def test_nested_template_directory(nested_path_handler: HTTPRouteHandler, template_config: TemplateConfig) -> None:
+    with create_test_client(route_handlers=[nested_path_handler], template_config=template_config) as client:
+        response = client.request("GET", "/nested")
+        assert response.status_code == 200, response.text
+        assert response.text == "<html>Does nested dirs work? yep</html>"
+        assert response.headers["Content-Type"] == "text/html; charset=utf-8"
+
+
+def test_raise_for_invalid_template_name(template_config: TemplateConfig) -> None:
+    @get(path="/")
+    def invalid_template_name_handler() -> Template:
+        return Template(name="invalid.html", context={"test": "yep"})
+
+    with create_test_client(route_handlers=[invalid_template_name_handler], template_config=template_config) as client:
+        response = client.request("GET", "/")
+        assert response.status_code == 500
+        assert response.json() == {"detail": "Template invalid.html not found.", "extra": None, "status_code": 500}
+
+
+def test_no_context(template_dir: pathlib.Path, template_config: TemplateConfig) -> None:
+    with open(template_dir / "index.html", "w") as file:
+        file.write("<html>This works!</html>")
+
+    @get(path="/")
+    def index() -> Template:
+        return Template(name="index.html")
+
+    with create_test_client(route_handlers=[index], template_config=template_config) as client:
+        index_response = client.request("GET", "/")
+        assert index_response.status_code == 200
+        assert index_response.text == "<html>This works!</html>"
+        assert index_response.headers["Content-Type"] == "text/html; charset=utf-8"
diff --git a/tests/template/test_template.py b/tests/template/test_template.py
new file mode 100644
--- /dev/null
+++ b/tests/template/test_template.py
@@ -0,0 +1,37 @@
+import pathlib
+
+from starlite import Starlite, Template, TemplateConfig, get
+from starlite.template.jinja import JinjaTemplateEngine
+from starlite.testing import create_test_client
+
+
+def test_handler_raise_for_no_template_engine() -> None:
+    @get(path="/")
+    def invalid_path() -> Template:
+        return Template(name="index.html", context={"ye": "yeeee"})
+
+    with create_test_client(route_handlers=[invalid_path]) as client:
+        response = client.request("GET", "/")
+        assert response.status_code == 500
+        assert response.json() == {"detail": "Template engine is not configured", "extra": None, "status_code": 500}
+
+
+def test_engine_passed_to_callback(template_dir: pathlib.Path) -> None:
+    received_engine: JinjaTemplateEngine | None = None
+
+    def callback(engine: JinjaTemplateEngine) -> JinjaTemplateEngine:
+        nonlocal received_engine
+        received_engine = engine
+        return engine
+
+    app = Starlite(
+        route_handlers=[],
+        template_config=TemplateConfig(
+            directory=template_dir,
+            engine=JinjaTemplateEngine,
+            engine_callback=callback,
+        ),
+    )
+
+    assert received_engine is not None
+    assert received_engine is app.template_engine
diff --git a/tests/test_template.py b/tests/test_template.py
--- a/tests/test_template.py
+++ b/tests/test_template.py
@@ -1,135 +0,0 @@
-import os
-from typing import Any
-
-from starlite import Template, TemplateConfig, get
-from starlite.template.jinja import JinjaTemplateEngine
-from starlite.template.mako import MakoTemplateEngine
-from starlite.testing import create_test_client
-
-
-def test_jinja_template(tmpdir: Any) -> None:
-    path = os.path.join(tmpdir, "index.html")
-    with open(path, "w") as file:
-        file.write("<html>Injected? {{test}}</html>")
-
-    nested_dir = os.path.join(tmpdir, "users")
-    os.mkdir(nested_dir)
-    nested_path = os.path.join(nested_dir, "nested.html")
-
-    with open(nested_path, "w") as file:
-        file.write("<html>Does nested dirs work? {{test}}</html>")
-
-    @get(path="/")
-    def index_handler() -> Template:
-        return Template(name="index.html", context={"test": "yep"})
-
-    @get(path="/nested")
-    def nested_path_handler() -> Template:
-        return Template(name="users/nested.html", context={"test": "yep"})
-
-    with create_test_client(
-        route_handlers=[index_handler, nested_path_handler],
-        template_config=TemplateConfig(engine=JinjaTemplateEngine, directory=tmpdir),
-    ) as client:
-        index_response = client.request("GET", "/")
-        assert index_response.status_code == 200
-        assert index_response.text == "<html>Injected? yep</html>"
-        assert index_response.headers["Content-Type"] == "text/html; charset=utf-8"
-
-        nested_response = client.request("GET", "/nested")
-        assert nested_response.status_code == 200
-        assert nested_response.text == "<html>Does nested dirs work? yep</html>"
-        assert nested_response.headers["Content-Type"] == "text/html; charset=utf-8"
-
-
-def test_jinja_raise_for_invalid_path(tmpdir: Any) -> None:
-    @get(path="/")
-    def invalid_path() -> Template:
-        return Template(name="invalid.html", context={"test": "yep"})
-
-    with create_test_client(
-        route_handlers=[invalid_path],
-        template_config=TemplateConfig(engine=JinjaTemplateEngine, directory=tmpdir),
-    ) as client:
-        response = client.request("GET", "/")
-        assert response.status_code == 500
-        assert response.json() == {"detail": "Template invalid.html not found.", "extra": None, "status_code": 500}
-
-
-def test_mako_template(tmpdir: Any) -> None:
-    path = os.path.join(tmpdir, "index.html")
-    with open(path, "w") as file:
-        file.write("<html>Injected? ${test}</html>")
-
-    nested_dir = os.path.join(tmpdir, "users")
-    os.mkdir(nested_dir)
-    nested_path = os.path.join(nested_dir, "nested.html")
-
-    with open(nested_path, "w") as file:
-        file.write("<html>Does nested dirs work? ${test}</html>")
-
-    @get(path="/")
-    def index_handler() -> Template:
-        return Template(name="index.html", context={"test": "yep"})
-
-    @get(path="/nested")
-    def nested_path_handler() -> Template:
-        return Template(name="users/nested.html", context={"test": "yep"})
-
-    with create_test_client(
-        route_handlers=[index_handler, nested_path_handler],
-        template_config=TemplateConfig(engine=MakoTemplateEngine, directory=tmpdir),
-    ) as client:
-        index_response = client.request("GET", "/")
-        assert index_response.status_code == 200
-        assert index_response.text == "<html>Injected? yep</html>"
-        assert index_response.headers["Content-Type"] == "text/html; charset=utf-8"
-
-        nested_response = client.request("GET", "/nested")
-        assert nested_response.status_code == 200
-        assert nested_response.text == "<html>Does nested dirs work? yep</html>"
-        assert nested_response.headers["Content-Type"] == "text/html; charset=utf-8"
-
-
-def test_mako_raise_for_invalid_path(tmpdir: Any) -> None:
-    @get(path="/")
-    def invalid_path() -> Template:
-        return Template(name="invalid.html", context={"test": "yep"})
-
-    with create_test_client(
-        route_handlers=[invalid_path],
-        template_config=TemplateConfig(engine=MakoTemplateEngine, directory=tmpdir),
-    ) as client:
-        response = client.request("GET", "/")
-        assert response.status_code == 500
-        assert response.json() == {"detail": "Template invalid.html not found.", "extra": None, "status_code": 500}
-
-
-def test_handler_raise_for_no_template_engine() -> None:
-    @get(path="/")
-    def invalid_path() -> Template:
-        return Template(name="index.html", context={"ye": "yeeee"})
-
-    with create_test_client(route_handlers=[invalid_path]) as client:
-        response = client.request("GET", "/")
-        assert response.status_code == 500
-        assert response.json() == {"detail": "Template engine is not configured", "extra": None, "status_code": 500}
-
-
-def test_template_with_no_context(tmpdir: Any) -> None:
-    path = os.path.join(tmpdir, "index.html")
-    with open(path, "w") as file:
-        file.write("<html>This works!</html>")
-
-    @get(path="/")
-    def index() -> Template:
-        return Template(name="index.html")
-
-    with create_test_client(
-        route_handlers=[index],
-        template_config=TemplateConfig(engine=JinjaTemplateEngine, directory=tmpdir),
-    ) as client:
-        index_response = client.request("GET", "/")
-        assert index_response.status_code == 200
-        assert index_response.text == "<html>This works!</html>"
-        assert index_response.headers["Content-Type"] == "text/html; charset=utf-8"