|
|
|
# mako/lookup.py
|
|
|
|
# Copyright 2006-2024 the Mako authors and contributors <see AUTHORS file>
|
|
|
|
#
|
|
|
|
# This module is part of Mako and is released under
|
|
|
|
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
|
|
|
|
|
|
|
import os
|
|
|
|
import posixpath
|
|
|
|
import re
|
|
|
|
import stat
|
|
|
|
import threading
|
|
|
|
|
|
|
|
from mako import exceptions
|
|
|
|
from mako import util
|
|
|
|
from mako.template import Template
|
|
|
|
|
|
|
|
|
|
|
|
class TemplateCollection:
|
|
|
|
|
|
|
|
"""Represent a collection of :class:`.Template` objects,
|
|
|
|
identifiable via URI.
|
|
|
|
|
|
|
|
A :class:`.TemplateCollection` is linked to the usage of
|
|
|
|
all template tags that address other templates, such
|
|
|
|
as ``<%include>``, ``<%namespace>``, and ``<%inherit>``.
|
|
|
|
The ``file`` attribute of each of those tags refers
|
|
|
|
to a string URI that is passed to that :class:`.Template`
|
|
|
|
object's :class:`.TemplateCollection` for resolution.
|
|
|
|
|
|
|
|
:class:`.TemplateCollection` is an abstract class,
|
|
|
|
with the usual default implementation being :class:`.TemplateLookup`.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
def has_template(self, uri):
|
|
|
|
"""Return ``True`` if this :class:`.TemplateLookup` is
|
|
|
|
capable of returning a :class:`.Template` object for the
|
|
|
|
given ``uri``.
|
|
|
|
|
|
|
|
:param uri: String URI of the template to be resolved.
|
|
|
|
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
self.get_template(uri)
|
|
|
|
return True
|
|
|
|
except exceptions.TemplateLookupException:
|
|
|
|
return False
|
|
|
|
|
|
|
|
def get_template(self, uri, relativeto=None):
|
|
|
|
"""Return a :class:`.Template` object corresponding to the given
|
|
|
|
``uri``.
|
|
|
|
|
|
|
|
The default implementation raises
|
|
|
|
:class:`.NotImplementedError`. Implementations should
|
|
|
|
raise :class:`.TemplateLookupException` if the given ``uri``
|
|
|
|
cannot be resolved.
|
|
|
|
|
|
|
|
:param uri: String URI of the template to be resolved.
|
|
|
|
:param relativeto: if present, the given ``uri`` is assumed to
|
|
|
|
be relative to this URI.
|
|
|
|
|
|
|
|
"""
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
def filename_to_uri(self, uri, filename):
|
|
|
|
"""Convert the given ``filename`` to a URI relative to
|
|
|
|
this :class:`.TemplateCollection`."""
|
|
|
|
|
|
|
|
return uri
|
|
|
|
|
|
|
|
def adjust_uri(self, uri, filename):
|
|
|
|
"""Adjust the given ``uri`` based on the calling ``filename``.
|
|
|
|
|
|
|
|
When this method is called from the runtime, the
|
|
|
|
``filename`` parameter is taken directly to the ``filename``
|
|
|
|
attribute of the calling template. Therefore a custom
|
|
|
|
:class:`.TemplateCollection` subclass can place any string
|
|
|
|
identifier desired in the ``filename`` parameter of the
|
|
|
|
:class:`.Template` objects it constructs and have them come back
|
|
|
|
here.
|
|
|
|
|
|
|
|
"""
|
|
|
|
return uri
|
|
|
|
|
|
|
|
|
|
|
|
class TemplateLookup(TemplateCollection):
|
|
|
|
|
|
|
|
"""Represent a collection of templates that locates template source files
|
|
|
|
from the local filesystem.
|
|
|
|
|
|
|
|
The primary argument is the ``directories`` argument, the list of
|
|
|
|
directories to search:
|
|
|
|
|
|
|
|
.. sourcecode:: python
|
|
|
|
|
|
|
|
lookup = TemplateLookup(["/path/to/templates"])
|
|
|
|
some_template = lookup.get_template("/index.html")
|
|
|
|
|
|
|
|
The :class:`.TemplateLookup` can also be given :class:`.Template` objects
|
|
|
|
programatically using :meth:`.put_string` or :meth:`.put_template`:
|
|
|
|
|
|
|
|
.. sourcecode:: python
|
|
|
|
|
|
|
|
lookup = TemplateLookup()
|
|
|
|
lookup.put_string("base.html", '''
|
|
|
|
<html><body>${self.next()}</body></html>
|
|
|
|
''')
|
|
|
|
lookup.put_string("hello.html", '''
|
|
|
|
<%include file='base.html'/>
|
|
|
|
|
|
|
|
Hello, world !
|
|
|
|
''')
|
|
|
|
|
|
|
|
|
|
|
|
:param directories: A list of directory names which will be
|
|
|
|
searched for a particular template URI. The URI is appended
|
|
|
|
to each directory and the filesystem checked.
|
|
|
|
|
|
|
|
:param collection_size: Approximate size of the collection used
|
|
|
|
to store templates. If left at its default of ``-1``, the size
|
|
|
|
is unbounded, and a plain Python dictionary is used to
|
|
|
|
relate URI strings to :class:`.Template` instances.
|
|
|
|
Otherwise, a least-recently-used cache object is used which
|
|
|
|
will maintain the size of the collection approximately to
|
|
|
|
the number given.
|
|
|
|
|
|
|
|
:param filesystem_checks: When at its default value of ``True``,
|
|
|
|
each call to :meth:`.TemplateLookup.get_template()` will
|
|
|
|
compare the filesystem last modified time to the time in
|
|
|
|
which an existing :class:`.Template` object was created.
|
|
|
|
This allows the :class:`.TemplateLookup` to regenerate a
|
|
|
|
new :class:`.Template` whenever the original source has
|
|
|
|
been updated. Set this to ``False`` for a very minor
|
|
|
|
performance increase.
|
|
|
|
|
|
|
|
:param modulename_callable: A callable which, when present,
|
|
|
|
is passed the path of the source file as well as the
|
|
|
|
requested URI, and then returns the full path of the
|
|
|
|
generated Python module file. This is used to inject
|
|
|
|
alternate schemes for Python module location. If left at
|
|
|
|
its default of ``None``, the built in system of generation
|
|
|
|
based on ``module_directory`` plus ``uri`` is used.
|
|
|
|
|
|
|
|
All other keyword parameters available for
|
|
|
|
:class:`.Template` are mirrored here. When new
|
|
|
|
:class:`.Template` objects are created, the keywords
|
|
|
|
established with this :class:`.TemplateLookup` are passed on
|
|
|
|
to each new :class:`.Template`.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
directories=None,
|
|
|
|
module_directory=None,
|
|
|
|
filesystem_checks=True,
|
|
|
|
collection_size=-1,
|
|
|
|
format_exceptions=False,
|
|
|
|
error_handler=None,
|
|
|
|
output_encoding=None,
|
|
|
|
encoding_errors="strict",
|
|
|
|
cache_args=None,
|
|
|
|
cache_impl="beaker",
|
|
|
|
cache_enabled=True,
|
|
|
|
cache_type=None,
|
|
|
|
cache_dir=None,
|
|
|
|
cache_url=None,
|
|
|
|
modulename_callable=None,
|
|
|
|
module_writer=None,
|
|
|
|
default_filters=None,
|
|
|
|
buffer_filters=(),
|
|
|
|
strict_undefined=False,
|
|
|
|
imports=None,
|
|
|
|
future_imports=None,
|
|
|
|
enable_loop=True,
|
|
|
|
input_encoding=None,
|
|
|
|
preprocessor=None,
|
|
|
|
lexer_cls=None,
|
|
|
|
include_error_handler=None,
|
|
|
|
):
|
|
|
|
self.directories = [
|
|
|
|
posixpath.normpath(d) for d in util.to_list(directories, ())
|
|
|
|
]
|
|
|
|
self.module_directory = module_directory
|
|
|
|
self.modulename_callable = modulename_callable
|
|
|
|
self.filesystem_checks = filesystem_checks
|
|
|
|
self.collection_size = collection_size
|
|
|
|
|
|
|
|
if cache_args is None:
|
|
|
|
cache_args = {}
|
|
|
|
# transfer deprecated cache_* args
|
|
|
|
if cache_dir:
|
|
|
|
cache_args.setdefault("dir", cache_dir)
|
|
|
|
if cache_url:
|
|
|
|
cache_args.setdefault("url", cache_url)
|
|
|
|
if cache_type:
|
|
|
|
cache_args.setdefault("type", cache_type)
|
|
|
|
|
|
|
|
self.template_args = {
|
|
|
|
"format_exceptions": format_exceptions,
|
|
|
|
"error_handler": error_handler,
|
|
|
|
"include_error_handler": include_error_handler,
|
|
|
|
"output_encoding": output_encoding,
|
|
|
|
"cache_impl": cache_impl,
|
|
|
|
"encoding_errors": encoding_errors,
|
|
|
|
"input_encoding": input_encoding,
|
|
|
|
"module_directory": module_directory,
|
|
|
|
"module_writer": module_writer,
|
|
|
|
"cache_args": cache_args,
|
|
|
|
"cache_enabled": cache_enabled,
|
|
|
|
"default_filters": default_filters,
|
|
|
|
"buffer_filters": buffer_filters,
|
|
|
|
"strict_undefined": strict_undefined,
|
|
|
|
"imports": imports,
|
|
|
|
"future_imports": future_imports,
|
|
|
|
"enable_loop": enable_loop,
|
|
|
|
"preprocessor": preprocessor,
|
|
|
|
"lexer_cls": lexer_cls,
|
|
|
|
}
|
|
|
|
|
|
|
|
if collection_size == -1:
|
|
|
|
self._collection = {}
|
|
|
|
self._uri_cache = {}
|
|
|
|
else:
|
|
|
|
self._collection = util.LRUCache(collection_size)
|
|
|
|
self._uri_cache = util.LRUCache(collection_size)
|
|
|
|
self._mutex = threading.Lock()
|
|
|
|
|
|
|
|
def get_template(self, uri):
|
|
|
|
"""Return a :class:`.Template` object corresponding to the given
|
|
|
|
``uri``.
|
|
|
|
|
|
|
|
.. note:: The ``relativeto`` argument is not supported here at
|
|
|
|
the moment.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
if self.filesystem_checks:
|
|
|
|
return self._check(uri, self._collection[uri])
|
|
|
|
else:
|
|
|
|
return self._collection[uri]
|
|
|
|
except KeyError as e:
|
|
|
|
u = re.sub(r"^\/+", "", uri)
|
|
|
|
for dir_ in self.directories:
|
|
|
|
# make sure the path seperators are posix - os.altsep is empty
|
|
|
|
# on POSIX and cannot be used.
|
|
|
|
dir_ = dir_.replace(os.path.sep, posixpath.sep)
|
|
|
|
srcfile = posixpath.normpath(posixpath.join(dir_, u))
|
|
|
|
if os.path.isfile(srcfile):
|
|
|
|
return self._load(srcfile, uri)
|
|
|
|
else:
|
|
|
|
raise exceptions.TopLevelLookupException(
|
|
|
|
"Can't locate template for uri %r" % uri
|
|
|
|
) from e
|
|
|
|
|
|
|
|
def adjust_uri(self, uri, relativeto):
|
|
|
|
"""Adjust the given ``uri`` based on the given relative URI."""
|
|
|
|
|
|
|
|
key = (uri, relativeto)
|
|
|
|
if key in self._uri_cache:
|
|
|
|
return self._uri_cache[key]
|
|
|
|
|
|
|
|
if uri[0] == "/":
|
|
|
|
v = self._uri_cache[key] = uri
|
|
|
|
elif relativeto is not None:
|
|
|
|
v = self._uri_cache[key] = posixpath.join(
|
|
|
|
posixpath.dirname(relativeto), uri
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
v = self._uri_cache[key] = "/" + uri
|
|
|
|
return v
|
|
|
|
|
|
|
|
def filename_to_uri(self, filename):
|
|
|
|
"""Convert the given ``filename`` to a URI relative to
|
|
|
|
this :class:`.TemplateCollection`."""
|
|
|
|
|
|
|
|
try:
|
|
|
|
return self._uri_cache[filename]
|
|
|
|
except KeyError:
|
|
|
|
value = self._relativeize(filename)
|
|
|
|
self._uri_cache[filename] = value
|
|
|
|
return value
|
|
|
|
|
|
|
|
def _relativeize(self, filename):
|
|
|
|
"""Return the portion of a filename that is 'relative'
|
|
|
|
to the directories in this lookup.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
filename = posixpath.normpath(filename)
|
|
|
|
for dir_ in self.directories:
|
|
|
|
if filename[0 : len(dir_)] == dir_:
|
|
|
|
return filename[len(dir_) :]
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def _load(self, filename, uri):
|
|
|
|
self._mutex.acquire()
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
# try returning from collection one
|
|
|
|
# more time in case concurrent thread already loaded
|
|
|
|
return self._collection[uri]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
try:
|
|
|
|
if self.modulename_callable is not None:
|
|
|
|
module_filename = self.modulename_callable(filename, uri)
|
|
|
|
else:
|
|
|
|
module_filename = None
|
|
|
|
self._collection[uri] = template = Template(
|
|
|
|
uri=uri,
|
|
|
|
filename=posixpath.normpath(filename),
|
|
|
|
lookup=self,
|
|
|
|
module_filename=module_filename,
|
|
|
|
**self.template_args,
|
|
|
|
)
|
|
|
|
return template
|
|
|
|
except:
|
|
|
|
# if compilation fails etc, ensure
|
|
|
|
# template is removed from collection,
|
|
|
|
# re-raise
|
|
|
|
self._collection.pop(uri, None)
|
|
|
|
raise
|
|
|
|
finally:
|
|
|
|
self._mutex.release()
|
|
|
|
|
|
|
|
def _check(self, uri, template):
|
|
|
|
if template.filename is None:
|
|
|
|
return template
|
|
|
|
|
|
|
|
try:
|
|
|
|
template_stat = os.stat(template.filename)
|
|
|
|
if template.module._modified_time >= template_stat[stat.ST_MTIME]:
|
|
|
|
return template
|
|
|
|
self._collection.pop(uri, None)
|
|
|
|
return self._load(template.filename, uri)
|
|
|
|
except OSError as e:
|
|
|
|
self._collection.pop(uri, None)
|
|
|
|
raise exceptions.TemplateLookupException(
|
|
|
|
"Can't locate template for uri %r" % uri
|
|
|
|
) from e
|
|
|
|
|
|
|
|
def put_string(self, uri, text):
|
|
|
|
"""Place a new :class:`.Template` object into this
|
|
|
|
:class:`.TemplateLookup`, based on the given string of
|
|
|
|
``text``.
|
|
|
|
|
|
|
|
"""
|
|
|
|
self._collection[uri] = Template(
|
|
|
|
text, lookup=self, uri=uri, **self.template_args
|
|
|
|
)
|
|
|
|
|
|
|
|
def put_template(self, uri, template):
|
|
|
|
"""Place a new :class:`.Template` object into this
|
|
|
|
:class:`.TemplateLookup`, based on the given
|
|
|
|
:class:`.Template` object.
|
|
|
|
|
|
|
|
"""
|
|
|
|
self._collection[uri] = template
|