Source code for score.tpl._init

# Copyright © 2015-2017 STRG.AT GmbH, Vienna, Austria
#
# This file is part of the The SCORE Framework.
#
# The SCORE Framework and all its parts are free software: you can redistribute
# them and/or modify them under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation which is in the
# file named COPYING.LESSER.txt.
#
# The SCORE Framework and all its parts are distributed without any WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. For more details see the GNU Lesser General Public
# License.
#
# If you have not received a copy of the GNU Lesser General Public License see
# http://www.gnu.org/licenses/.
#
# The License-Agreement realised between you as Licensee and STRG.AT GmbH as
# Licenser including the issue of its valid conclusion and its pre- and
# post-contractual effects is governed by the laws of Austria. Any disputes
# concerning this License-Agreement including the issue of its valid conclusion
# and its pre- and post-contractual effects are exclusively decided by the
# competent court, in whose district STRG.AT GmbH has its registered seat, at
# the discretion of STRG.AT GmbH also the competent court, in whose district the
# Licensee has his registered seat, an establishment or assets.

import os
from ._exc import TemplateNotFound
from .loader import FileSystemLoader
from collections import namedtuple, defaultdict, OrderedDict
from score.init import (
    parse_list, extract_conf, ConfiguredModule, ConfigurationError)
import re


defaults = {
    'rootdirs': [],
}


