1# -*- coding: utf-8 -*- 
    2# 
    3# Copyright (C) 2006-2010 Edgewall Software 
    4# All rights reserved. 
    5# 
    6# This software is licensed as described in the file COPYING, which 
    7# you should have received as part of this distribution. The terms 
    8# are also available at http://genshi.edgewall.org/wiki/License. 
    9# 
    10# This software consists of voluntary contributions made by many 
    11# individuals. For the exact contribution history, see the revision 
    12# history and logs, available at http://genshi.edgewall.org/log/. 
    13 
    14"""Template loading and caching.""" 
    15 
    16import os 
    17try: 
    18    import threading 
    19except ImportError: 
    20    import dummy_threading as threading 
    21 
    22from genshi.compat import string_types 
    23from genshi.template.base import TemplateError 
    24from genshi.util import LRUCache 
    25 
    26__all__ = ['TemplateLoader', 'TemplateNotFound', 'directory', 'package', 
    27           'prefixed'] 
    28__docformat__ = 'restructuredtext en' 
    29 
    30 
    31class TemplateNotFound(TemplateError): 
    32    """Exception raised when a specific template file could not be found.""" 
    33 
    34    def __init__(self, name, search_path): 
    35        """Create the exception. 
    36         
    37        :param name: the filename of the template 
    38        :param search_path: the search path used to lookup the template 
    39        """ 
    40        TemplateError.__init__(self, 'Template "%s" not found' % name) 
    41        self.search_path = search_path 
    42 
    43 
    44class TemplateLoader(object): 
    45    """Responsible for loading templates from files on the specified search 
    46    path. 
    47     
    48    >>> import tempfile 
    49    >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template') 
    50    >>> os.write(fd, u'<p>$var</p>'.encode('utf-8')) 
    51    11 
    52    >>> os.close(fd) 
    53     
    54    The template loader accepts a list of directory paths that are then used 
    55    when searching for template files, in the given order: 
    56     
    57    >>> loader = TemplateLoader([os.path.dirname(path)]) 
    58     
    59    The `load()` method first checks the template cache whether the requested 
    60    template has already been loaded. If not, it attempts to locate the 
    61    template file, and returns the corresponding `Template` object: 
    62     
    63    >>> from genshi.template import MarkupTemplate 
    64    >>> template = loader.load(os.path.basename(path)) 
    65    >>> isinstance(template, MarkupTemplate) 
    66    True 
    67     
    68    Template instances are cached: requesting a template with the same name 
    69    results in the same instance being returned: 
    70     
    71    >>> loader.load(os.path.basename(path)) is template 
    72    True 
    73     
    74    The `auto_reload` option can be used to control whether a template should 
    75    be automatically reloaded when the file it was loaded from has been 
    76    changed. Disable this automatic reloading to improve performance. 
    77     
    78    >>> os.remove(path) 
    79    """ 
    80    def __init__(self, search_path=None, auto_reload=False, 
    81                 default_encoding=None, max_cache_size=25, default_class=None, 
    82                 variable_lookup='strict', allow_exec=True, callback=None): 
    83        """Create the template laoder. 
    84         
    85        :param search_path: a list of absolute path names that should be 
    86                            searched for template files, or a string containing 
    87                            a single absolute path; alternatively, any item on 
    88                            the list may be a ''load function'' that is passed 
    89                            a filename and returns a file-like object and some 
    90                            metadata 
    91        :param auto_reload: whether to check the last modification time of 
    92                            template files, and reload them if they have changed 
    93        :param default_encoding: the default encoding to assume when loading 
    94                                 templates; defaults to UTF-8 
    95        :param max_cache_size: the maximum number of templates to keep in the 
    96                               cache 
    97        :param default_class: the default `Template` subclass to use when 
    98                              instantiating templates 
    99        :param variable_lookup: the variable lookup mechanism; either "strict" 
    100                                (the default), "lenient", or a custom lookup 
    101                                class 
    102        :param allow_exec: whether to allow Python code blocks in templates 
    103        :param callback: (optional) a callback function that is invoked after a 
    104                         template was initialized by this loader; the function 
    105                         is passed the template object as only argument. This 
    106                         callback can be used for example to add any desired 
    107                         filters to the template 
    108        :see: `LenientLookup`, `StrictLookup` 
    109         
    110        :note: Changed in 0.5: Added the `allow_exec` argument 
    111        """ 
    112        from genshi.template.markup import MarkupTemplate 
    113 
    114        self.search_path = search_path 
    115        if self.search_path is None: 
    116            self.search_path = [] 
    117        elif not isinstance(self.search_path, (list, tuple)): 
    118            self.search_path = [self.search_path] 
    119 
    120        self.auto_reload = auto_reload 
    121        """Whether templates should be reloaded when the underlying file is 
    122        changed""" 
    123 
    124        self.default_encoding = default_encoding 
    125        self.default_class = default_class or MarkupTemplate 
    126        self.variable_lookup = variable_lookup 
    127        self.allow_exec = allow_exec 
    128        if callback is not None and not hasattr(callback, '__call__'): 
    129            raise TypeError('The "callback" parameter needs to be callable') 
    130        self.callback = callback 
    131        self._cache = LRUCache(max_cache_size) 
    132        self._uptodate = {} 
    133        self._lock = threading.RLock() 
    134 
    135    def __getstate__(self): 
    136        state = self.__dict__.copy() 
    137        state['_lock'] = None 
    138        return state 
    139 
    140    def __setstate__(self, state): 
    141        self.__dict__ = state 
    142        self._lock = threading.RLock() 
    143 
    144    def load(self, filename, relative_to=None, cls=None, encoding=None): 
    145        """Load the template with the given name. 
    146         
    147        If the `filename` parameter is relative, this method searches the 
    148        search path trying to locate a template matching the given name. If the 
    149        file name is an absolute path, the search path is ignored. 
    150         
    151        If the requested template is not found, a `TemplateNotFound` exception 
    152        is raised. Otherwise, a `Template` object is returned that represents 
    153        the parsed template. 
    154         
    155        Template instances are cached to avoid having to parse the same 
    156        template file more than once. Thus, subsequent calls of this method 
    157        with the same template file name will return the same `Template` 
    158        object (unless the ``auto_reload`` option is enabled and the file was 
    159        changed since the last parse.) 
    160         
    161        If the `relative_to` parameter is provided, the `filename` is 
    162        interpreted as being relative to that path. 
    163         
    164        :param filename: the relative path of the template file to load 
    165        :param relative_to: the filename of the template from which the new 
    166                            template is being loaded, or ``None`` if the 
    167                            template is being loaded directly 
    168        :param cls: the class of the template object to instantiate 
    169        :param encoding: the encoding of the template to load; defaults to the 
    170                         ``default_encoding`` of the loader instance 
    171        :return: the loaded `Template` instance 
    172        :raises TemplateNotFound: if a template with the given name could not 
    173                                  be found 
    174        """ 
    175        if cls is None: 
    176            cls = self.default_class 
    177        search_path = self.search_path 
    178 
    179        # Make the filename relative to the template file its being loaded 
    180        # from, but only if that file is specified as a relative path, or no 
    181        # search path has been set up 
    182        if relative_to and (not search_path or not os.path.isabs(relative_to)): 
    183            filename = os.path.join(os.path.dirname(relative_to), filename) 
    184 
    185        filename = os.path.normpath(filename) 
    186        cachekey = filename 
    187 
    188        self._lock.acquire() 
    189        try: 
    190            # First check the cache to avoid reparsing the same file 
    191            try: 
    192                tmpl = self._cache[cachekey] 
    193                if not self.auto_reload: 
    194                    return tmpl 
    195                uptodate = self._uptodate[cachekey] 
    196                if uptodate is not None and uptodate(): 
    197                    return tmpl 
    198            except (KeyError, OSError): 
    199                pass 
    200 
    201            isabs = False 
    202 
    203            if os.path.isabs(filename): 
    204                # Bypass the search path if the requested filename is absolute 
    205                search_path = [os.path.dirname(filename)] 
    206                isabs = True 
    207 
    208            elif relative_to and os.path.isabs(relative_to): 
    209                # Make sure that the directory containing the including 
    210                # template is on the search path 
    211                dirname = os.path.dirname(relative_to) 
    212                if dirname not in search_path: 
    213                    search_path = list(search_path) + [dirname] 
    214                isabs = True 
    215 
    216            elif not search_path: 
    217                # Uh oh, don't know where to look for the template 
    218                raise TemplateError('Search path for templates not configured') 
    219 
    220            for loadfunc in search_path: 
    221                if isinstance(loadfunc, string_types): 
    222                    loadfunc = directory(loadfunc) 
    223                try: 
    224                    filepath, filename, fileobj, uptodate = loadfunc(filename) 
    225                except IOError: 
    226                    continue 
    227                else: 
    228                    try: 
    229                        if isabs: 
    230                            # If the filename of either the included or the  
    231                            # including template is absolute, make sure the 
    232                            # included template gets an absolute path, too, 
    233                            # so that nested includes work properly without a 
    234                            # search path 
    235                            filename = filepath 
    236                        tmpl = self._instantiate(cls, fileobj, filepath, 
    237                                                 filename, encoding=encoding) 
    238                        if self.callback: 
    239                            self.callback(tmpl) 
    240                        self._cache[cachekey] = tmpl 
    241                        self._uptodate[cachekey] = uptodate 
    242                    finally: 
    243                        if hasattr(fileobj, 'close'): 
    244                            fileobj.close() 
    245                    return tmpl 
    246 
    247            raise TemplateNotFound(filename, search_path) 
    248 
    249        finally: 
    250            self._lock.release() 
    251 
    252    def _instantiate(self, cls, fileobj, filepath, filename, encoding=None): 
    253        """Instantiate and return the `Template` object based on the given 
    254        class and parameters. 
    255         
    256        This function is intended for subclasses to override if they need to 
    257        implement special template instantiation logic. Code that just uses 
    258        the `TemplateLoader` should use the `load` method instead. 
    259         
    260        :param cls: the class of the template object to instantiate 
    261        :param fileobj: a readable file-like object containing the template 
    262                        source 
    263        :param filepath: the absolute path to the template file 
    264        :param filename: the path to the template file relative to the search 
    265                         path 
    266        :param encoding: the encoding of the template to load; defaults to the 
    267                         ``default_encoding`` of the loader instance 
    268        :return: the loaded `Template` instance 
    269        :rtype: `Template` 
    270        """ 
    271        if encoding is None: 
    272            encoding = self.default_encoding 
    273        return cls(fileobj, filepath=filepath, filename=filename, loader=self, 
    274                   encoding=encoding, lookup=self.variable_lookup, 
    275                   allow_exec=self.allow_exec) 
    276 
    277    @staticmethod 
    278    def directory(path): 
    279        """Loader factory for loading templates from a local directory. 
    280         
    281        :param path: the path to the local directory containing the templates 
    282        :return: the loader function to load templates from the given directory 
    283        :rtype: ``function`` 
    284        """ 
    285        def _load_from_directory(filename): 
    286            filepath = os.path.join(path, filename) 
    287            fileobj = open(filepath, 'rb') 
    288            mtime = os.path.getmtime(filepath) 
    289            def _uptodate(): 
    290                return mtime == os.path.getmtime(filepath) 
    291            return filepath, filename, fileobj, _uptodate 
    292        return _load_from_directory 
    293 
    294    @staticmethod 
    295    def package(name, path): 
    296        """Loader factory for loading templates from egg package data. 
    297         
    298        :param name: the name of the package containing the resources 
    299        :param path: the path inside the package data 
    300        :return: the loader function to load templates from the given package 
    301        :rtype: ``function`` 
    302        """ 
    303        from pkg_resources import resource_stream 
    304        def _load_from_package(filename): 
    305            filepath = os.path.join(path, filename) 
    306            return filepath, filename, resource_stream(name, filepath), None 
    307        return _load_from_package 
    308 
    309    @staticmethod 
    310    def prefixed(**delegates): 
    311        """Factory for a load function that delegates to other loaders 
    312        depending on the prefix of the requested template path. 
    313         
    314        The prefix is stripped from the filename when passing on the load 
    315        request to the delegate. 
    316         
    317        >>> load = prefixed( 
    318        ...     app1 = lambda filename: ('app1', filename, None, None), 
    319        ...     app2 = lambda filename: ('app2', filename, None, None) 
    320        ... ) 
    321        >>> print(load('app1/foo.html')) 
    322        ('app1', 'app1/foo.html', None, None) 
    323        >>> print(load('app2/bar.html')) 
    324        ('app2', 'app2/bar.html', None, None) 
    325         
    326        :param delegates: mapping of path prefixes to loader functions 
    327        :return: the loader function 
    328        :rtype: ``function`` 
    329        """ 
    330        def _dispatch_by_prefix(filename): 
    331            for prefix, delegate in delegates.items(): 
    332                if filename.startswith(prefix): 
    333                    if isinstance(delegate, string_types): 
    334                        delegate = directory(delegate) 
    335                    filepath, _, fileobj, uptodate = delegate( 
    336                        filename[len(prefix):].lstrip('/\\') 
    337                    ) 
    338                    return filepath, filename, fileobj, uptodate 
    339            raise TemplateNotFound(filename, list(delegates.keys())) 
    340        return _dispatch_by_prefix 
    341 
    342 
    343directory = TemplateLoader.directory 
    344package = TemplateLoader.package 
    345prefixed = TemplateLoader.prefixed