cmlenz@336: # -*- coding: utf-8 -*- cmlenz@336: # cmlenz@408: # Copyright (C) 2006-2007 Edgewall Software cmlenz@336: # All rights reserved. cmlenz@336: # cmlenz@336: # This software is licensed as described in the file COPYING, which cmlenz@336: # you should have received as part of this distribution. The terms cmlenz@336: # are also available at http://genshi.edgewall.org/wiki/License. cmlenz@336: # cmlenz@336: # This software consists of voluntary contributions made by many cmlenz@336: # individuals. For the exact contribution history, see the revision cmlenz@336: # history and logs, available at http://genshi.edgewall.org/log/. cmlenz@336: cmlenz@336: """Template loading and caching.""" cmlenz@336: cmlenz@336: import os cmlenz@336: try: cmlenz@336: import threading cmlenz@336: except ImportError: cmlenz@336: import dummy_threading as threading cmlenz@336: cmlenz@400: from genshi.template.base import TemplateError cmlenz@336: from genshi.util import LRUCache cmlenz@336: cmlenz@336: __all__ = ['TemplateLoader', 'TemplateNotFound'] cmlenz@425: __docformat__ = 'restructuredtext en' cmlenz@336: cmlenz@336: cmlenz@336: class TemplateNotFound(TemplateError): cmlenz@336: """Exception raised when a specific template file could not be found.""" cmlenz@336: cmlenz@336: def __init__(self, name, search_path): cmlenz@435: """Create the exception. cmlenz@435: cmlenz@435: :param name: the filename of the template cmlenz@435: :param search_path: the search path used to lookup the template cmlenz@435: """ cmlenz@336: TemplateError.__init__(self, 'Template "%s" not found' % name) cmlenz@336: self.search_path = search_path cmlenz@336: cmlenz@336: cmlenz@336: class TemplateLoader(object): cmlenz@336: """Responsible for loading templates from files on the specified search cmlenz@336: path. cmlenz@336: cmlenz@336: >>> import tempfile cmlenz@336: >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template') cmlenz@336: >>> os.write(fd, '

$var

