1# mako/lookup.py
2# Copyright (C) 2006-2015 the Mako authors and contributors <see AUTHORS file>
3#
4# This module is part of Mako and is released under
5# the MIT License: http://www.opensource.org/licenses/mit-license.php
6
7import os, stat, posixpath, re
8from mako import exceptions, util
9from mako.template import Template
10
11try:
12    import threading
13except:
14    import dummy_threading as threading
15
16class TemplateCollection(object):
17    """Represent a collection of :class:`.Template` objects,
18    identifiable via URI.
19
20    A :class:`.TemplateCollection` is linked to the usage of
21    all template tags that address other templates, such
22    as ``<%include>``, ``<%namespace>``, and ``<%inherit>``.
23    The ``file`` attribute of each of those tags refers
24    to a string URI that is passed to that :class:`.Template`
25    object's :class:`.TemplateCollection` for resolution.
26
27    :class:`.TemplateCollection` is an abstract class,
28    with the usual default implementation being :class:`.TemplateLookup`.
29
30     """
31
32    def has_template(self, uri):
33        """Return ``True`` if this :class:`.TemplateLookup` is
34        capable of returning a :class:`.Template` object for the
35        given ``uri``.
36
37        :param uri: String URI of the template to be resolved.
38
39        """
40        try:
41            self.get_template(uri)
42            return True
43        except exceptions.TemplateLookupException:
44            return False
45
46    def get_template(self, uri, relativeto=None):
47        """Return a :class:`.Template` object corresponding to the given
48        ``uri``.
49
50        The default implementation raises
51        :class:`.NotImplementedError`. Implementations should
52        raise :class:`.TemplateLookupException` if the given ``uri``
53        cannot be resolved.
54
55        :param uri: String URI of the template to be resolved.
56        :param relativeto: if present, the given ``uri`` is assumed to
57         be relative to this URI.
58
59        """
60        raise NotImplementedError()
61
62    def filename_to_uri(self, uri, filename):
63        """Convert the given ``filename`` to a URI relative to
64           this :class:`.TemplateCollection`."""
65
66        return uri
67
68    def adjust_uri(self, uri, filename):
69        """Adjust the given ``uri`` based on the calling ``filename``.
70
71        When this method is called from the runtime, the
72        ``filename`` parameter is taken directly to the ``filename``
73        attribute of the calling template. Therefore a custom
74        :class:`.TemplateCollection` subclass can place any string
75        identifier desired in the ``filename`` parameter of the
76        :class:`.Template` objects it constructs and have them come back
77        here.
78
79        """
80        return uri
81
82class TemplateLookup(TemplateCollection):
83    """Represent a collection of templates that locates template source files
84    from the local filesystem.
85
86    The primary argument is the ``directories`` argument, the list of
87    directories to search:
88
89    .. sourcecode:: python
90
91        lookup = TemplateLookup(["/path/to/templates"])
92        some_template = lookup.get_template("/index.html")
93
94    The :class:`.TemplateLookup` can also be given :class:`.Template` objects
95    programatically using :meth:`.put_string` or :meth:`.put_template`:
96
97    .. sourcecode:: python
98
99        lookup = TemplateLookup()
100        lookup.put_string("base.html", '''
101            <html><body>${self.next()}</body></html>
102        ''')
103        lookup.put_string("hello.html", '''
104            <%include file='base.html'/>
105
106            Hello, world !
107        ''')
108
109
110    :param directories: A list of directory names which will be
111     searched for a particular template URI. The URI is appended
112     to each directory and the filesystem checked.
113
114    :param collection_size: Approximate size of the collection used
115     to store templates. If left at its default of ``-1``, the size
116     is unbounded, and a plain Python dictionary is used to
117     relate URI strings to :class:`.Template` instances.
118     Otherwise, a least-recently-used cache object is used which
119     will maintain the size of the collection approximately to
120     the number given.
121
122    :param filesystem_checks: When at its default value of ``True``,
123     each call to :meth:`.TemplateLookup.get_template()` will
124     compare the filesystem last modified time to the time in
125     which an existing :class:`.Template` object was created.
126     This allows the :class:`.TemplateLookup` to regenerate a
127     new :class:`.Template` whenever the original source has
128     been updated. Set this to ``False`` for a very minor
129     performance increase.
130
131    :param modulename_callable: A callable which, when present,
132     is passed the path of the source file as well as the
133     requested URI, and then returns the full path of the
134     generated Python module file. This is used to inject
135     alternate schemes for Python module location. If left at
136     its default of ``None``, the built in system of generation
137     based on ``module_directory`` plus ``uri`` is used.
138
139    All other keyword parameters available for
140    :class:`.Template` are mirrored here. When new
141    :class:`.Template` objects are created, the keywords
142    established with this :class:`.TemplateLookup` are passed on
143    to each new :class:`.Template`.
144
145    """
146
147    def __init__(self,
148                        directories=None,
149                        module_directory=None,
150                        filesystem_checks=True,
151                        collection_size=-1,
152                        format_exceptions=False,
153                        error_handler=None,
154                        disable_unicode=False,
155                        bytestring_passthrough=False,
156                        output_encoding=None,
157                        encoding_errors='strict',
158
159                        cache_args=None,
160                        cache_impl='beaker',
161                        cache_enabled=True,
162                        cache_type=None,
163                        cache_dir=None,
164                        cache_url=None,
165
166                        modulename_callable=None,
167                        module_writer=None,
168                        default_filters=None,
169                        buffer_filters=(),
170                        strict_undefined=False,
171                        imports=None,
172                        future_imports=None,
173                        enable_loop=True,
174                        input_encoding=None,
175                        preprocessor=None,
176                        lexer_cls=None):
177
178        self.directories = [posixpath.normpath(d) for d in
179                            util.to_list(directories, ())
180                            ]
181        self.module_directory = module_directory
182        self.modulename_callable = modulename_callable
183        self.filesystem_checks = filesystem_checks
184        self.collection_size = collection_size
185
186        if cache_args is None:
187            cache_args = {}
188        # transfer deprecated cache_* args
189        if cache_dir:
190            cache_args.setdefault('dir', cache_dir)
191        if cache_url:
192            cache_args.setdefault('url', cache_url)
193        if cache_type:
194            cache_args.setdefault('type', cache_type)
195
196        self.template_args = {
197            'format_exceptions':format_exceptions,
198            'error_handler':error_handler,
199            'disable_unicode':disable_unicode,
200            'bytestring_passthrough':bytestring_passthrough,
201            'output_encoding':output_encoding,
202            'cache_impl':cache_impl,
203            'encoding_errors':encoding_errors,
204            'input_encoding':input_encoding,
205            'module_directory':module_directory,
206            'module_writer':module_writer,
207            'cache_args':cache_args,
208            'cache_enabled':cache_enabled,
209            'default_filters':default_filters,
210            'buffer_filters':buffer_filters,
211            'strict_undefined':strict_undefined,
212            'imports':imports,
213            'future_imports':future_imports,
214            'enable_loop':enable_loop,
215            'preprocessor':preprocessor,
216            'lexer_cls':lexer_cls
217        }
218
219        if collection_size == -1:
220            self._collection = {}
221            self._uri_cache = {}
222        else:
223            self._collection = util.LRUCache(collection_size)
224            self._uri_cache = util.LRUCache(collection_size)
225        self._mutex = threading.Lock()
226
227    def get_template(self, uri):
228        """Return a :class:`.Template` object corresponding to the given
229        ``uri``.
230
231        .. note:: The ``relativeto`` argument is not supported here at the moment.
232
233        """
234
235        try:
236            if self.filesystem_checks:
237                return self._check(uri, self._collection[uri])
238            else:
239                return self._collection[uri]
240        except KeyError:
241            u = re.sub(r'^\/+', '', uri)
242            for dir in self.directories:
243                srcfile = posixpath.normpath(posixpath.join(dir, u))
244                if os.path.isfile(srcfile):
245                    return self._load(srcfile, uri)
246            else:
247                raise exceptions.TopLevelLookupException(
248                                    "Cant locate template for uri %r" % uri)
249
250    def adjust_uri(self, uri, relativeto):
251        """Adjust the given ``uri`` based on the given relative URI."""
252
253        key = (uri, relativeto)
254        if key in self._uri_cache:
255            return self._uri_cache[key]
256
257        if uri[0] != '/':
258            if relativeto is not None:
259                v = self._uri_cache[key] = posixpath.join(
260                                            posixpath.dirname(relativeto), uri)
261            else:
262                v = self._uri_cache[key] = '/' + uri
263        else:
264            v = self._uri_cache[key] = uri
265        return v
266
267
268    def filename_to_uri(self, filename):
269        """Convert the given ``filename`` to a URI relative to
270           this :class:`.TemplateCollection`."""
271
272        try:
273            return self._uri_cache[filename]
274        except KeyError:
275            value = self._relativeize(filename)
276            self._uri_cache[filename] = value
277            return value
278
279    def _relativeize(self, filename):
280        """Return the portion of a filename that is 'relative'
281           to the directories in this lookup.
282
283        """
284
285        filename = posixpath.normpath(filename)
286        for dir in self.directories:
287            if filename[0:len(dir)] == dir:
288                return filename[len(dir):]
289        else:
290            return None
291
292    def _load(self, filename, uri):
293        self._mutex.acquire()
294        try:
295            try:
296                # try returning from collection one
297                # more time in case concurrent thread already loaded
298                return self._collection[uri]
299            except KeyError:
300                pass
301            try:
302                if self.modulename_callable is not None:
303                    module_filename = self.modulename_callable(filename, uri)
304                else:
305                    module_filename = None
306                self._collection[uri] = template = Template(
307                                        uri=uri,
308                                        filename=posixpath.normpath(filename),
309                                        lookup=self,
310                                        module_filename=module_filename,
311                                        **self.template_args)
312                return template
313            except:
314                # if compilation fails etc, ensure
315                # template is removed from collection,
316                # re-raise
317                self._collection.pop(uri, None)
318                raise
319        finally:
320            self._mutex.release()
321
322    def _check(self, uri, template):
323        if template.filename is None:
324            return template
325
326        try:
327            template_stat = os.stat(template.filename)
328            if template.module._modified_time < \
329                        template_stat[stat.ST_MTIME]:
330                self._collection.pop(uri, None)
331                return self._load(template.filename, uri)
332            else:
333                return template
334        except OSError:
335            self._collection.pop(uri, None)
336            raise exceptions.TemplateLookupException(
337                                "Cant locate template for uri %r" % uri)
338
339
340    def put_string(self, uri, text):
341        """Place a new :class:`.Template` object into this
342        :class:`.TemplateLookup`, based on the given string of
343        ``text``.
344
345        """
346        self._collection[uri] = Template(
347                                    text,
348                                    lookup=self,
349                                    uri=uri,
350                                    **self.template_args)
351
352    def put_template(self, uri, template):
353        """Place a new :class:`.Template` object into this
354        :class:`.TemplateLookup`, based on the given
355        :class:`.Template` object.
356
357        """
358        self._collection[uri] = template
359
360