blob: 00d18f2d4bf0327398fe17a2374add3c315f1922 [file] [log] [blame]
# -*- coding: utf-8 -*-
"""
jinja.loaders
~~~~~~~~~~~~~
Jinja loader classes.
:copyright: 2007 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
import codecs
import sha
import time
from os import path
from threading import Lock
from jinja.parser import Parser
from jinja.translators.python import PythonTranslator, Template
from jinja.exceptions import TemplateNotFound
from jinja.utils import CacheDict
try:
from pkg_resources import resource_exists, resource_string, \
resource_filename
except ImportError:
resource_exists = resource_string = resource_filename = None
__all__ = ['FileSystemLoader', 'PackageLoader']
def get_template_filename(searchpath, name):
"""
Return the filesystem filename wanted.
"""
return path.join(searchpath, path.sep.join([p for p in name.split('/')
if p and p[0] != '.']))
def get_cachename(cachepath, name):
"""
Return the filename for a cached file.
"""
return path.join(cachepath, 'jinja_%s.cache' %
sha.new('jinja(%s)tmpl' % name).hexdigest())
class LoaderWrapper(object):
"""
Wraps a loader so that it's bound to an environment.
"""
def __init__(self, environment, loader):
self.environment = environment
self.loader = loader
if self.loader is None:
self.get_source = self.parse = self.load = self._loader_missing
self.available = False
else:
self.available = True
def get_source(self, name, parent=None):
"""Retrieve the sourcecode of a template."""
# just ascii chars are allowed as template names
name = str(name)
return self.loader.get_source(self.environment, name, parent)
def parse(self, name, parent=None):
"""Retreive a template and parse it."""
# just ascii chars are allowed as template names
name = str(name)
return self.loader.parse(self.environment, name, parent)
def load(self, name, translator=PythonTranslator):
"""
Translate a template and return it. This must not necesarily
be a template class. The javascript translator for example
will just output a string with the translated code.
"""
# just ascii chars are allowed as template names
name = str(name)
return self.loader.load(self.environment, name, translator)
def _loader_missing(self, *args, **kwargs):
"""Helper method that overrides all other methods if no
loader is defined."""
raise RuntimeError('no loader defined')
def __nonzero__(self):
return self.loader is not None
class LoaderMixin(object):
"""
Use this class to implement loaders.
Just mixin this class and implement a method called `get_source`
with the signature (`environment`, `name`, `parent`) that returns
sourcecode for the template.
For more complex loaders you probably want to override `load` to
or not use the `LoaderMixin` at all.
"""
def parse(self, environment, name, parent):
"""
Load and parse a template
"""
source = self.get_source(environment, name, parent)
return Parser(environment, source, name).parse()
def load(self, environment, name, translator):
"""
Load and translate a template
"""
ast = self.parse(environment, name, None)
return translator.process(environment, ast)
class CachedLoaderMixin(LoaderMixin):
"""
Works like the loader mixin just that it supports caching.
"""
def __init__(self, use_memcache, cache_size, cache_folder, auto_reload):
if use_memcache:
self.__memcache = CacheDict(cache_size)
else:
self.__memcache = None
self.__cache_folder = cache_folder
if not hasattr(self, 'check_source_changed'):
self.__auto_reload = False
else:
self.__auto_reload = auto_reload
self.__times = {}
self.__lock = Lock()
def load(self, environment, name, translator):
self.__lock.acquire()
try:
# caching is only possible for the python translator. skip
# all other translators
if translator is PythonTranslator:
tmpl = None
# auto reload enabled? check for the last change of
# the template
if self.__auto_reload:
last_change = self.check_source_changed(environment, name)
else:
last_change = None
# check if we have something in the memory cache and the
# memory cache is enabled.
if self.__memcache is not None and name in self.__memcache:
tmpl = self.__memcache[name]
if last_change is not None and \
last_change > self.__times[name]:
tmpl = None
# if diskcache is enabled look for an already compiled
# template.
if self.__cache_folder is not None:
cache_fn = get_cachename(self.__cache_folder, name)
# there is an up to date compiled template
if tmpl is not None and last_change is None:
try:
cache_time = path.getmtime(cache_fn)
except OSError:
cache_time = 0
if last_change >= cache_time:
f = file(cache_fn, 'rb')
try:
tmpl = Template.load(environment, f)
finally:
f.close()
# no template so far, parse, translate and compile it
elif tmpl is None:
tmpl = LoaderMixin.load(self, environment,
name, translator)
# save the compiled template
f = file(cache_fn, 'wb')
try:
tmpl.dump(f)
finally:
f.close()
# if memcaching is enabled push the template
if tmpl is not None:
if self.__memcache is not None:
self.__times[name] = time.time()
self.__memcache[name] = tmpl
return tmpl
# if we reach this point we don't have caching enabled or translate
# to something else than python
return LoaderMixin.load(self, environment, name, translator)
finally:
self.__lock.release()
class FileSystemLoader(CachedLoaderMixin):
"""
Loads templates from the filesystem:
.. sourcecode:: python
from jinja import Environment, FileSystemLoader
e = Environment(loader=FileSystemLoader('templates/'))
You can pass the following keyword arguments to the loader on
initialisation:
=================== =================================================
``searchpath`` String with the path to the templates on the
filesystem.
``use_memcache`` Set this to ``True`` to enable memory caching.
This is usually a good idea in production mode,
but disable it during development since it won't
reload template changes automatically.
This only works in persistent environments like
FastCGI.
``memcache_size`` Number of template instance you want to cache.
Defaults to ``40``.
``cache_folder`` Set this to an existing directory to enable
caching of templates on the file system. Note
that this only affects templates transformed
into python code. Default is ``None`` which means
that caching is disabled.
``auto_reload`` Set this to `False` for a slightly better
performance. In that case Jinja won't check for
template changes on the filesystem.
=================== =================================================
"""
def __init__(self, searchpath, use_memcache=False, memcache_size=40,
cache_folder=None, auto_reload=True):
self.searchpath = searchpath
CachedLoaderMixin.__init__(self, use_memcache, memcache_size,
cache_folder, auto_reload)
def get_source(self, environment, name, parent):
filename = get_template_filename(self.searchpath, name)
if path.exists(filename):
f = codecs.open(filename, 'r', environment.template_charset)
try:
return f.read()
finally:
f.close()
else:
raise TemplateNotFound(name)
def check_source_changed(self, environment, name):
return path.getmtime(get_template_filename(self.searchpath, name))
class PackageLoader(CachedLoaderMixin):
"""
Loads templates from python packages using setuptools.
.. sourcecode:: python
from jinja import Environment, PackageLoader
e = Environment(loader=PackageLoader('yourapp', 'template/path'))
You can pass the following keyword arguments to the loader on
initialisation:
=================== =================================================
``package_name`` Name of the package containing the templates.
``package_path`` Path of the templates inside the package.
``use_memcache`` Set this to ``True`` to enable memory caching.
This is usually a good idea in production mode,
but disable it during development since it won't
reload template changes automatically.
This only works in persistent environments like
FastCGI.
``memcache_size`` Number of template instance you want to cache.
Defaults to ``40``.
``cache_folder`` Set this to an existing directory to enable
caching of templates on the file system. Note
that this only affects templates transformed
into python code. Default is ``None`` which means
that caching is disabled.
``auto_reload`` Set this to `False` for a slightly better
performance. In that case Jinja won't check for
template changes on the filesystem. If the
templates are inside of an egg file this won't
have an effect.
=================== =================================================
"""
def __init__(self, package_name, package_path, use_memcache=False,
memcache_size=40, cache_folder=None, auto_reload=True):
if resource_filename is None:
raise ImportError('setuptools not found')
self.package_name = package_name
self.package_path = package_path
# if we have an loader we probably retrieved it from an egg
# file. In that case don't use the auto_reload!
if auto_reload:
package = __import__(package_name, '', '', [''])
if package.__loader__ is not None:
auto_reload = False
CachedLoaderMixin.__init__(self, use_memcache, memcache_size,
cache_folder, auto_reload)
def get_source(self, environment, name, parent):
name = '/'.join([self.package_path] + [p for p in name.split('/')
if p and p[0] != '.'])
if not resource_exists(self.package, name):
raise TemplateNotFound(name)
contents = resource_string(self.package_name, name)
return contents.decode(environment.template_charset)
def check_source_changed(self, environment, name):
name = '/'.join([self.package_path] + [p for p in name.split('/')
if p and p[0] != '.'])
return path.getmtime(resource_filename(name))