1import functools
2from collections import Counter
3from pathlib import Path
4
5from django.apps import apps
6from django.conf import settings
7from django.core.exceptions import ImproperlyConfigured
8from django.utils.functional import cached_property
9from django.utils.module_loading import import_string
10
11
12class InvalidTemplateEngineError(ImproperlyConfigured):
13 pass
14
15
16class EngineHandler:
17 def __init__(self, templates=None):
18 """
19 templates is an optional list of template engine definitions
20 (structured like settings.TEMPLATES).
21 """
22 self._templates = templates
23 self._engines = {}
24
25 @cached_property
26 def templates(self):
27 if self._templates is None:
28 self._templates = settings.TEMPLATES
29
30 templates = {}
31 backend_names = []
32 for tpl in self._templates:
33 try:
34 # This will raise an exception if 'BACKEND' doesn't exist or
35 # isn't a string containing at least one dot.
36 default_name = tpl["BACKEND"].rsplit(".", 2)[-2]
37 except Exception:
38 invalid_backend = tpl.get("BACKEND", "<not defined>")
39 raise ImproperlyConfigured(
40 "Invalid BACKEND for a template engine: {}. Check "
41 "your TEMPLATES setting.".format(invalid_backend)
42 )
43
44 tpl = {
45 "NAME": default_name,
46 "DIRS": [],
47 "APP_DIRS": False,
48 "OPTIONS": {},
49 **tpl,
50 }
51
52 templates[tpl["NAME"]] = tpl
53 backend_names.append(tpl["NAME"])
54
55 counts = Counter(backend_names)
56 duplicates = [alias for alias, count in counts.most_common() if count > 1]
57 if duplicates:
58 raise ImproperlyConfigured(
59 "Template engine aliases aren't unique, duplicates: {}. "
60 "Set a unique NAME for each engine in settings.TEMPLATES.".format(
61 ", ".join(duplicates)
62 )
63 )
64
65 return templates
66
67 def __getitem__(self, alias):
68 try:
69 return self._engines[alias]
70 except KeyError:
71 try:
72 params = self.templates[alias]
73 except KeyError:
74 raise InvalidTemplateEngineError(
75 "Could not find config for '{}' "
76 "in settings.TEMPLATES".format(alias)
77 )
78
79 # If importing or initializing the backend raises an exception,
80 # self._engines[alias] isn't set and this code may get executed
81 # again, so we must preserve the original params. See #24265.
82 params = params.copy()
83 backend = params.pop("BACKEND")
84 engine_cls = import_string(backend)
85 engine = engine_cls(params)
86
87 self._engines[alias] = engine
88 return engine
89
90 def __iter__(self):
91 return iter(self.templates)
92
93 def all(self):
94 return [self[alias] for alias in self]
95
96
97@functools.lru_cache
98def get_app_template_dirs(dirname):
99 """
100 Return an iterable of paths of directories to load app templates from.
101
102 dirname is the name of the subdirectory containing templates inside
103 installed applications.
104 """
105 # Immutable return value because it will be cached and shared by callers.
106 return tuple(
107 path
108 for app_config in apps.get_app_configs()
109 if app_config.path and (path := Path(app_config.path) / dirname).is_dir()
110 )