""" API and implementations for loading templates from different data
sources .
"""
import importlib . util
import os
import posixpath
import sys
import typing as t
import weakref
import zipimport
from collections import abc
from hashlib import sha1
from importlib import import_module
from types import ModuleType
from . exceptions import TemplateNotFound
from . utils import internalcode
if t . TYPE_CHECKING :
from . environment import Environment
from . environment import Template
def split_template_path ( template : str ) - > t . List [ str ] :
""" Split a path into segments and perform a sanity check. If it detects
' .. ' in the path it will raise a ` TemplateNotFound ` error .
"""
pieces = [ ]
for piece in template . split ( " / " ) :
if (
os . path . sep in piece
or ( os . path . altsep and os . path . altsep in piece )
or piece == os . path . pardir
) :
raise TemplateNotFound ( template )
elif piece and piece != " . " :
pieces . append ( piece )
return pieces
class BaseLoader :
""" Baseclass for all loaders. Subclass this and override `get_source` to
implement a custom loading mechanism . The environment provides a
` get_template ` method that calls the loader ' s `load` method to get the
: class : ` Template ` object .
A very basic example for a loader that looks up templates on the file
system could look like this : :
from jinja2 import BaseLoader , TemplateNotFound
from os . path import join , exists , getmtime
class MyLoader ( BaseLoader ) :
def __init__ ( self , path ) :
self . path = path
def get_source ( self , environment , template ) :
path = join ( self . path , template )
if not exists ( path ) :
raise TemplateNotFound ( template )
mtime = getmtime ( path )
with open ( path ) as f :
source = f . read ( )
return source , path , lambda : mtime == getmtime ( path )
"""
#: if set to `False` it indicates that the loader cannot provide access
#: to the source of templates.
#:
#: .. versionadded:: 2.4
has_source_access = True
def get_source (
self , environment : " Environment " , template : str
) - > t . Tuple [ str , t . Optional [ str ] , t . Optional [ t . Callable [ [ ] , bool ] ] ] :
""" Get the template source, filename and reload helper for a template.
It ' s passed the environment and template name and has to return a
tuple in the form ` ` ( source , filename , uptodate ) ` ` or raise a
` TemplateNotFound ` error if it can ' t locate the template.
The source part of the returned tuple must be the source of the
template as a string . The filename should be the name of the
file on the filesystem if it was loaded from there , otherwise
` ` None ` ` . The filename is used by Python for the tracebacks
if no loader extension is used .
The last item in the tuple is the ` uptodate ` function . If auto
reloading is enabled it ' s always called to check if the template
changed . No arguments are passed so the function must store the
old state somewhere ( for example in a closure ) . If it returns ` False `
the template will be reloaded .
"""
if not self . has_source_access :
raise RuntimeError (
f " { type ( self ) . __name__ } cannot provide access to the source "
)
raise TemplateNotFound ( template )
def list_templates ( self ) - > t . List [ str ] :
""" Iterates over all templates. If the loader does not support that
it should raise a : exc : ` TypeError ` which is the default behavior .
"""
raise TypeError ( " this loader cannot iterate over all templates " )
@internalcode
def load (
self ,
environment : " Environment " ,
name : str ,
globals : t . Optional [ t . MutableMapping [ str , t . Any ] ] = None ,
) - > " Template " :
""" Loads a template. This method looks up the template in the cache
or loads one by calling : meth : ` get_source ` . Subclasses should not
override this method as loaders working on collections of other
loaders ( such as : class : ` PrefixLoader ` or : class : ` ChoiceLoader ` )
will not call this method but ` get_source ` directly .
"""
code = None
if globals is None :
globals = { }
# first we try to get the source for this template together
# with the filename and the uptodate function.
source , filename , uptodate = self . get_source ( environment , name )
# try to load the code from the bytecode cache if there is a
# bytecode cache configured.
bcc = environment . bytecode_cache
if bcc is not None :
bucket = bcc . get_bucket ( environment , name , filename , source )
code = bucket . code
# if we don't have code so far (not cached, no longer up to
# date) etc. we compile the template
if code is None :
code = environment . compile ( source , name , filename )
# if the bytecode cache is available and the bucket doesn't
# have a code so far, we give the bucket the new code and put
# it back to the bytecode cache.
if bcc is not None and bucket . code is None :
bucket . code = code
bcc . set_bucket ( bucket )
return environment . template_class . from_code (
environment , code , globals , uptodate
)
class FileSystemLoader ( BaseLoader ) :
""" Load templates from a directory in the file system.
The path can be relative or absolute . Relative paths are relative to
the current working directory .
. . code - block : : python
loader = FileSystemLoader ( " templates " )
A list of paths can be given . The directories will be searched in
order , stopping at the first matching template .
. . code - block : : python
loader = FileSystemLoader ( [ " /override/templates " , " /default/templates " ] )
: param searchpath : A path , or list of paths , to the directory that
contains the templates .
: param encoding : Use this encoding to read the text from template
files .
: param followlinks : Follow symbolic links in the path .
. . versionchanged : : 2.8
Added the ` ` followlinks ` ` parameter .
"""
def __init__ (
self ,
searchpath : t . Union [ str , os . PathLike , t . Sequence [ t . Union [ str , os . PathLike ] ] ] ,
encoding : str = " utf-8 " ,
followlinks : bool = False ,
) - > None :
if not isinstance ( searchpath , abc . Iterable ) or isinstance ( searchpath , str ) :
searchpath = [ searchpath ]
self . searchpath = [ os . fspath ( p ) for p in searchpath ]
self . encoding = encoding
self . followlinks = followlinks
def get_source (
self , environment : " Environment " , template : str
) - > t . Tuple [ str , str , t . Callable [ [ ] , bool ] ] :
pieces = split_template_path ( template )
for searchpath in self . searchpath :
# Use posixpath even on Windows to avoid "drive:" or UNC
# segments breaking out of the search directory.
filename = posixpath . join ( searchpath , * pieces )
if os . path . isfile ( filename ) :
break
else :
raise TemplateNotFound ( template )
with open ( filename , encoding = self . encoding ) as f :
contents = f . read ( )
mtime = os . path . getmtime ( filename )
def uptodate ( ) - > bool :
try :
return os . path . getmtime ( filename ) == mtime
except OSError :
return False
# Use normpath to convert Windows altsep to sep.
return contents , os . path . normpath ( filename ) , uptodate
def list_templates ( self ) - > t . List [ str ] :
found = set ( )
for searchpath in self . searchpath :
walk_dir = os . walk ( searchpath , followlinks = self . followlinks )
for dirpath , _ , filenames in walk_dir :
for filename in filenames :
template = (
os . path . join ( dirpath , filename ) [ len ( searchpath ) : ]
. strip ( os . path . sep )
. replace ( os . path . sep , " / " )
)
if template [ : 2 ] == " ./ " :
template = template [ 2 : ]
if template not in found :
found . add ( template )
return sorted ( found )
class PackageLoader ( BaseLoader ) :
""" Load templates from a directory in a Python package.
: param package_name : Import name of the package that contains the
template directory .
: param package_path : Directory within the imported package that
contains the templates .
: param encoding : Encoding of template files .
The following example looks up templates in the ` ` pages ` ` directory
within the ` ` project . ui ` ` package .
. . code - block : : python
loader = PackageLoader ( " project.ui " , " pages " )
Only packages installed as directories ( standard pip behavior ) or
zip / egg files ( less common ) are supported . The Python API for
introspecting data in packages is too limited to support other
installation methods the way this loader requires .
There is limited support for : pep : ` 420 ` namespace packages . The
template directory is assumed to only be in one namespace
contributor . Zip files contributing to a namespace are not
supported .
. . versionchanged : : 3.0
No longer uses ` ` setuptools ` ` as a dependency .
. . versionchanged : : 3.0
Limited PEP 420 namespace package support .
"""
def __init__ (
self ,
package_name : str ,
package_path : " str " = " templates " ,
encoding : str = " utf-8 " ,
) - > None :
package_path = os . path . normpath ( package_path ) . rstrip ( os . path . sep )
# normpath preserves ".", which isn't valid in zip paths.
if package_path == os . path . curdir :
package_path = " "
elif package_path [ : 2 ] == os . path . curdir + os . path . sep :
package_path = package_path [ 2 : ]
self . package_path = package_path
self . package_name = package_name
self . encoding = encoding
# Make sure the package exists. This also makes namespace
# packages work, otherwise get_loader returns None.
import_module ( package_name )
spec = importlib . util . find_spec ( package_name )
assert spec is not None , " An import spec was not found for the package. "
loader = spec . loader
assert loader is not None , " A loader was not found for the package. "
self . _loader = loader
self . _archive = None
template_root = None
if isinstance ( loader , zipimport . zipimporter ) :
self . _archive = loader . archive
pkgdir = next ( iter ( spec . submodule_search_locations ) ) # type: ignore
template_root = os . path . join ( pkgdir , package_path ) . rstrip ( os . path . sep )
else :
roots : t . List [ str ] = [ ]
# One element for regular packages, multiple for namespace
# packages, or None for single module file.
if spec . submodule_search_locations :
roots . extend ( spec . submodule_search_locations )
# A single module file, use the parent directory instead.
elif spec . origin is not None :
roots . append ( os . path . dirname ( spec . origin ) )
for root in roots :
root = os . path . join ( root , package_path )
if os . path . isdir ( root ) :
template_root = root
break
if template_root is None :
raise ValueError (
f " The { package_name !r} package was not installed in a "
" way that PackageLoader understands. "
)
self . _template_root = template_root
def get_source (
self , environment : " Environment " , template : str
) - > t . Tuple [ str , str , t . Optional [ t . Callable [ [ ] , bool ] ] ] :
# Use posixpath even on Windows to avoid "drive:" or UNC
# segments breaking out of the search directory. Use normpath to
# convert Windows altsep to sep.
p = os . path . normpath (
posixpath . join ( self . _template_root , * split_template_path ( template ) )
)
up_to_date : t . Optional [ t . Callable [ [ ] , bool ] ]
if self . _archive is None :
# Package is a directory.
if not os . path . isfile ( p ) :
raise TemplateNotFound ( template )
with open ( p , " rb " ) as f :
source = f . read ( )
mtime = os . path . getmtime ( p )
def up_to_date ( ) - > bool :
return os . path . isfile ( p ) and os . path . getmtime ( p ) == mtime
else :
# Package is a zip file.
try :
source = self . _loader . get_data ( p ) # type: ignore
except OSError as e :
raise TemplateNotFound ( template ) from e
# Could use the zip's mtime for all template mtimes, but
# would need to safely reload the module if it's out of
# date, so just report it as always current.
up_to_date = None
return source . decode ( self . encoding ) , p , up_to_date
def list_templates ( self ) - > t . List [ str ] :
results : t . List [ str ] = [ ]
if self . _archive is None :
# Package is a directory.
offset = len ( self . _template_root )
for dirpath , _ , filenames in os . walk ( self . _template_root ) :
dirpath = dirpath [ offset : ] . lstrip ( os . path . sep )
results . extend (
os . path . join ( dirpath , name ) . replace ( os . path . sep , " / " )
for name in filenames
)
else :
if not hasattr ( self . _loader , " _files " ) :
raise TypeError (
" This zip import does not have the required "
" metadata to list templates. "
)
# Package is a zip file.
prefix = (
self . _template_root [ len ( self . _archive ) : ] . lstrip ( os . path . sep )
+ os . path . sep
)
offset = len ( prefix )
for name in self . _loader . _files . keys ( ) :
# Find names under the templates directory that aren't directories.
if name . startswith ( prefix ) and name [ - 1 ] != os . path . sep :
results . append ( name [ offset : ] . replace ( os . path . sep , " / " ) )
results . sort ( )
return results
class DictLoader ( BaseLoader ) :
""" Loads a template from a Python dict mapping template names to
template source . This loader is useful for unittesting :
>> > loader = DictLoader ( { ' index.html ' : ' source here ' } )
Because auto reloading is rarely useful this is disabled per default .
"""
def __init__ ( self , mapping : t . Mapping [ str , str ] ) - > None :
self . mapping = mapping
def get_source (
self , environment : " Environment " , template : str
) - > t . Tuple [ str , None , t . Callable [ [ ] , bool ] ] :
if template in self . mapping :
source = self . mapping [ template ]
return source , None , lambda : source == self . mapping . get ( template )
raise TemplateNotFound ( template )
def list_templates ( self ) - > t . List [ str ] :
return sorted ( self . mapping )
class FunctionLoader ( BaseLoader ) :
""" A loader that is passed a function which does the loading. The
function receives the name of the template and has to return either
a string with the template source , a tuple in the form ` ` ( source ,
filename , uptodatefunc ) ` ` or ` None ` if the template does not exist .
>> > def load_template ( name ) :
. . . if name == ' index.html ' :
. . . return ' ... '
. . .
>> > loader = FunctionLoader ( load_template )
The ` uptodatefunc ` is a function that is called if autoreload is enabled
and has to return ` True ` if the template is still up to date . For more
details have a look at : meth : ` BaseLoader . get_source ` which has the same
return value .
"""
def __init__ (
self ,
load_func : t . Callable [
[ str ] ,
t . Optional [
t . Union [
str , t . Tuple [ str , t . Optional [ str ] , t . Optional [ t . Callable [ [ ] , bool ] ] ]
]
] ,
] ,
) - > None :
self . load_func = load_func
def get_source (
self , environment : " Environment " , template : str
) - > t . Tuple [ str , t . Optional [ str ] , t . Optional [ t . Callable [ [ ] , bool ] ] ] :
rv = self . load_func ( template )
if rv is None :
raise TemplateNotFound ( template )
if isinstance ( rv , str ) :
return rv , None , None
return rv
class PrefixLoader ( BaseLoader ) :
""" A loader that is passed a dict of loaders where each loader is bound
to a prefix . The prefix is delimited from the template by a slash per
default , which can be changed by setting the ` delimiter ` argument to
something else : :
loader = PrefixLoader ( {
' app1 ' : PackageLoader ( ' mypackage.app1 ' ) ,
' app2 ' : PackageLoader ( ' mypackage.app2 ' )
} )
By loading ` ` ' app1/index.html ' ` ` the file from the app1 package is loaded ,
by loading ` ` ' app2/index.html ' ` ` the file from the second .
"""
def __init__ (
self , mapping : t . Mapping [ str , BaseLoader ] , delimiter : str = " / "
) - > None :
self . mapping = mapping
self . delimiter = delimiter
def get_loader ( self , template : str ) - > t . Tuple [ BaseLoader , str ] :
try :
prefix , name = template . split ( self . delimiter , 1 )
loader = self . mapping [ prefix ]
except ( ValueError , KeyError ) as e :
raise TemplateNotFound ( template ) from e
return loader , name
def get_source (
self , environment : " Environment " , template : str
) - > t . Tuple [ str , t . Optional [ str ] , t . Optional [ t . Callable [ [ ] , bool ] ] ] :
loader , name = self . get_loader ( template )
try :
return loader . get_source ( environment , name )
except TemplateNotFound as e :
# re-raise the exception with the correct filename here.
# (the one that includes the prefix)
raise TemplateNotFound ( template ) from e
@internalcode
def load (
self ,
environment : " Environment " ,
name : str ,
globals : t . Optional [ t . MutableMapping [ str , t . Any ] ] = None ,
) - > " Template " :
loader , local_name = self . get_loader ( name )
try :
return loader . load ( environment , local_name , globals )
except TemplateNotFound as e :
# re-raise the exception with the correct filename here.
# (the one that includes the prefix)
raise TemplateNotFound ( name ) from e
def list_templates ( self ) - > t . List [ str ] :
result = [ ]
for prefix , loader in self . mapping . items ( ) :
for template in loader . list_templates ( ) :
result . append ( prefix + self . delimiter + template )
return result
class ChoiceLoader ( BaseLoader ) :
""" This loader works like the `PrefixLoader` just that no prefix is
specified . If a template could not be found by one loader the next one
is tried .
>> > loader = ChoiceLoader ( [
. . . FileSystemLoader ( ' /path/to/user/templates ' ) ,
. . . FileSystemLoader ( ' /path/to/system/templates ' )
. . . ] )
This is useful if you want to allow users to override builtin templates
from a different location .
"""
def __init__ ( self , loaders : t . Sequence [ BaseLoader ] ) - > None :
self . loaders = loaders
def get_source (
self , environment : " Environment " , template : str
) - > t . Tuple [ str , t . Optional [ str ] , t . Optional [ t . Callable [ [ ] , bool ] ] ] :
for loader in self . loaders :
try :
return loader . get_source ( environment , template )
except TemplateNotFound :
pass
raise TemplateNotFound ( template )
@internalcode
def load (
self ,
environment : " Environment " ,
name : str ,
globals : t . Optional [ t . MutableMapping [ str , t . Any ] ] = None ,
) - > " Template " :
for loader in self . loaders :
try :
return loader . load ( environment , name , globals )
except TemplateNotFound :
pass
raise TemplateNotFound ( name )
def list_templates ( self ) - > t . List [ str ] :
found = set ( )
for loader in self . loaders :
found . update ( loader . list_templates ( ) )
return sorted ( found )
class _TemplateModule ( ModuleType ) :
""" Like a normal module but with support for weak references """
class ModuleLoader ( BaseLoader ) :
""" This loader loads templates from precompiled templates.
Example usage :
>> > loader = ChoiceLoader ( [
. . . ModuleLoader ( ' /path/to/compiled/templates ' ) ,
. . . FileSystemLoader ( ' /path/to/templates ' )
. . . ] )
Templates can be precompiled with : meth : ` Environment . compile_templates ` .
"""
has_source_access = False
def __init__ (
self , path : t . Union [ str , os . PathLike , t . Sequence [ t . Union [ str , os . PathLike ] ] ]
) - > None :
package_name = f " _jinja2_module_templates_ { id ( self ) : x } "
# create a fake module that looks for the templates in the
# path given.
mod = _TemplateModule ( package_name )
if not isinstance ( path , abc . Iterable ) or isinstance ( path , str ) :
path = [ path ]
mod . __path__ = [ os . fspath ( p ) for p in path ]
sys . modules [ package_name ] = weakref . proxy (
mod , lambda x : sys . modules . pop ( package_name , None )
)
# the only strong reference, the sys.modules entry is weak
# so that the garbage collector can remove it once the
# loader that created it goes out of business.
self . module = mod
self . package_name = package_name
@staticmethod
def get_template_key ( name : str ) - > str :
return " tmpl_ " + sha1 ( name . encode ( " utf-8 " ) ) . hexdigest ( )
@staticmethod
def get_module_filename ( name : str ) - > str :
return ModuleLoader . get_template_key ( name ) + " .py "
@internalcode
def load (
self ,
environment : " Environment " ,
name : str ,
globals : t . Optional [ t . MutableMapping [ str , t . Any ] ] = None ,
) - > " Template " :
key = self . get_template_key ( name )
module = f " { self . package_name } . { key } "
mod = getattr ( self . module , module , None )
if mod is None :
try :
mod = __import__ ( module , None , None , [ " root " ] )
except ImportError as e :
raise TemplateNotFound ( name ) from e
# remove the entry from sys.modules, we only want the attribute
# on the module object we have stored on the loader.
sys . modules . pop ( module , None )
if globals is None :
globals = { }
return environment . template_class . from_module_dict (
environment , mod . __dict__ , globals
)