') cmlenz@336: 11 cmlenz@336: >>> os.close(fd) cmlenz@336: cmlenz@336: The template loader accepts a list of directory paths that are then used cmlenz@336: when searching for template files, in the given order: cmlenz@336: cmlenz@336: >>> loader = TemplateLoader([os.path.dirname(path)]) cmlenz@336: cmlenz@336: The `load()` method first checks the template cache whether the requested cmlenz@336: template has already been loaded. If not, it attempts to locate the cmlenz@336: template file, and returns the corresponding `Template` object: cmlenz@336: cmlenz@363: >>> from genshi.template import MarkupTemplate cmlenz@336: >>> template = loader.load(os.path.basename(path)) cmlenz@336: >>> isinstance(template, MarkupTemplate) cmlenz@336: True cmlenz@336: cmlenz@336: Template instances are cached: requesting a template with the same name cmlenz@336: results in the same instance being returned: cmlenz@336: cmlenz@336: >>> loader.load(os.path.basename(path)) is template cmlenz@336: True cmlenz@336: cmlenz@548: The `auto_reload` option can be used to control whether a template should cmlenz@548: be automatically reloaded when the file it was loaded from has been cmlenz@548: changed. Disable this automatic reloading to improve performance. cmlenz@548: cmlenz@336: >>> os.remove(path) cmlenz@336: """ cmlenz@336: def __init__(self, search_path=None, auto_reload=False, cmlenz@439: default_encoding=None, max_cache_size=25, default_class=None, cmlenz@606: variable_lookup='strict', allow_exec=True, callback=None): cmlenz@336: """Create the template laoder. cmlenz@336: cmlenz@425: :param search_path: a list of absolute path names that should be cmlenz@425: searched for template files, or a string containing cmlenz@696: a single absolute path; alternatively, any item on cmlenz@696: the list may be a ''load function'' that is passed cmlenz@696: a filename and returns a file-like object and some cmlenz@696: metadata cmlenz@425: :param auto_reload: whether to check the last modification time of cmlenz@425: template files, and reload them if they have changed cmlenz@425: :param default_encoding: the default encoding to assume when loading cmlenz@425: templates; defaults to UTF-8 cmlenz@425: :param max_cache_size: the maximum number of templates to keep in the cmlenz@425: cache cmlenz@425: :param default_class: the default `Template` subclass to use when cmlenz@425: instantiating templates cmlenz@606: :param variable_lookup: the variable lookup mechanism; either "strict" cmlenz@606: (the default), "lenient", or a custom lookup cmlenz@442: class cmlenz@545: :param allow_exec: whether to allow Python code blocks in templates cmlenz@439: :param callback: (optional) a callback function that is invoked after a cmlenz@439: template was initialized by this loader; the function cmlenz@439: is passed the template object as only argument. This cmlenz@439: callback can be used for example to add any desired cmlenz@439: filters to the template cmlenz@442: :see: `LenientLookup`, `StrictLookup` cmlenz@545: cmlenz@545: :note: Changed in 0.5: Added the `allow_exec` argument cmlenz@336: """ cmlenz@363: from genshi.template.markup import MarkupTemplate cmlenz@363: cmlenz@336: self.search_path = search_path cmlenz@336: if self.search_path is None: cmlenz@336: self.search_path = [] cmlenz@696: elif not isinstance(self.search_path, (list, tuple)): cmlenz@336: self.search_path = [self.search_path] cmlenz@548: cmlenz@336: self.auto_reload = auto_reload cmlenz@548: """Whether templates should be reloaded when the underlying file is cmlenz@548: changed""" cmlenz@548: cmlenz@336: self.default_encoding = default_encoding cmlenz@363: self.default_class = default_class or MarkupTemplate cmlenz@442: self.variable_lookup = variable_lookup cmlenz@545: self.allow_exec = allow_exec cmlenz@439: if callback is not None and not callable(callback): cmlenz@439: raise TypeError('The "callback" parameter needs to be callable') cmlenz@439: self.callback = callback cmlenz@336: self._cache = LRUCache(max_cache_size) cmlenz@336: self._mtime = {} cmlenz@548: self._lock = threading.RLock() cmlenz@336: cmlenz@363: def load(self, filename, relative_to=None, cls=None, encoding=None): cmlenz@336: """Load the template with the given name. cmlenz@336: cmlenz@696: If the `filename` parameter is relative, this method searches the cmlenz@696: search path trying to locate a template matching the given name. If the cmlenz@696: file name is an absolute path, the search path is ignored. cmlenz@336: cmlenz@435: If the requested template is not found, a `TemplateNotFound` exception cmlenz@435: is raised. Otherwise, a `Template` object is returned that represents cmlenz@435: the parsed template. cmlenz@336: cmlenz@336: Template instances are cached to avoid having to parse the same cmlenz@336: template file more than once. Thus, subsequent calls of this method cmlenz@336: with the same template file name will return the same `Template` cmlenz@425: object (unless the ``auto_reload`` option is enabled and the file was cmlenz@336: changed since the last parse.) cmlenz@336: cmlenz@336: If the `relative_to` parameter is provided, the `filename` is cmlenz@336: interpreted as being relative to that path. cmlenz@336: cmlenz@425: :param filename: the relative path of the template file to load cmlenz@425: :param relative_to: the filename of the template from which the new cmlenz@435: template is being loaded, or ``None`` if the cmlenz@435: template is being loaded directly cmlenz@425: :param cls: the class of the template object to instantiate cmlenz@425: :param encoding: the encoding of the template to load; defaults to the cmlenz@425: ``default_encoding`` of the loader instance cmlenz@435: :return: the loaded `Template` instance cmlenz@695: :raises TemplateNotFound: if a template with the given name could not cmlenz@695: be found cmlenz@336: """ cmlenz@363: if cls is None: cmlenz@363: cls = self.default_class cmlenz@336: if relative_to and not os.path.isabs(relative_to): cmlenz@336: filename = os.path.join(os.path.dirname(relative_to), filename) cmlenz@336: filename = os.path.normpath(filename) cmlenz@336: cmlenz@336: self._lock.acquire() cmlenz@336: try: cmlenz@336: # First check the cache to avoid reparsing the same file cmlenz@336: try: cmlenz@336: tmpl = self._cache[filename] cmlenz@696: if not self.auto_reload: cmlenz@696: return tmpl cmlenz@696: mtime = self._mtime[filename] cmlenz@696: if mtime and mtime == os.path.getmtime(tmpl.filepath): cmlenz@336: return tmpl cmlenz@590: except KeyError, OSError: cmlenz@336: pass cmlenz@336: cmlenz@336: search_path = self.search_path cmlenz@336: isabs = False cmlenz@336: cmlenz@336: if os.path.isabs(filename): cmlenz@336: # Bypass the search path if the requested filename is absolute cmlenz@336: search_path = [os.path.dirname(filename)] cmlenz@336: isabs = True cmlenz@336: cmlenz@336: elif relative_to and os.path.isabs(relative_to): cmlenz@336: # Make sure that the directory containing the including cmlenz@336: # template is on the search path cmlenz@336: dirname = os.path.dirname(relative_to) cmlenz@336: if dirname not in search_path: cmlenz@697: search_path = list(search_path) + [dirname] cmlenz@336: isabs = True cmlenz@336: cmlenz@336: elif not search_path: cmlenz@336: # Uh oh, don't know where to look for the template cmlenz@336: raise TemplateError('Search path for templates not configured') cmlenz@336: cmlenz@696: for loadfunc in search_path: cmlenz@696: if isinstance(loadfunc, basestring): cmlenz@696: loadfunc = TemplateLoader.directory(loadfunc) cmlenz@336: try: cmlenz@696: dirname, filename, fileobj, mtime = loadfunc(filename) cmlenz@696: except IOError: cmlenz@696: continue cmlenz@696: else: cmlenz@336: try: cmlenz@336: if isabs: cmlenz@336: # If the filename of either the included or the cmlenz@336: # including template is absolute, make sure the cmlenz@336: # included template gets an absolute path, too, cmlenz@696: # so that nested includes work properly without a cmlenz@336: # search path cmlenz@336: filename = os.path.join(dirname, filename) cmlenz@336: dirname = '' cmlenz@695: tmpl = self.instantiate(cls, fileobj, dirname, cmlenz@695: filename, encoding=encoding) cmlenz@439: if self.callback: cmlenz@439: self.callback(tmpl) cmlenz@439: self._cache[filename] = tmpl cmlenz@696: self._mtime[filename] = mtime cmlenz@336: finally: cmlenz@696: if hasattr(fileobj, 'close'): cmlenz@696: fileobj.close() cmlenz@336: return tmpl cmlenz@336: cmlenz@336: raise TemplateNotFound(filename, search_path) cmlenz@336: cmlenz@336: finally: cmlenz@336: self._lock.release() cmlenz@695: cmlenz@695: def instantiate(self, cls, fileobj, dirname, filename, encoding=None): cmlenz@695: """Instantiate and return the `Template` object based on the given cmlenz@695: class and parameters. cmlenz@695: cmlenz@695: This function is intended for subclasses to override if they need to cmlenz@695: implement special template instantiation logic. Code that just uses cmlenz@695: the `TemplateLoader` should use the `load` method instead. cmlenz@695: cmlenz@695: :param cls: the class of the template object to instantiate cmlenz@695: :param fileobj: a readable file-like object containing the template cmlenz@695: source cmlenz@695: :param dirname: the name of the base directory containing the template cmlenz@695: file cmlenz@695: :param filename: the name of the template file, relative to the given cmlenz@695: base directory cmlenz@695: :param encoding: the encoding of the template to load; defaults to the cmlenz@695: ``default_encoding`` of the loader instance cmlenz@695: :return: the loaded `Template` instance cmlenz@695: :rtype: `Template` cmlenz@695: """ cmlenz@695: if encoding is None: cmlenz@695: encoding = self.default_encoding cmlenz@695: return cls(fileobj, basedir=dirname, filename=filename, loader=self, cmlenz@695: encoding=encoding, lookup=self.variable_lookup, cmlenz@695: allow_exec=self.allow_exec) cmlenz@696: cmlenz@696: def directory(path): cmlenz@696: """Loader factory for loading templates from a local directory. cmlenz@696: cmlenz@696: :param path: the path to the local directory containing the templates cmlenz@696: :return: the loader function to load templates from the given directory cmlenz@696: :rtype: ``function`` cmlenz@696: """ cmlenz@696: def _load_from_directory(filename): cmlenz@696: filepath = os.path.join(path, filename) cmlenz@696: fileobj = open(filepath, 'U') cmlenz@696: return path, filename, fileobj, os.path.getmtime(filepath) cmlenz@696: return _load_from_directory cmlenz@696: directory = staticmethod(directory) cmlenz@696: cmlenz@696: def package(name, path): cmlenz@696: """Loader factory for loading templates from egg package data. cmlenz@696: cmlenz@696: :param name: the name of the package containing the resources cmlenz@696: :param path: the path inside the package data cmlenz@696: :return: the loader function to load templates from the given package cmlenz@696: :rtype: ``function`` cmlenz@696: """ cmlenz@696: from pkg_resources import resource_stream cmlenz@696: def _load_from_package(filename): cmlenz@696: filepath = os.path.join(path, filename) cmlenz@696: return path, filename, resource_stream(name, filepath), None cmlenz@696: return _load_from_package cmlenz@696: package = staticmethod(package) cmlenz@696: cmlenz@696: def prefixed(**delegates): cmlenz@696: """Factory for a load function that delegates to other loaders cmlenz@696: depending on the prefix of the requested template path. cmlenz@696: cmlenz@696: The prefix is stripped from the filename when passing on the load cmlenz@696: request to the delegate. cmlenz@696: cmlenz@696: >>> load = prefixed( cmlenz@696: ... app1 = lambda filename: ('app1', filename), cmlenz@696: ... app2 = lambda filename: ('app2', filename) cmlenz@696: ... ) cmlenz@696: >>> print load('app1/foo.html') cmlenz@696: ('app1', 'foo.html') cmlenz@696: >>> print load('app2/bar.html') cmlenz@696: ('app2', 'bar.html') cmlenz@696: cmlenz@696: :param delegates: mapping of path prefixes to loader functions cmlenz@696: :return: the loader function cmlenz@696: :rtype: ``function`` cmlenz@696: """ cmlenz@696: def _dispatch_by_prefix(filename): cmlenz@696: for prefix, delegate in delegates.items(): cmlenz@696: if filename.startswith(prefix): cmlenz@696: if isinstance(delegate, basestring): cmlenz@696: delegate = TemplateLoader.directory(delegate) cmlenz@696: return delegate(filename[len(prefix):].lstrip('/\\')) cmlenz@696: raise TemplateNotFound(filename, delegates.keys()) cmlenz@696: return _dispatch_by_prefix cmlenz@696: prefixed = staticmethod(prefixed) cmlenz@696: cmlenz@696: directory = TemplateLoader.directory cmlenz@696: package = TemplateLoader.package cmlenz@696: prefixed = TemplateLoader.prefixed