1from collections import defaultdict
2from importlib import import_module
3from pkgutil import walk_packages
4
5from django.apps import apps
6from django.conf import settings
7from django.core.checks import Error, Warning
8from django.template import TemplateDoesNotExist
9from django.template.context import make_context
10from django.template.engine import Engine
11from django.template.library import InvalidTemplateLibrary
12
13from .base import BaseEngine
14
15
16class DjangoTemplates(BaseEngine):
17 app_dirname = "templates"
18
19 def __init__(self, params):
20 params = params.copy()
21 options = params.pop("OPTIONS").copy()
22 options.setdefault("autoescape", True)
23 options.setdefault("debug", settings.DEBUG)
24 options.setdefault("file_charset", "utf-8")
25 libraries = options.get("libraries", {})
26 options["libraries"] = self.get_templatetag_libraries(libraries)
27 super().__init__(params)
28 self.engine = Engine(self.dirs, self.app_dirs, **options)
29
30 def check(self, **kwargs):
31 return [
32 *self._check_string_if_invalid_is_string(),
33 *self._check_for_template_tags_with_the_same_name(),
34 ]
35
36 def _check_string_if_invalid_is_string(self):
37 value = self.engine.string_if_invalid
38 if not isinstance(value, str):
39 return [
40 Error(
41 "'string_if_invalid' in TEMPLATES OPTIONS must be a string but "
42 "got: %r (%s)." % (value, type(value)),
43 obj=self,
44 id="templates.E002",
45 )
46 ]
47 return []
48
49 def _check_for_template_tags_with_the_same_name(self):
50 libraries = defaultdict(set)
51
52 for module_name, module_path in get_template_tag_modules():
53 libraries[module_name].add(module_path)
54
55 for module_name, module_path in self.engine.libraries.items():
56 libraries[module_name].add(module_path)
57
58 errors = []
59
60 for library_name, items in libraries.items():
61 if len(items) > 1:
62 items = ", ".join(repr(item) for item in sorted(items))
63 errors.append(
64 Warning(
65 f"{library_name!r} is used for multiple template tag modules: "
66 f"{items}",
67 obj=self,
68 id="templates.W003",
69 )
70 )
71
72 return errors
73
74 def from_string(self, template_code):
75 return Template(self.engine.from_string(template_code), self)
76
77 def get_template(self, template_name):
78 try:
79 return Template(self.engine.get_template(template_name), self)
80 except TemplateDoesNotExist as exc:
81 reraise(exc, self)
82
83 def get_templatetag_libraries(self, custom_libraries):
84 """
85 Return a collation of template tag libraries from installed
86 applications and the supplied custom_libraries argument.
87 """
88 libraries = get_installed_libraries()
89 libraries.update(custom_libraries)
90 return libraries
91
92
93class Template:
94 def __init__(self, template, backend):
95 self.template = template
96 self.backend = backend
97
98 @property
99 def origin(self):
100 return self.template.origin
101
102 def render(self, context=None, request=None):
103 context = make_context(
104 context, request, autoescape=self.backend.engine.autoescape
105 )
106 try:
107 return self.template.render(context)
108 except TemplateDoesNotExist as exc:
109 reraise(exc, self.backend)
110
111
112def copy_exception(exc, backend=None):
113 """
114 Create a new TemplateDoesNotExist. Preserve its declared attributes and
115 template debug data but discard __traceback__, __context__, and __cause__
116 to make this object suitable for keeping around (in a cache, for example).
117 """
118 backend = backend or exc.backend
119 new = exc.__class__(*exc.args, tried=exc.tried, backend=backend, chain=exc.chain)
120 if hasattr(exc, "template_debug"):
121 new.template_debug = exc.template_debug
122 return new
123
124
125def reraise(exc, backend):
126 """
127 Reraise TemplateDoesNotExist while maintaining template debug information.
128 """
129 new = copy_exception(exc, backend)
130 raise new from exc
131
132
133def get_template_tag_modules():
134 """
135 Yield (module_name, module_path) pairs for all installed template tag
136 libraries.
137 """
138 candidates = ["django.templatetags"]
139 candidates.extend(
140 f"{app_config.name}.templatetags" for app_config in apps.get_app_configs()
141 )
142
143 for candidate in candidates:
144 try:
145 pkg = import_module(candidate)
146 except ImportError:
147 # No templatetags package defined. This is safe to ignore.
148 continue
149
150 if hasattr(pkg, "__path__"):
151 for name in get_package_libraries(pkg):
152 yield name.removeprefix(candidate).lstrip("."), name
153
154
155def get_installed_libraries():
156 """
157 Return the built-in template tag libraries and those from installed
158 applications. Libraries are stored in a dictionary where keys are the
159 individual module names, not the full module paths. Example:
160 django.templatetags.i18n is stored as i18n.
161 """
162 return {
163 module_name: full_name for module_name, full_name in get_template_tag_modules()
164 }
165
166
167def get_package_libraries(pkg):
168 """
169 Recursively yield template tag libraries defined in submodules of a
170 package.
171 """
172 for entry in walk_packages(pkg.__path__, pkg.__name__ + "."):
173 try:
174 module = import_module(entry[1])
175 except ImportError as e:
176 raise InvalidTemplateLibrary(
177 "Invalid template library specified. ImportError raised when "
178 "trying to load '%s': %s" % (entry[1], e)
179 ) from e
180
181 if hasattr(module, "register"):
182 yield entry[1]