Coverage for /Users/davegaeddert/Development/dropseed/plain/plain/plain/templates/jinja/defaults.py: 75%
61 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:04 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:04 -0500
1import functools
2from importlib import import_module
3from pathlib import Path
5from jinja2 import Environment, StrictUndefined
7from plain.packages import packages
8from plain.runtime import settings
9from plain.utils.module_loading import import_string, module_has_submodule
11from .filters import default_filters
12from .globals import default_globals
15@functools.lru_cache
16def _get_app_template_dirs():
17 """
18 Return an iterable of paths of directories to load app templates from.
20 dirname is the name of the subdirectory containing templates inside
21 installed applications.
22 """
23 dirname = "templates"
24 template_dirs = [
25 Path(package_config.path) / dirname
26 for package_config in packages.get_package_configs()
27 if package_config.path and (Path(package_config.path) / dirname).is_dir()
28 ]
29 # Immutable return value because it will be cached and shared by callers.
30 return tuple(template_dirs)
33def _get_installed_extensions() -> tuple[list, dict, dict]:
34 """Automatically load extensions, globals, filters from INSTALLED_PACKAGES jinja module and root jinja module"""
35 extensions = []
36 globals = {}
37 filters = {}
39 for package_config in packages.get_package_configs():
40 if module_has_submodule(package_config.module, "jinja"):
41 module = import_module(f"{package_config.name}.jinja")
42 else:
43 continue
45 if hasattr(module, "extensions"):
46 extensions.extend(module.extensions)
48 if hasattr(module, "globals"):
49 globals.update(module.globals)
51 if hasattr(module, "filters"):
52 filters.update(module.filters)
54 try:
55 import jinja
57 if hasattr(jinja, "extensions"):
58 extensions.extend(jinja.extensions)
60 if hasattr(jinja, "globals"):
61 globals.update(jinja.globals)
63 if hasattr(jinja, "filters"):
64 filters.update(jinja.filters)
65 except ImportError:
66 pass
68 return extensions, globals, filters
71def finalize_callable_error(obj):
72 """Prevent direct rendering of a callable (likely just forgotten ()) by raising a TypeError"""
73 if callable(obj):
74 raise TypeError(f"{obj} is callable, did you forget parentheses?")
76 # TODO find a way to prevent <object representation> from being rendered
77 # if obj.__class__.__str__ is object.__str__:
78 # raise TypeError(f"{obj} does not have a __str__ method")
80 return obj
83def get_template_dirs():
84 jinja_templates = Path(__file__).parent / "templates"
85 app_templates = settings.path.parent / "templates"
86 return (jinja_templates, app_templates) + _get_app_template_dirs()
89def create_default_environment(include_packages=True, **environment_kwargs):
90 """
91 This default jinja environment, also used by the error rendering and internal views so
92 customization needs to happen by using this function, not settings that hook in internally.
93 """
94 loader = import_string(settings.JINJA_LOADER)(get_template_dirs())
95 kwargs = {
96 "loader": loader,
97 "autoescape": True,
98 "auto_reload": settings.DEBUG,
99 "undefined": StrictUndefined,
100 "finalize": finalize_callable_error,
101 "extensions": ["jinja2.ext.loopcontrols", "jinja2.ext.debug"],
102 }
103 kwargs.update(**environment_kwargs)
104 env = Environment(**kwargs)
106 # Load the top-level defaults
107 env.globals.update(default_globals)
108 env.filters.update(default_filters)
110 if include_packages:
111 app_extensions, app_globals, app_filters = _get_installed_extensions()
113 for extension in app_extensions:
114 env.add_extension(extension)
116 env.globals.update(app_globals)
117 env.filters.update(app_filters)
119 return env