[docs]def init(confdict): """ Initializes this module acoording to :ref:`our module initialization guidelines <module_initialization>` with the following configuration keys: :confkey:`rootdirs` :confdefault:`None` Denotes the root folder containing all templates. """ conf = dict(defaults.items()) conf['rootdirs'] = [] conf.update(confdict) if 'rootdir' in conf and conf['rootdirs']: import score.tpl raise ConfigurationError( score.tpl, "Both 'rootdir' and 'rootdirs' given") if 'rootdir' in conf: conf['rootdirs'] = conf['rootdir'] rootdirs = parse_list(conf['rootdirs']) for rootdir in rootdirs: if not os.path.isdir(rootdir): import score.tpl raise ConfigurationError( score.tpl, 'Given rootdir is not a folder: %s' % (rootdir,)) tpl = ConfiguredTplModule(rootdirs) extensions = set() for key in extract_conf(conf, 'filetype.'): extensions.add(key.rsplit('.', 1)[0]) for ext in extensions: mimetype = conf.get('filetype.%s.mimetype' % ext) if mimetype is None: import score.tpl raise ConfigurationError( score.tpl, 'No mimetype configured for extension %s' % (ext,)) tpl.filetypes[mimetype].extensions.append(ext) return tpl
[docs]class ConfiguredTplModule(ConfiguredModule): """ This module's :class:`configuration class <score.init.ConfiguredModule>`. """ def __init__(self, rootdirs): super().__init__(__package__) self.rootdirs = rootdirs self.filetypes = FileTypes(self) self.loaders = Loaders(self) self.engines = Engines(self) self._renderers = defaultdict(dict) def define_global(self, mimetype, name, value, escape=True): self.filetypes[mimetype].add_global(name, value, escape=escape)
[docs] def iter_paths(self, mimetype=None): """ Provides a generator iterating over all known template paths. If the optional parameter *mimetype* is present, only templates of that mime type will pe provided instead. """ def all_paths(): for mimetype in self.filetypes: for extension in self.filetypes[mimetype].extensions: for loader in self.loaders[extension]: yield from loader.iter_paths() def valid_paths(): for path in all_paths(): try: self._find_filetype(path) yield path except TemplateNotFound: pass if mimetype: extensions = self.filetypes[mimetype].extensions regex = re.compile(r'(^|/)[^/.]+\.(%s)($|\.)' % '|'.join(map(re.escape, extensions))) for path in valid_paths(): if regex.search(path): yield path else: yield from valid_paths()
[docs] def load(self, path): """ Loads given template *path*. See :meth:`Loader.load`. """ return self._find_loader(path).load(path)
[docs] def render(self, path, variables=None, *, apply_postprocessors=True): """ Renders give template *path* with the optional `dict` of *variables*. It is possible to prevent running the file type's postprocessors by passing a falsey value for the *apply_postprocessors* parameter. """ filetype = self._find_filetype(path) is_file, result = self.load(path) if variables is None: variables = {} for renderer in self._find_renderers(path, filetype=filetype): if is_file: result = renderer.render_file(result, variables, path=path) is_file = False else: result = renderer.render_string(result, variables, path=path) if is_file: result = open(result).read() if apply_postprocessors: for postprocessor in filetype.postprocessors: result = postprocessor(result) return result
[docs] def mimetype(self, path): """ Provides to mime type associated with given *path*. See :ref:`tpl_file_types`. """ return self._find_filetype(path).mimetype
[docs] def hash(self, path): """ Provides a hash for given template *path*. See :meth:`Loader.hash`. """ return self._find_loader(path).hash(path)
def _finalize(self): # make sure that every file extension is associated with # at most one filetype all_extensions = set() for filetype in self.filetypes.values(): duplicates = all_extensions.intersection(filetype.extensions) if not duplicates: all_extensions |= set(filetype.extensions) continue extension = duplicates.pop() filetypes = [f for f in self.filetypes if extension in f.extensions.extensions] import score.tpl raise ConfigurationError( score.tpl, 'Extension %s registered with multiple mimetypes: %s' % (extension, filetypes)) for extension in self.loaders: if extension not in all_extensions: import score.tpl raise ConfigurationError( score.tpl, 'Loader Extension "%s" has no filetype' % (extension,)) for extension in self.engines: if extension not in all_extensions: import score.tpl raise ConfigurationError( score.tpl, 'Engine Extension "%s" has no filetype' % (extension,)) # sort loaders and engines by length of extension string # this is important, as we want to test 'tar.gz' before 'gz' self.loaders = OrderedDict( (ext, self.loaders[ext]) for ext in sorted(all_extensions, key=len, reverse=True)) self.engines = OrderedDict( (ext, self.engines[ext]) for ext in sorted(self.engines, key=len, reverse=True)) def _find_loader(self, path): parts = os.path.basename(path).split('.', maxsplit=1) if len(parts) == 1: # TODO: other exception? raise TemplateNotFound(path) if parts[1] not in self.loaders: while '.' in parts[1]: more_parts = parts[1].split('.', maxsplit=1) parts[0] += '.' + more_parts[0] parts[1] = more_parts[1] if parts[1] in self.loaders: break else: raise TemplateNotFound(path) for loader in self.loaders[parts[1]]: if loader.is_valid(path): return loader raise TemplateNotFound(path) def _find_renderers(self, path, *, filetype=None): renderers = [] filename = os.path.basename(path) if filetype is None: filetype = self._find_filetype(path) while True: candidates = [] for extension, engine in self.engines.items(): idx = filename.rfind('.%s' % extension) if idx < 0: continue candidates.append((extension, idx, engine)) if not candidates: break extension, idx, engine = sorted( candidates, key=lambda x: (x[1] + len(x[0]), len(x[0])), reverse=True)[0] filename = filename[:len(extension) + 1] if filetype not in self._renderers[engine]: self._renderers[engine][filetype] = engine(self, filetype) renderers.append(self._renderers[engine][filetype]) return renderers def _find_filetype(self, path): filename = os.path.basename(path) extensions = filename.split('.')[1:] for i in range(len(extensions), 0, -1): extension = '.'.join(extensions[:i]) try: return next(f for f in self.filetypes.values() if extension in f.extensions) except StopIteration: pass raise TemplateNotFound(path)
class Loaders(defaultdict): def __init__(self, conf): self.conf = conf super().__init__() def __missing__(self, key): if '/' in key: raise ValueError('Invalid File extension "%s"' % (key,)) loaders = list() if self.conf.rootdirs: loaders.append(FileSystemLoader(self.conf.rootdirs, key)) self[key] = loaders return loaders class Engines(dict): def __init__(self, conf): self.conf = conf super().__init__() def __setitem__(self, key, value): if key in self: raise ValueError( 'Renderer for extension `%s` already defined as %s' % (key, repr(self[key]))) if '/' in key: raise ValueError('Invalid File extension "%s"' % (key,)) return super().__setitem__(key, value) class FileTypes(defaultdict): def __init__(self, conf): self.conf = conf super().__init__() def __missing__(self, key): filetype = FileType(self.conf, key) self[key] = filetype return filetype
[docs]class FileType: """ Represents a known :term:`file type`. Attributes may only be modified until :ref:`finalization <finalization>` of the module. """ def __init__(self, conf, mimetype): self.__conf = conf self.__mimetype = mimetype self.__extensions = [] self.__postprocessors = [] self.__globals = [] self.__finalized = False self.__escape = None def _finalize(self): # TODO: check for duplicates in extensions self.__extensions = tuple(self.__extensions) self.__postprocessors = tuple(self.__postprocessors) self.__globals = tuple(self.__globals) self.__finalized = True @property def mimetype(self): return self.__mimetype @property def extensions(self): return self.__extensions @extensions.setter def extensions(self, value): assert not self.__finalized self.__extensions = value @property def postprocessors(self): return self.__postprocessors @postprocessors.setter def postprocessors(self, value): assert not self.__finalized self.__postprocessors = value @property def escape(self): return self.__escape @escape.setter def escape(self, callback): assert not self.__finalized self.__escape = callback @property def globals(self): return self.__globals
[docs] def add_global(self, name, value, *, escape=True): """ Defines a new global variable with given *name* and *value* for this file type. The optional parameter *escape* determines, whether the value must be escaped in the final output. """ assert not self.__finalized assert not any(x for x in self.__globals if x.name == name) self.__globals.append(VariableDefinition(name, value, escape))
VariableDefinition = namedtuple('VariableDefinition', ('name', 'value', 'escape'))