import os
import os.path
import stat
import time
import typing
from wheezy.template.comp import Tuple
from wheezy.template.engine import Engine
from wheezy.template.typing import Loader, SupportsRender
[docs]class FileLoader(Loader):
"""Loads templates from file system.
``directories`` - search path of directories to scan for template.
``encoding`` - decode template content per encoding.
"""
def __init__(
self, directories: typing.List[str], encoding: str = "UTF-8"
) -> None:
searchpath: typing.List[str] = []
for path in directories:
abspath = os.path.abspath(path)
assert os.path.exists(abspath)
assert os.path.isdir(abspath)
searchpath.append(abspath)
self.searchpath = searchpath
self.encoding = encoding
[docs] def list_names(self) -> Tuple[str, ...]:
"""Return a list of names relative to directories. Ignores any files
and directories that start with dot.
"""
names = []
for path in self.searchpath:
pathlen = len(path) + 1
for dirpath, dirnames, filenames in os.walk(path):
for i in [
i
for i, name in enumerate(dirnames)
if name.startswith(".")
]:
del dirnames[i]
for filename in filenames:
if filename.startswith("."):
continue
name = os.path.join(dirpath, filename)[pathlen:]
name = name.replace("\\", "/")
names.append(name)
return tuple(sorted(names))
[docs] def get_fullname(self, name: str) -> typing.Optional[str]:
"""Returns a full path by a template name."""
for path in self.searchpath:
filename = os.path.join(path, name)
if not os.path.exists(filename):
continue
if not os.path.isfile(filename):
continue
return filename
else:
return None
[docs] def load(self, name: str) -> typing.Optional[str]:
"""Loads a template by name from file system."""
filename = self.get_fullname(name)
if filename:
f = open(filename, "rb")
try:
return f.read().decode(self.encoding)
finally:
f.close()
return None
[docs]class DictLoader(Loader):
"""Loads templates from python dictionary.
``templates`` - a dict where key corresponds to template name and
value to template content.
"""
def __init__(self, templates: typing.Mapping[str, str]) -> None:
self.templates = templates
[docs] def list_names(self) -> Tuple[str, ...]:
"""List all keys from internal dict."""
return tuple(sorted(self.templates.keys()))
[docs] def load(self, name: str) -> typing.Optional[str]:
"""Returns template by name."""
if name not in self.templates:
return None
return self.templates[name]
[docs]class ChainLoader(Loader):
"""Loads templates from ``loaders`` until first succeed."""
def __init__(self, loaders: typing.List[Loader]) -> None:
self.loaders = loaders
[docs] def list_names(self) -> Tuple[str, ...]:
"""Returns as list of names from all loaders."""
names = set()
for loader in self.loaders:
names |= set(loader.list_names())
return tuple(sorted(names))
[docs] def load(self, name: str) -> typing.Optional[str]:
"""Returns template by name from the first loader that succeed."""
for loader in self.loaders:
source = loader.load(name)
if source is not None:
return source
return None
[docs]class PreprocessLoader(Loader):
"""Performs preprocessing of loaded template."""
def __init__(
self,
engine: Engine,
ctx: typing.Optional[typing.Mapping[str, typing.Any]] = None,
) -> None:
self.engine = engine
self.ctx = ctx or {}
def list_names(self) -> Tuple[str, ...]:
return self.engine.loader.list_names()
def load(self, name: str) -> str:
return self.engine.render(name, self.ctx, {}, {})
[docs]def autoreload(engine: Engine, enabled: bool = True) -> Engine:
"""Auto reload template if changes are detected in file.
Limitation: master (inherited), imported and preprocessed templates.
It is recommended to use application server that supports
file reload instead.
"""
if not enabled:
return engine
return AutoReloadProxy(engine)
# region: internal details
[docs]class AutoReloadProxy(Engine):
def __init__(self, engine: Engine):
from warnings import warn
self.engine = engine
self.names: typing.Dict[str, float] = {}
warn(
"autoreload limitation: master (inherited), imported "
"and preprocessed templates. It is recommended to use "
"application server that supports file reload instead.",
stacklevel=3,
)
[docs] def get_template(self, name: str) -> SupportsRender:
if self.file_changed(name):
self.remove(name)
return self.engine.get_template(name)
[docs] def render(
self,
name: str,
ctx: typing.Mapping[str, typing.Any],
local_defs: typing.Mapping[str, typing.Any],
super_defs: typing.Mapping[str, typing.Any],
) -> str:
if self.file_changed(name):
self.remove(name)
return self.engine.render(name, ctx, local_defs, super_defs)
[docs] def remove(self, name: str) -> None:
self.engine.remove(name)
# region: internal details
def __getattr__(self, name: str) -> typing.Any:
return getattr(self.engine, name)
def file_changed(self, name: str) -> bool:
try:
last_known_stamp = self.names[name]
current_time = time.time()
if current_time - last_known_stamp <= 2:
return False
except KeyError:
last_known_stamp = 0
loader = self.engine.loader
abspath = loader.get_fullname(name) # type: ignore[attr-defined]
if not abspath:
return False
last_modified_stamp = os.stat(abspath)[stat.ST_MTIME]
if last_modified_stamp <= last_known_stamp:
return False
self.names[name] = last_modified_stamp
return True