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