blob: 0f0ced7aefa1e1b398ee4489bf9f96525548ba99 [file] [log] [blame]
# -*- coding: utf-8 -*-
"""
jinja.loaders
~~~~~~~~~~~~~
Jinja loader classes.
:copyright: 2007 by Armin Ronacher, Bryan McLemore.
: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, TemplateSyntaxError
from jinja.utils import CacheDict, raise_syntax_error
#: when updating this, update the listing in the jinja package too
__all__ = ['FileSystemLoader', 'PackageLoader', 'DictLoader', 'ChoiceLoader',
'FunctionLoader', 'MemcachedFileSystemLoader']
def get_template_filename(searchpath, name):
"""
Return the filesystem filename wanted.
"""
return path.join(searchpath, *[p for p in name.split('/')
if p and p[0] != '.'])
def get_cachename(cachepath, name, salt=None):
"""
Return the filename for a cached file.
"""
return path.join(cachepath, 'jinja_%s.cache' %
sha.new('jinja(%s|%s)tmpl' %
(name, salt or '')).hexdigest())
def _loader_missing(*args, **kwargs):
"""Helper function for `LoaderWrapper`."""
raise RuntimeError('no loader defined')
class LoaderWrapper(object):
"""
Wraps a loader so that it's bound to an environment.
Also handles template syntax errors.
"""
def __init__(self, environment, loader):
self.environment = environment
self.loader = loader
if self.loader is None:
self.get_source = self.parse = self.load = _loader_missing
self.available = False
else:
self.available = True
def __getattr__(self, name):
"""
Not found attributes are redirected to the loader
"""
return getattr(self.loader, name)
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)
try:
return self.loader.load(self.environment, name, translator)
except TemplateSyntaxError, e:
if not self.environment.friendly_traceback:
raise
__traceback_hide__ = True
raise_syntax_error(e, self.environment)
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.available
class BaseLoader(object):
"""
Use this class to implement loaders.
Just inherit from 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 `BaseLoader` 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)
def get_source(self, environment, name, parent):
"""
Override this method to get the source for a template.
"""
raise TemplateNotFound(name)
class CachedLoaderMixin(object):
"""
Mixin this class to implement simple memory and disk caching. The
memcaching just uses a dict in the loader so if you have a global
environment or at least a global loader this can speed things up.
If the memcaching is enabled you can use (with Jinja 1.1 onwards)
the `clear_memcache` function to clear the cache.
For memcached support check the `MemcachedLoaderMixin`.
"""
def __init__(self, use_memcache, cache_size, cache_folder, auto_reload,
cache_salt=None):
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.__salt = cache_salt
self.__times = {}
self.__lock = Lock()
def clear_memcache(self):
"""
Clears the memcache.
"""
if self.__memcache is not None:
self.__memcache.clear()
def load(self, environment, name, translator):
"""
Load and translate a template. First we check if there is a
cached version of this template in the memory cache. If this is
not the cache check for a compiled template in the disk cache
folder. And if none of this is the case we translate the temlate,
cache and return it.
"""
self.__lock.acquire()
try:
# caching is only possible for the python translator. skip
# all other translators
if translator is not PythonTranslator:
return super(CachedLoaderMixin, self).load(
environment, name, translator)
tmpl = None
save_to_disk = False
push_to_memory = False
# 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:
if name in self.__memcache:
tmpl = self.__memcache[name]
# if auto reload is enabled check if the template changed
if last_change and last_change > self.__times[name]:
tmpl = None
push_to_memory = True
else:
push_to_memory = True
# mem cache disabled or not cached by now
# try to load if from the disk cache
if tmpl is None and self.__cache_folder is not None:
cache_fn = get_cachename(self.__cache_folder, name, self.__salt)
if last_change is not None:
try:
cache_time = path.getmtime(cache_fn)
except OSError:
cache_time = 0
if last_change is None or (cache_time and
last_change <= cache_time):
try:
f = file(cache_fn, 'rb')
except IOError:
tmpl = None
save_to_disk = True
else:
try:
tmpl = Template.load(environment, f)
finally:
f.close()
else:
save_to_disk = True
# if we still have no template we load, parse and translate it.
if tmpl is None:
tmpl = super(CachedLoaderMixin, self).load(
environment, name, translator)
# save the compiled template on the disk if enabled
if save_to_disk:
f = file(cache_fn, 'wb')
try:
tmpl.dump(f)
finally:
f.close()
# if memcaching is enabled and the template not loaded
# we add that there.
if push_to_memory:
self.__times[name] = time.time()
self.__memcache[name] = tmpl
return tmpl
finally:
self.__lock.release()
class MemcachedLoaderMixin(object):
"""
Uses a memcached server to cache the templates.
Requires the memcache library from tummy__.
__ http://www.tummy.com/Community/software/python-memcached/
"""
def __init__(self, use_memcache, memcache_time=60 * 60 * 24 * 7,
memcache_host=None, item_prefix='template/'):
try:
from memcache import Client
except ImportError:
raise RuntimeError('the %r loader requires an installed '
'memcache module' % self.__class__.__name__)
if memcache_host is None:
memcache_host = ['127.0.0.1:11211']
if use_memcache:
self.__memcache = Client(list(memcache_host))
self.__memcache_time = memcache_time
else:
self.__memcache = None
self.__item_prefix = item_prefix
self.__lock = Lock()
def load(self, environment, name, translator):
"""
Load and translate a template. First we check if there is a
cached version of this template in the memory cache. If this is
not the cache check for a compiled template in the disk cache
folder. And if none of this is the case we translate the template,
cache and return it.
"""
self.__lock.acquire()
try:
# caching is only possible for the python translator. skip
# all other translators
if translator is not PythonTranslator:
return super(MemcachedLoaderMixin, self).load(
environment, name, translator)
tmpl = None
push_to_memory = False
# check if we have something in the memory cache and the
# memory cache is enabled.
if self.__memcache is not None:
bytecode = self.__memcache.get(self.__item_prefix + name)
if bytecode:
tmpl = Template.load(environment, bytecode)
else:
push_to_memory = True
# if we still have no template we load, parse and translate it.
if tmpl is None:
tmpl = super(MemcachedLoaderMixin, self).load(
environment, name, translator)
# if memcaching is enabled and the template not loaded
# we add that there.
if push_to_memory:
self.__memcache.set(self.__item_prefix + name, tmpl.dump(),
self.__memcache_time)
return tmpl
finally:
self.__lock.release()
class BaseFileSystemLoader(BaseLoader):
"""
Baseclass for the file system loader that does not do any caching.
It exists to avoid redundant code, just don't use it without subclassing.
How subclassing can work:
.. sourcecode:: python
from jinja.loaders import BaseFileSystemLoader
class MyFileSystemLoader(BaseFileSystemLoader):
def __init__(self):
BaseFileSystemLoader.__init__(self, '/path/to/templates')
The base file system loader only takes one parameter beside self which
is the path to the templates.
"""
def __init__(self, searchpath):
self.searchpath = path.abspath(searchpath)
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)
class FileSystemLoader(CachedLoaderMixin, BaseFileSystemLoader):
"""
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
initialization:
=================== =================================================
``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.
``cache_salt`` Optional unique number to not confuse the
caching system when caching more than one
template loader in the same folder. Defaults
to the searchpath. *New in Jinja 1.1*
=================== =================================================
"""
def __init__(self, searchpath, use_memcache=False, memcache_size=40,
cache_folder=None, auto_reload=True, cache_salt=None):
BaseFileSystemLoader.__init__(self, searchpath)
if cache_salt is None:
cache_salt = self.searchpath
CachedLoaderMixin.__init__(self, use_memcache, memcache_size,
cache_folder, auto_reload, cache_salt)
def check_source_changed(self, environment, name):
filename = get_template_filename(self.searchpath, name)
if path.exists(filename):
return path.getmtime(filename)
return -1
class MemcachedFileSystemLoader(MemcachedLoaderMixin, BaseFileSystemLoader):
"""
Loads templates from the filesystem and caches them on a memcached
server.
.. sourcecode:: python
from jinja import Environment, MemcachedFileSystemLoader
e = Environment(loader=MemcachedFileSystemLoader('templates/',
memcache_host=['192.168.2.250:11211']
))
You can pass the following keyword arguments to the loader on
initialization:
=================== =================================================
``searchpath`` String with the path to the templates on the
filesystem.
``use_memcache`` Set this to ``True`` to enable memcached caching.
In that case it behaves like a normal
`FileSystemLoader` with disabled caching.
``memcache_time`` The expire time of a template in the cache.
``memcache_host`` a list of memcached servers.
``item_prefix`` The prefix for the items on the server. Defaults
to ``'template/'``.
=================== =================================================
"""
def __init__(self, searchpath, use_memcache=True,
memcache_time=60 * 60 * 24 * 7, memcache_host=None,
item_prefix='template/'):
BaseFileSystemLoader.__init__(self, searchpath)
MemcachedLoaderMixin.__init__(self, use_memcache, memcache_time,
memcache_host, item_prefix)
class BasePackageLoader(BaseLoader):
"""
Baseclass for the package loader that does not do any caching.
It accepts two parameters: The name of the package and the path relative
to the package:
.. sourcecode:: python
from jinja.loaders import BasePackageLoader
class MyPackageLoader(BasePackageLoader):
def __init__(self):
BasePackageLoader.__init__(self, 'my_package', 'shared/templates')
The relative path must use slashes as path delimiters, even on Mac OS
and Microsoft Windows.
It uses the `pkg_resources` libraries distributed with setuptools for
retrieving the data from the packages. This works for eggs too so you
don't have to mark your egg as non zip safe.
"""
def __init__(self, package_name, package_path):
try:
import pkg_resources
except ImportError:
raise RuntimeError('setuptools not installed')
self.package_name = package_name
self.package_path = package_path
def get_source(self, environment, name, parent):
from pkg_resources import resource_exists, resource_string
path = '/'.join([self.package_path] + [p for p in name.split('/')
if p != '..'])
if not resource_exists(self.package_name, path):
raise TemplateNotFound(name)
contents = resource_string(self.package_name, path)
return contents.decode(environment.template_charset)
class PackageLoader(CachedLoaderMixin, BasePackageLoader):
"""
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
initialization:
=================== =================================================
``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.
``cache_salt`` Optional unique number to not confuse the
caching system when caching more than one
template loader in the same folder. Defaults
to ``package_name + '/' + package_path``.
*New in Jinja 1.1*
=================== =================================================
Important note: If you're using an application that is inside of an
egg never set `auto_reload` to `True`. The egg resource manager will
automatically export files to the file system and touch them so that
you not only end up with additional temporary files but also an automatic
reload each time you load a template.
"""
def __init__(self, package_name, package_path, use_memcache=False,
memcache_size=40, cache_folder=None, auto_reload=True,
cache_salt=None):
BasePackageLoader.__init__(self, package_name, package_path)
if cache_salt is None:
cache_salt = package_name + '/' + package_path
CachedLoaderMixin.__init__(self, use_memcache, memcache_size,
cache_folder, auto_reload, cache_salt)
def check_source_changed(self, environment, name):
from pkg_resources import resource_exists, resource_filename
fn = resource_filename(self.package_name, '/'.join([self.package_path] +
[p for p in name.split('/') if p and p[0] != '.']))
if resource_exists(self.package_name, fn):
return path.getmtime(fn)
return -1
class BaseFunctionLoader(BaseLoader):
"""
Baseclass for the function loader that doesn't do any caching.
It just accepts one parameter which is the function which is called
with the name of the requested template. If the return value is `None`
the loader will raise a `TemplateNotFound` error.
.. sourcecode:: python
from jinja.loaders import BaseFunctionLoader
templates = {...}
class MyFunctionLoader(BaseFunctionLoader):
def __init__(self):
BaseFunctionLoader(templates.get)
"""
def __init__(self, loader_func):
self.loader_func = loader_func
def get_source(self, environment, name, parent):
rv = self.loader_func(name)
if rv is None:
raise TemplateNotFound(name)
if isinstance(rv, str):
return rv.decode(environment.template_charset)
return rv
class FunctionLoader(CachedLoaderMixin, BaseFunctionLoader):
"""
Loads templates by calling a function which has to return a string
or `None` if an error occoured.
.. sourcecode:: python
from jinja import Environment, FunctionLoader
def my_load_func(template_name):
if template_name == 'foo':
return '...'
e = Environment(loader=FunctionLoader(my_load_func))
Because the interface is limited there is no way to cache such
templates. Usually you should try to use a loader with a more
solid backend.
You can pass the following keyword arguments to the loader on
initialization:
=================== =================================================
``loader_func`` Function that takes the name of the template to
load. If it returns a string or unicode object
it's used to load a template. If the return
value is None it's considered missing.
``getmtime_func`` Function used to check if templates requires
reloading. Has to return the UNIX timestamp of
the last template change or ``-1`` if this template
does not exist or requires updates at any cost.
``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 of `getmtime_func`
not being provided this won't have an effect.
``cache_salt`` Optional unique number to not confuse the
caching system when caching more than one
template loader in the same folder.
=================== =================================================
"""
def __init__(self, loader_func, getmtime_func=None, use_memcache=False,
memcache_size=40, cache_folder=None, auto_reload=True,
cache_salt=None):
BaseFunctionLoader.__init__(self, loader_func)
# when changing the signature also check the jinja.plugin function
# loader instantiation.
self.getmtime_func = getmtime_func
if auto_reload and getmtime_func is None:
auto_reload = False
CachedLoaderMixin.__init__(self, use_memcache, memcache_size,
cache_folder, auto_reload, cache_salt)
def check_source_changed(self, environment, name):
return self.getmtime_func(name)
class DictLoader(BaseLoader):
"""
Load templates from a given dict:
.. sourcecode:: python
from jinja import Environment, DictLoader
e = Environment(loader=DictLoader(dict(
layout='...',
index='{% extends 'layout' %}...'
)))
This loader does not have any caching capabilities.
"""
def __init__(self, templates):
self.templates = templates
def get_source(self, environment, name, parent):
if name in self.templates:
return self.templates[name]
raise TemplateNotFound(name)
class ChoiceLoader(object):
"""
A loader that tries multiple loaders in the order they are given to
the `ChoiceLoader`:
.. sourcecode:: python
from jinja import ChoiceLoader, FileSystemLoader
loader1 = FileSystemLoader("templates1")
loader2 = FileSystemLoader("templates2")
loader = ChoiceLoader([loader1, loader2])
"""
def __init__(self, loaders):
self.loaders = list(loaders)
def get_source(self, environment, name, parent):
for loader in self.loaders:
try:
return loader.get_source(environment, name, parent)
except TemplateNotFound, e:
if e.name != name:
raise
continue
raise TemplateNotFound(name)
def parse(self, environment, name, parent):
for loader in self.loaders:
try:
return loader.parse(environment, name, parent)
except TemplateNotFound, e:
if e.name != name:
raise
continue
raise TemplateNotFound(name)
def load(self, environment, name, translator):
for loader in self.loaders:
try:
return loader.load(environment, name, translator)
except TemplateNotFound, e:
if e.name != name:
raise
continue
raise TemplateNotFound(name)