import os import sys import warnings from collections import Counter, OrderedDict from django.apps import apps from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.utils import lru_cache, six from django.utils.deprecation import RemovedInDjango20Warning from django.utils.functional import cached_property from django.utils.module_loading import import_string class InvalidTemplateEngineError(ImproperlyConfigured): pass class EngineHandler(object): def __init__(self, templates=None): """ templates is an optional list of template engine definitions (structured like settings.TEMPLATES). """ self._templates = templates self._engines = {} @cached_property def templates(self): if self._templates is None: self._templates = settings.TEMPLATES if not self._templates: warnings.warn( "You haven't defined a TEMPLATES setting. You must do so " "before upgrading to Django 2.0. Otherwise Django will be " "unable to load templates.", RemovedInDjango20Warning) self._templates = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': settings.TEMPLATE_DIRS, 'OPTIONS': { 'allowed_include_roots': settings.ALLOWED_INCLUDE_ROOTS, 'context_processors': settings.TEMPLATE_CONTEXT_PROCESSORS, 'loaders': settings.TEMPLATE_LOADERS, 'string_if_invalid': settings.TEMPLATE_STRING_IF_INVALID, }, }, ] templates = OrderedDict() for tpl in self._templates: tpl = tpl.copy() try: # This will raise an exception if 'BACKEND' doesn't exist or # isn't a string containing at least one dot. default_name = tpl['BACKEND'].rsplit('.', 2)[-2] except Exception: invalid_backend = tpl.get('BACKEND', '') raise ImproperlyConfigured( "Invalid BACKEND for a template engine: {}. Check " "your TEMPLATES setting.".format(invalid_backend)) tpl.setdefault('NAME', default_name) tpl.setdefault('DIRS', []) tpl.setdefault('APP_DIRS', False) tpl.setdefault('OPTIONS', {}) templates[tpl['NAME']] = tpl counts = Counter(list(templates)) duplicates = [alias for alias, count in counts.most_common() if count > 1] if duplicates: raise ImproperlyConfigured( "Template engine aliases aren't unique, duplicates: {}. " "Set a unique NAME for each engine in settings.TEMPLATES." .format(", ".join(duplicates))) return templates def __getitem__(self, alias): try: return self._engines[alias] except KeyError: try: params = self.templates[alias] except KeyError: raise InvalidTemplateEngineError( "Could not find config for '{}' " "in settings.TEMPLATES".format(alias)) # If importing or initializing the backend raises an exception, # self._engines[alias] isn't set and this code may get executed # again, so we must preserve the original params. See #24265. params = params.copy() backend = params.pop('BACKEND') engine_cls = import_string(backend) engine = engine_cls(params) self._engines[alias] = engine return engine def __iter__(self): return iter(self.templates) def all(self): return [self[alias] for alias in self] @lru_cache.lru_cache() def get_app_template_dirs(dirname): """ Return an iterable of paths of directories to load app templates from. dirname is the name of the subdirectory containing templates inside installed applications. """ if six.PY2: fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() template_dirs = [] for app_config in apps.get_app_configs(): if not app_config.path: continue template_dir = os.path.join(app_config.path, dirname) if os.path.isdir(template_dir): if six.PY2: template_dir = template_dir.decode(fs_encoding) template_dirs.append(template_dir) # Immutable return value because it will be cached and shared by callers. return tuple(template_dirs)