Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 1 | # -*- coding: utf-8 -*- |
| 2 | """ |
Armin Ronacher | bcb7c53 | 2008-04-11 16:30:34 +0200 | [diff] [blame] | 3 | jinja2.loaders |
| 4 | ~~~~~~~~~~~~~~ |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 5 | |
| 6 | Jinja loader classes. |
| 7 | |
Armin Ronacher | 55494e4 | 2010-01-22 09:41:48 +0100 | [diff] [blame] | 8 | :copyright: (c) 2010 by the Jinja Team. |
Armin Ronacher | 07bc684 | 2008-03-31 14:18:49 +0200 | [diff] [blame] | 9 | :license: BSD, see LICENSE for more details. |
| 10 | """ |
Armin Ronacher | 57c9b6d | 2008-09-13 19:19:22 +0200 | [diff] [blame] | 11 | from os import path |
Armin Ronacher | 63fd798 | 2008-06-20 18:47:56 +0200 | [diff] [blame] | 12 | try: |
| 13 | from hashlib import sha1 |
| 14 | except ImportError: |
| 15 | from sha import new as sha1 |
Armin Ronacher | bcb7c53 | 2008-04-11 16:30:34 +0200 | [diff] [blame] | 16 | from jinja2.exceptions import TemplateNotFound |
Armin Ronacher | d416a97 | 2009-02-24 22:58:00 +0100 | [diff] [blame] | 17 | from jinja2.utils import LRUCache, open_if_exists, internalcode |
Armin Ronacher | bcb7c53 | 2008-04-11 16:30:34 +0200 | [diff] [blame] | 18 | |
| 19 | |
Armin Ronacher | 9a82205 | 2008-04-17 18:44:07 +0200 | [diff] [blame] | 20 | def split_template_path(template): |
| 21 | """Split a path into segments and perform a sanity check. If it detects |
| 22 | '..' in the path it will raise a `TemplateNotFound` error. |
| 23 | """ |
| 24 | pieces = [] |
| 25 | for piece in template.split('/'): |
| 26 | if path.sep in piece \ |
| 27 | or (path.altsep and path.altsep in piece) or \ |
| 28 | piece == path.pardir: |
| 29 | raise TemplateNotFound(template) |
Armin Ronacher | 58f351d | 2008-05-28 21:30:14 +0200 | [diff] [blame] | 30 | elif piece and piece != '.': |
Armin Ronacher | 9a82205 | 2008-04-17 18:44:07 +0200 | [diff] [blame] | 31 | pieces.append(piece) |
| 32 | return pieces |
| 33 | |
| 34 | |
Armin Ronacher | bcb7c53 | 2008-04-11 16:30:34 +0200 | [diff] [blame] | 35 | class BaseLoader(object): |
Armin Ronacher | 203bfcb | 2008-04-24 21:54:44 +0200 | [diff] [blame] | 36 | """Baseclass for all loaders. Subclass this and override `get_source` to |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 37 | implement a custom loading mechanism. The environment provides a |
| 38 | `get_template` method that calls the loader's `load` method to get the |
| 39 | :class:`Template` object. |
Armin Ronacher | 814f6c2 | 2008-04-17 15:52:23 +0200 | [diff] [blame] | 40 | |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 41 | A very basic example for a loader that looks up templates on the file |
| 42 | system could look like this:: |
| 43 | |
| 44 | from jinja2 import BaseLoader, TemplateNotFound |
| 45 | from os.path import join, exists, getmtime |
| 46 | |
| 47 | class MyLoader(BaseLoader): |
| 48 | |
Armin Ronacher | 63fd798 | 2008-06-20 18:47:56 +0200 | [diff] [blame] | 49 | def __init__(self, path): |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 50 | self.path = path |
| 51 | |
| 52 | def get_source(self, environment, template): |
| 53 | path = join(self.path, template) |
| 54 | if not exists(path): |
| 55 | raise TemplateNotFound(template) |
| 56 | mtime = getmtime(path) |
| 57 | with file(path) as f: |
| 58 | source = f.read().decode('utf-8') |
Armin Ronacher | 4dc9578 | 2008-05-04 01:11:14 +0200 | [diff] [blame] | 59 | return source, path, lambda: mtime == getmtime(path) |
Armin Ronacher | 814f6c2 | 2008-04-17 15:52:23 +0200 | [diff] [blame] | 60 | """ |
| 61 | |
Armin Ronacher | bcb7c53 | 2008-04-11 16:30:34 +0200 | [diff] [blame] | 62 | def get_source(self, environment, template): |
Armin Ronacher | 814f6c2 | 2008-04-17 15:52:23 +0200 | [diff] [blame] | 63 | """Get the template source, filename and reload helper for a template. |
| 64 | It's passed the environment and template name and has to return a |
| 65 | tuple in the form ``(source, filename, uptodate)`` or raise a |
| 66 | `TemplateNotFound` error if it can't locate the template. |
| 67 | |
| 68 | The source part of the returned tuple must be the source of the |
| 69 | template as unicode string or a ASCII bytestring. The filename should |
| 70 | be the name of the file on the filesystem if it was loaded from there, |
| 71 | otherwise `None`. The filename is used by python for the tracebacks |
| 72 | if no loader extension is used. |
| 73 | |
| 74 | The last item in the tuple is the `uptodate` function. If auto |
| 75 | reloading is enabled it's always called to check if the template |
| 76 | changed. No arguments are passed so the function must store the |
| 77 | old state somewhere (for example in a closure). If it returns `False` |
| 78 | the template will be reloaded. |
| 79 | """ |
| 80 | raise TemplateNotFound(template) |
Armin Ronacher | bcb7c53 | 2008-04-11 16:30:34 +0200 | [diff] [blame] | 81 | |
Armin Ronacher | d416a97 | 2009-02-24 22:58:00 +0100 | [diff] [blame] | 82 | @internalcode |
Armin Ronacher | ba3757b | 2008-04-16 19:43:16 +0200 | [diff] [blame] | 83 | def load(self, environment, name, globals=None): |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 84 | """Loads a template. This method looks up the template in the cache |
| 85 | or loads one by calling :meth:`get_source`. Subclasses should not |
| 86 | override this method as loaders working on collections of other |
| 87 | loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`) |
| 88 | will not call this method but `get_source` directly. |
Armin Ronacher | 814f6c2 | 2008-04-17 15:52:23 +0200 | [diff] [blame] | 89 | """ |
Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 90 | code = None |
Armin Ronacher | 814f6c2 | 2008-04-17 15:52:23 +0200 | [diff] [blame] | 91 | if globals is None: |
| 92 | globals = {} |
Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 93 | |
| 94 | # first we try to get the source for this template together |
| 95 | # with the filename and the uptodate function. |
Armin Ronacher | 814f6c2 | 2008-04-17 15:52:23 +0200 | [diff] [blame] | 96 | source, filename, uptodate = self.get_source(environment, name) |
Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 97 | |
Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 98 | # try to load the code from the bytecode cache if there is a |
| 99 | # bytecode cache configured. |
Armin Ronacher | aa1d17d | 2008-09-18 18:09:06 +0200 | [diff] [blame] | 100 | bcc = environment.bytecode_cache |
| 101 | if bcc is not None: |
Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 102 | bucket = bcc.get_bucket(environment, name, filename, source) |
Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 103 | code = bucket.code |
| 104 | |
Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 105 | # if we don't have code so far (not cached, no longer up to |
| 106 | # date) etc. we compile the template |
Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 107 | if code is None: |
| 108 | code = environment.compile(source, name, filename) |
| 109 | |
Armin Ronacher | a816bf4 | 2008-09-17 21:28:01 +0200 | [diff] [blame] | 110 | # if the bytecode cache is available and the bucket doesn't |
| 111 | # have a code so far, we give the bucket the new code and put |
| 112 | # it back to the bytecode cache. |
Armin Ronacher | aa1d17d | 2008-09-18 18:09:06 +0200 | [diff] [blame] | 113 | if bcc is not None and bucket.code is None: |
Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 114 | bucket.code = code |
Armin Ronacher | aa1d17d | 2008-09-18 18:09:06 +0200 | [diff] [blame] | 115 | bcc.set_bucket(bucket) |
Armin Ronacher | 4d5bdff | 2008-09-17 16:19:46 +0200 | [diff] [blame] | 116 | |
Armin Ronacher | 7259c76 | 2008-04-30 13:03:59 +0200 | [diff] [blame] | 117 | return environment.template_class.from_code(environment, code, |
| 118 | globals, uptodate) |
Armin Ronacher | bcb7c53 | 2008-04-11 16:30:34 +0200 | [diff] [blame] | 119 | |
| 120 | |
| 121 | class FileSystemLoader(BaseLoader): |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 122 | """Loads templates from the file system. This loader can find templates |
| 123 | in folders on the file system and is the preferred way to load them. |
| 124 | |
| 125 | The loader takes the path to the templates as string, or if multiple |
| 126 | locations are wanted a list of them which is then looked up in the |
| 127 | given order: |
| 128 | |
| 129 | >>> loader = FileSystemLoader('/path/to/templates') |
| 130 | >>> loader = FileSystemLoader(['/path/to/templates', '/other/path']) |
| 131 | |
| 132 | Per default the template encoding is ``'utf-8'`` which can be changed |
| 133 | by setting the `encoding` parameter to something else. |
| 134 | """ |
Armin Ronacher | bcb7c53 | 2008-04-11 16:30:34 +0200 | [diff] [blame] | 135 | |
Armin Ronacher | 7259c76 | 2008-04-30 13:03:59 +0200 | [diff] [blame] | 136 | def __init__(self, searchpath, encoding='utf-8'): |
Armin Ronacher | 814f6c2 | 2008-04-17 15:52:23 +0200 | [diff] [blame] | 137 | if isinstance(searchpath, basestring): |
| 138 | searchpath = [searchpath] |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 139 | self.searchpath = list(searchpath) |
Armin Ronacher | bcb7c53 | 2008-04-11 16:30:34 +0200 | [diff] [blame] | 140 | self.encoding = encoding |
| 141 | |
| 142 | def get_source(self, environment, template): |
Armin Ronacher | 9a82205 | 2008-04-17 18:44:07 +0200 | [diff] [blame] | 143 | pieces = split_template_path(template) |
Armin Ronacher | 814f6c2 | 2008-04-17 15:52:23 +0200 | [diff] [blame] | 144 | for searchpath in self.searchpath: |
| 145 | filename = path.join(searchpath, *pieces) |
Armin Ronacher | ccae055 | 2008-10-05 23:08:58 +0200 | [diff] [blame] | 146 | f = open_if_exists(filename) |
| 147 | if f is None: |
Armin Ronacher | 9a82205 | 2008-04-17 18:44:07 +0200 | [diff] [blame] | 148 | continue |
Armin Ronacher | 9a82205 | 2008-04-17 18:44:07 +0200 | [diff] [blame] | 149 | try: |
| 150 | contents = f.read().decode(self.encoding) |
| 151 | finally: |
| 152 | f.close() |
Armin Ronacher | d34eb12 | 2008-10-13 23:47:51 +0200 | [diff] [blame] | 153 | |
| 154 | mtime = path.getmtime(filename) |
| 155 | def uptodate(): |
| 156 | try: |
| 157 | return path.getmtime(filename) == mtime |
| 158 | except OSError: |
| 159 | return False |
| 160 | return contents, filename, uptodate |
Armin Ronacher | 814f6c2 | 2008-04-17 15:52:23 +0200 | [diff] [blame] | 161 | raise TemplateNotFound(template) |
Armin Ronacher | 41ef36f | 2008-04-11 19:55:08 +0200 | [diff] [blame] | 162 | |
| 163 | |
Armin Ronacher | 9a82205 | 2008-04-17 18:44:07 +0200 | [diff] [blame] | 164 | class PackageLoader(BaseLoader): |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 165 | """Load templates from python eggs or packages. It is constructed with |
| 166 | the name of the python package and the path to the templates in that |
Armin Ronacher | 8e64adf | 2010-02-07 02:00:11 +0100 | [diff] [blame^] | 167 | package:: |
Armin Ronacher | 9a82205 | 2008-04-17 18:44:07 +0200 | [diff] [blame] | 168 | |
Armin Ronacher | 8e64adf | 2010-02-07 02:00:11 +0100 | [diff] [blame^] | 169 | loader = PackageLoader('mypackage', 'views') |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 170 | |
| 171 | If the package path is not given, ``'templates'`` is assumed. |
| 172 | |
| 173 | Per default the template encoding is ``'utf-8'`` which can be changed |
| 174 | by setting the `encoding` parameter to something else. Due to the nature |
| 175 | of eggs it's only possible to reload templates if the package was loaded |
| 176 | from the file system and not a zip file. |
| 177 | """ |
| 178 | |
| 179 | def __init__(self, package_name, package_path='templates', |
Armin Ronacher | 7259c76 | 2008-04-30 13:03:59 +0200 | [diff] [blame] | 180 | encoding='utf-8'): |
Armin Ronacher | ccae055 | 2008-10-05 23:08:58 +0200 | [diff] [blame] | 181 | from pkg_resources import DefaultProvider, ResourceManager, \ |
| 182 | get_provider |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 183 | provider = get_provider(package_name) |
| 184 | self.encoding = encoding |
| 185 | self.manager = ResourceManager() |
| 186 | self.filesystem_bound = isinstance(provider, DefaultProvider) |
| 187 | self.provider = provider |
Armin Ronacher | 9a82205 | 2008-04-17 18:44:07 +0200 | [diff] [blame] | 188 | self.package_path = package_path |
| 189 | |
| 190 | def get_source(self, environment, template): |
Armin Ronacher | 963f97d | 2008-04-25 11:44:59 +0200 | [diff] [blame] | 191 | pieces = split_template_path(template) |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 192 | p = '/'.join((self.package_path,) + tuple(pieces)) |
| 193 | if not self.provider.has_resource(p): |
Armin Ronacher | 9a82205 | 2008-04-17 18:44:07 +0200 | [diff] [blame] | 194 | raise TemplateNotFound(template) |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 195 | |
| 196 | filename = uptodate = None |
| 197 | if self.filesystem_bound: |
| 198 | filename = self.provider.get_resource_filename(self.manager, p) |
| 199 | mtime = path.getmtime(filename) |
| 200 | def uptodate(): |
Armin Ronacher | d34eb12 | 2008-10-13 23:47:51 +0200 | [diff] [blame] | 201 | try: |
| 202 | return path.getmtime(filename) == mtime |
| 203 | except OSError: |
| 204 | return False |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 205 | |
| 206 | source = self.provider.get_resource_string(self.manager, p) |
| 207 | return source.decode(self.encoding), filename, uptodate |
Armin Ronacher | 9a82205 | 2008-04-17 18:44:07 +0200 | [diff] [blame] | 208 | |
| 209 | |
Armin Ronacher | 41ef36f | 2008-04-11 19:55:08 +0200 | [diff] [blame] | 210 | class DictLoader(BaseLoader): |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 211 | """Loads a template from a python dict. It's passed a dict of unicode |
| 212 | strings bound to template names. This loader is useful for unittesting: |
Armin Ronacher | 41ef36f | 2008-04-11 19:55:08 +0200 | [diff] [blame] | 213 | |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 214 | >>> loader = DictLoader({'index.html': 'source here'}) |
| 215 | |
| 216 | Because auto reloading is rarely useful this is disabled per default. |
| 217 | """ |
| 218 | |
Armin Ronacher | 7259c76 | 2008-04-30 13:03:59 +0200 | [diff] [blame] | 219 | def __init__(self, mapping): |
Armin Ronacher | 41ef36f | 2008-04-11 19:55:08 +0200 | [diff] [blame] | 220 | self.mapping = mapping |
| 221 | |
| 222 | def get_source(self, environment, template): |
| 223 | if template in self.mapping: |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 224 | source = self.mapping[template] |
Armin Ronacher | 2e46a5c | 2008-09-17 22:25:04 +0200 | [diff] [blame] | 225 | return source, None, lambda: source != self.mapping.get(template) |
Armin Ronacher | 41ef36f | 2008-04-11 19:55:08 +0200 | [diff] [blame] | 226 | raise TemplateNotFound(template) |
Armin Ronacher | 203bfcb | 2008-04-24 21:54:44 +0200 | [diff] [blame] | 227 | |
| 228 | |
| 229 | class FunctionLoader(BaseLoader): |
| 230 | """A loader that is passed a function which does the loading. The |
Armin Ronacher | 963f97d | 2008-04-25 11:44:59 +0200 | [diff] [blame] | 231 | function becomes the name of the template passed and has to return either |
| 232 | an unicode string with the template source, a tuple in the form ``(source, |
| 233 | filename, uptodatefunc)`` or `None` if the template does not exist. |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 234 | |
| 235 | >>> def load_template(name): |
Armin Ronacher | 8e64adf | 2010-02-07 02:00:11 +0100 | [diff] [blame^] | 236 | ... if name == 'index.html': |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 237 | ... return '...' |
| 238 | ... |
| 239 | >>> loader = FunctionLoader(load_template) |
| 240 | |
| 241 | The `uptodatefunc` is a function that is called if autoreload is enabled |
| 242 | and has to return `True` if the template is still up to date. For more |
| 243 | details have a look at :meth:`BaseLoader.get_source` which has the same |
| 244 | return value. |
Armin Ronacher | 203bfcb | 2008-04-24 21:54:44 +0200 | [diff] [blame] | 245 | """ |
| 246 | |
Armin Ronacher | 7259c76 | 2008-04-30 13:03:59 +0200 | [diff] [blame] | 247 | def __init__(self, load_func): |
Armin Ronacher | 203bfcb | 2008-04-24 21:54:44 +0200 | [diff] [blame] | 248 | self.load_func = load_func |
| 249 | |
| 250 | def get_source(self, environment, template): |
Armin Ronacher | 963f97d | 2008-04-25 11:44:59 +0200 | [diff] [blame] | 251 | rv = self.load_func(template) |
Armin Ronacher | 203bfcb | 2008-04-24 21:54:44 +0200 | [diff] [blame] | 252 | if rv is None: |
| 253 | raise TemplateNotFound(template) |
Armin Ronacher | 963f97d | 2008-04-25 11:44:59 +0200 | [diff] [blame] | 254 | elif isinstance(rv, basestring): |
| 255 | return rv, None, None |
Armin Ronacher | 203bfcb | 2008-04-24 21:54:44 +0200 | [diff] [blame] | 256 | return rv |
| 257 | |
| 258 | |
| 259 | class PrefixLoader(BaseLoader): |
| 260 | """A loader that is passed a dict of loaders where each loader is bound |
Armin Ronacher | 7259c76 | 2008-04-30 13:03:59 +0200 | [diff] [blame] | 261 | to a prefix. The prefix is delimited from the template by a slash per |
| 262 | default, which can be changed by setting the `delimiter` argument to |
Armin Ronacher | 8e64adf | 2010-02-07 02:00:11 +0100 | [diff] [blame^] | 263 | something else:: |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 264 | |
Armin Ronacher | 8e64adf | 2010-02-07 02:00:11 +0100 | [diff] [blame^] | 265 | loader = PrefixLoader({ |
| 266 | 'app1': PackageLoader('mypackage.app1'), |
| 267 | 'app2': PackageLoader('mypackage.app2') |
| 268 | }) |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 269 | |
| 270 | By loading ``'app1/index.html'`` the file from the app1 package is loaded, |
| 271 | by loading ``'app2/index.html'`` the file from the second. |
Armin Ronacher | 203bfcb | 2008-04-24 21:54:44 +0200 | [diff] [blame] | 272 | """ |
| 273 | |
Armin Ronacher | 7259c76 | 2008-04-30 13:03:59 +0200 | [diff] [blame] | 274 | def __init__(self, mapping, delimiter='/'): |
Armin Ronacher | 203bfcb | 2008-04-24 21:54:44 +0200 | [diff] [blame] | 275 | self.mapping = mapping |
| 276 | self.delimiter = delimiter |
| 277 | |
| 278 | def get_source(self, environment, template): |
| 279 | try: |
| 280 | prefix, template = template.split(self.delimiter, 1) |
| 281 | loader = self.mapping[prefix] |
| 282 | except (ValueError, KeyError): |
| 283 | raise TemplateNotFound(template) |
| 284 | return loader.get_source(environment, template) |
| 285 | |
| 286 | |
| 287 | class ChoiceLoader(BaseLoader): |
| 288 | """This loader works like the `PrefixLoader` just that no prefix is |
| 289 | specified. If a template could not be found by one loader the next one |
Armin Ronacher | 7259c76 | 2008-04-30 13:03:59 +0200 | [diff] [blame] | 290 | is tried. |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 291 | |
| 292 | >>> loader = ChoiceLoader([ |
| 293 | ... FileSystemLoader('/path/to/user/templates'), |
Armin Ronacher | 8e64adf | 2010-02-07 02:00:11 +0100 | [diff] [blame^] | 294 | ... FileSystemLoader('/path/to/system/templates') |
Armin Ronacher | 61a5a24 | 2008-05-26 12:07:44 +0200 | [diff] [blame] | 295 | ... ]) |
Armin Ronacher | d134231 | 2008-04-28 12:20:12 +0200 | [diff] [blame] | 296 | |
| 297 | This is useful if you want to allow users to override builtin templates |
| 298 | from a different location. |
Armin Ronacher | 203bfcb | 2008-04-24 21:54:44 +0200 | [diff] [blame] | 299 | """ |
| 300 | |
Armin Ronacher | 7259c76 | 2008-04-30 13:03:59 +0200 | [diff] [blame] | 301 | def __init__(self, loaders): |
Armin Ronacher | 203bfcb | 2008-04-24 21:54:44 +0200 | [diff] [blame] | 302 | self.loaders = loaders |
| 303 | |
| 304 | def get_source(self, environment, template): |
| 305 | for loader in self.loaders: |
| 306 | try: |
| 307 | return loader.get_source(environment, template) |
| 308 | except TemplateNotFound: |
| 309 | pass |
| 310 | raise TemplateNotFound(template) |