1import functools
2from pathlib import Path
3from typing import Any
4
5from jinja2 import Environment, StrictUndefined
6from jinja2.loaders import FileSystemLoader
7
8from plain.packages import packages_registry
9from plain.runtime import settings
10
11from .filters import default_filters
12from .globals import default_globals
13
14
15def _finalize_callable_error(obj: Any) -> Any:
16 """Prevent direct rendering of a callable (likely just forgotten ()) by raising a TypeError"""
17 if callable(obj):
18 raise TypeError(f"{obj} is callable, did you forget parentheses?")
19
20 # TODO find a way to prevent <object representation> from being rendered
21 # if obj.__class__.__str__ is object.__str__:
22 # raise TypeError(f"{obj} does not have a __str__ method")
23
24 return obj
25
26
27def get_template_dirs() -> tuple[Path, ...]:
28 jinja_templates = Path(__file__).parent / "templates"
29 app_templates = settings.path.parent / "templates"
30 return (jinja_templates, app_templates) + _get_app_template_dirs()
31
32
33@functools.lru_cache
34def _get_app_template_dirs() -> tuple[Path, ...]:
35 """
36 Return an iterable of paths of directories to load app templates from.
37
38 dirname is the name of the subdirectory containing templates inside
39 installed applications.
40 """
41 dirname = "templates"
42 template_dirs = [
43 Path(package_config.path) / dirname
44 for package_config in packages_registry.get_package_configs()
45 if package_config.path and (Path(package_config.path) / dirname).is_dir()
46 ]
47 # Immutable return value because it will be cached and shared by callers.
48 return tuple(template_dirs)
49
50
51class DefaultEnvironment(Environment):
52 def __init__(self):
53 super().__init__(
54 loader=FileSystemLoader(get_template_dirs()),
55 autoescape=True,
56 auto_reload=settings.DEBUG,
57 undefined=StrictUndefined,
58 finalize=_finalize_callable_error,
59 extensions=["jinja2.ext.loopcontrols", "jinja2.ext.debug"],
60 )
61
62 # Load the top-level defaults
63 self.globals.update(default_globals)
64 self.filters.update(default_filters)