"""
Module
------
jinja2_interface.py
Description
-----------
This module contains classes and functions to read and write
Jinja2-formatted files.
Functions
---------
_concat_tmpl(tmpl_path, in_dict)
This function parses a Jinja-formatted template file and finds
(any) referenced Jinja-formatted template files with paths
preficed with `%include`; the contents will then be extracted
and concatenated to produce a single Jinja-formatted template
file devoid of the `%include` statements.
_fail_missing_vars(tmpl_path, in_dict, warn, fail_missing=False)
This function parses the Jinja2-formatted template file and
the Python dictionary containing the Jinja2 template key and
value pairs; if a Jinja2-formatted template file variable has
not been defined (i.e., is missing) a Jinja2InterfaceError
exception is raised.
_get_env(tmpl_path)
This function defines the Jinja2 environment object.
_get_template(tmpl_path)
This function defines the Jinja2 environment template object.
_get_template_file_attrs(tmpl_path)
This function returns the template file name attributes.
_get_template_vars(tmpl_path)
This function collects the template variable names from a
Jinja2-formatted template file.
_replace_tmplmarkser(tmpl_path)
This function replaces specified non-Jinja2-formatted template
string-values with the respective Jinja2-formatted template
indicators; the updated template file is written to a
temporary (e.g., virtual) file path and returned to the
calling function; the non-Jinja2-formatted template
string-values are defined bu the `confs/template_interface.py`
module attribute `TMPL_ITEM_LIST`.
expand_template(tmpl_path, in_dict=None)
This function expands a Jinja-formatted template file by
concatenating all `%include` directive file paths into a
single virtual filepath (`output_tmpl`); if `in_dict` is not
NoneType, any Jinja-formatted variabl within the available
`%include` directives will be replaced prior to expansion.
write_from_template(tmpl_path, output_file, in_dict,
fail_missing=False, rpl_tmpl_mrks=False,
f90_bool=False, skip_missing=False, warn=True,
concat_tmpl, **kwargs)
This function writes a Jinja2-formatted file established from
a templated Jinja2-formatted file.
write_jinja2(jinja2_file, in_dict)
This function writes a Jinja2 formatted file using the
specified Python dictionary.
Author(s)
---------
Henry R. Winterbottom; 27 December 2022
History
-------
2022-12-27: Henry Winterbottom -- Initial implementation.
"""
# ----
# pylint: disable=broad-except
# pylint: disable=consider-using-f-string
# pylint: disable=raise-missing-from
# pylint: disable=too-many-arguments
# pylint: disable=too-many-locals
# pylint: disable=unused-argument
# ----
import os
from typing import Dict, List, Tuple
from jinja2 import Environment, FileSystemLoader, meta
from tools import fileio_interface, parser_interface
from utils.exceptions_interface import Jinja2InterfaceError
from utils.logger_interface import Logger
from confs.template_interface import TMPL_ITEM_LIST
# ----
# Define all available module properties.
__all__ = ["expand_template", "write_from_template", "write_jinja2"]
# ----
logger = Logger(caller_name=__name__)
# ----
def _concat_tmpl(tmpl_path: str, in_dict: Dict) -> None:
"""
Description
-----------
This function parses a Jinja-formatted template file and finds
(any) referenced Jinja-formatted template files with paths
preficed with `%include`; the contents will then be extracted and
concatenated to produce a single Jinja-formatted template file
devoid of the `%include` statements.
Parameters
----------
tmpl_path: ``str``
A Python string specifying the path to a Jinja-formatted
template file to be parsed and used to produce a concatenated
Jinja-formatted file.
in_dict: ``Dict``
A Python dictionary containing strings to be replaced prior to
concatenation.
Returns
-------
concat_tmpl_path: ``str``
A Python string specifying the path to the virtual file path
containing the concatenated Jinja-formatted file.
"""
# Read the Jinja-formatted template file to be parsed.
with open(tmpl_path, "r", encoding="utf-8") as infile:
content = infile.read()
if in_dict is not None:
for key, value in in_dict.items():
content = content.replace("{{ %s }}" % key, "%s" % value)
content = content.split("\n")
# Build the concatenated Jinja-formatted template file.
concat_tmpl_path = fileio_interface.virtual_file().name
with open(concat_tmpl_path, "w", encoding="utf-8") as outfile:
for line in content:
if line.startswith("%include"):
incl_path = line.split(" ", 1)[1].strip(' "')
with open(incl_path, "r", encoding="utf-8") as inclfile:
incl = inclfile.read()
outfile.write(f"{incl}")
else:
outfile.write(f"{line}\n")
return concat_tmpl_path
# ----
def _find_missing_vars(
tmpl_path: str,
in_dict: Dict,
warn: bool,
fail_missing: bool = False,
) -> List:
"""
Description
-----------
This function parses the Jinja2-formatted template file and the
Python dictionary containing the Jinja2 template key and value
pairs; if a Jinja2-formatted template file variable has not been
defined (i.e., is missing) a Jinja2InterfaceError exception is
raised.
Parameters
----------
tmpl_path: ``str``
A Python string defining the path to the Jinja2-formatted
template file.
in_dict: ``Dict``
A Python dictionary containing the Jinja2 template key and
value pairs.
Keywords
--------
fail_missing: ``bool``, optional
A Python boolean valued variable specifying whether to raise a
Jinja2InterfaceError exception if a Jinja2-formatted template
variable has not been specified within the Python dictionary
containing the Jinja2 template key and value pairs.
Returns
-------
missing_vars_list: ``List``
A Python list containing (any) Jinja2-formatted template
variables that have not been specified within the Python
dictionary containing the Jinja2 template key and value pairs.
Raises
------
Jinja2InterfaceError:
- raised if variables within the Jinja2-formatted template
file have not been specified within the Python dictionary
containing the Jinja2-formatted file template variable key
and value pairs (in_dict); raised only if `fail_missing` is
`True` upon entry.
"""
# Check that the Python dictionary is not empty; proceed
# accordingly.
if not in_dict:
msg = (
"The Python dictionary `in_dict` provided upon entry is empty. Aborting!!!"
)
raise Jinja2InterfaceError(msg=msg)
# Collect the variables within the Jinja2-formatted template file.
variables = _get_template_vars(tmpl_path=tmpl_path)
# Build the list of attribute variables.
compare_variables = _get_defvars(in_dict=in_dict)
# Compare the respective variable lists and find unique (i.e.,
# missing variables).
missing_vars_list = [
variable for variable in variables if variable not in compare_variables
]
if len(missing_vars_list) != 0:
msg = (
"The following Jinja2-templated variables have not been "
f"defined: {', '.join(missing_vars_list)}."
)
if fail_missing:
msg = msg + " Aborting!!!"
raise Jinja2InterfaceError(msg=msg)
if warn:
logger.warn(msg=msg)
return missing_vars_list
# ----
def _get_defvars(in_dict: Dict) -> List:
"""
Description
-----------
This function defines a list of the variables provided to populate
the respective template; the variable names are collected from the
key values of the input Python dictionary `in_dict`.
Parameters
----------
in_dict: ``Dict``
A Python dictionary containing the variables to be used to
populate the Jinja2-formatted template.
Returns
-------
defvars_list: ``List``
A Python list of the variables defined within the Python
dictionary, to populate the Jinja2-formatted template, key and
value pairs.
"""
# Define a list of the specified variables to populate the
# respective template.
defvars_list = []
for item in list(in_dict):
if isinstance(item, tuple):
defvars_list.append(item[0])
else:
defvars_list.append(item)
return defvars_list
# ----
def _get_env(tmpl_path: str) -> Environment:
"""
Description
-----------
This function defines the Jinja2 environment object.
Parameters
----------
tmpl_path: ``str``
A Python string defining the path to the Jinja2-formatted
template file.
Returns
-------
env: ``Environment``
A Python Environment object containing the Jinja2 environment.
"""
# Collect the Jinja2-formatted template file attributes.
(dirname, _) = _get_template_file_attrs(tmpl_path=tmpl_path)
# Establish the Jinja2 environment.
env = Environment(loader=FileSystemLoader(searchpath=dirname))
return env
# ----
def _get_template(tmpl_path: str) -> object:
"""
Description
-----------
This function defines the Jinja2 environment template object.
Parameters
----------
tmpl_path: ``str``
A Python string defining the path to the Jinja2-formatted
template file.
Returns
-------
tmpl: ``object``
A Python object containing the Jinja2 environment template
object.
"""
# Collect the Jinja2-formatted template file attributes.
(_, basename) = _get_template_file_attrs(tmpl_path=tmpl_path)
# Establish the Jinja2 environment.
env = _get_env(tmpl_path=tmpl_path)
# Define the Jinja2-formatted template.
tmpl = env.get_template(basename)
return tmpl
# ----
def _get_template_file_attrs(tmpl_path: str) -> Tuple[str, str]:
"""
Description
-----------
This function returns the template file name attributes.
Parameters
----------
tmpl_path: ``str``
A Python string defining the path to the Jinja2-formatted
template file.
Returns
-------
dirname: ``str``
A Python string specifying the directory tree path within for
the Jinja2 templated file define upon entry.
basename: ``str``
A Python string specifying the base-filename for the Jinja2
templated file path defined upon entry.
"""
# Collect the Jinja2-formatted template file attributes.
(dirname, basename) = [os.path.dirname(tmpl_path), os.path.basename(tmpl_path)]
return (dirname, basename)
# ----
def _get_template_vars(tmpl_path: str) -> List:
"""
Description
-----------
This function collects the template variable names from a
Jinja2-formatted template file.
Parameters
----------
tmpl_path: ``str``
A Python string defining the path to the Jinja2-formatted
template file.
Returns
-------
variables: ``List``
A Python list of Jinja2-formatted template file variables.
"""
# Define the Jinja2 templating attributes.
env = _get_env(tmpl_path=tmpl_path)
tmpl = _get_template(tmpl_path=tmpl_path)
# Collect the templated variable names.
variables = list(meta.find_undeclared_variables(env.parse(tmpl)))
# If variables are collected, check again by search for the
# Jinja2-formatted template variables; proceed accordingly.
if len(variables) == 0:
# Initialize the variables.
variables = []
start_str = "{{"
stop_str = "}}"
# Collect all data from the Jinja2-formatted file.
with open(tmpl_path, "r", encoding="utf-8") as file:
data = file.read().split("\n")
# Search for Jinja2-formatted template variables; proceed
# accordingly; ignoring template variable with default values.
for item in data:
if (start_str and stop_str in item) and ("or" not in item):
start = item.index(start_str)
stop = item.index(stop_str)
string = (item[start + len(start_str) : stop].rstrip()).lstrip()
variables.append(string)
return variables
# ----
def _replace_tmplmarkers(tmpl_path: str) -> str:
"""
Description
-----------
This function replaces specified non-Jinja2-formatted template
string-values with the respective Jinja2-formatted template
indicators; the updated template file is written to a temporary
(e.g., virtual) file path and returned to the calling function;
the non-Jinja2-formatted template string-values are defined bu the
`confs/template_interface.py` module attribute `TMPL_ITEM_LIST`.
Parameters
----------
tmpl_path: ``str``
A Python string defining the path to the template file
containing non-Jinja2-formatted template string-values.
Returns
-------
virtfile: ``str``
A Python string defining the path to the temporary (i.e.,
virtual) file path containing the Jinja2-formatted template
defined from the attributes contained within `tmpl_path` upon
entry.
"""
# Read the non-Jinja2-formatted template file.
with open(tmpl_path, "r", encoding="utf-8") as file:
inputs = file.read().split("\n")
# Parse the contents of the non-Jinja2-formatted template file;
# update any encountered non-Jinja2-formatted template
# string-values with the appropriate Jinja2-formatted template
# string-values.
virtfile = fileio_interface.virtual_file().name
with open(virtfile, "w", encoding="utf-8") as file:
for string in inputs:
for item in TMPL_ITEM_LIST:
tmplstr = item.split("%s")
if (tmplstr[0] in string) and (tmplstr[1] in string):
string = string.replace(tmplstr[0].strip(), "{{ ")
string = string.replace(tmplstr[1].strip(), " }}")
break
file.write(f"{string}\n")
return virtfile
# ----
[docs]def expand_template(tmpl_path: str, in_dict: Dict = None) -> str:
"""
Description
-----------
This function expands a Jinja-formatted template file by
concatenating all `%include` directive file paths into a single
virtual filepath (`output_tmpl`); if `in_dict` is not NoneType,
any Jinja-formatted variabl within the available `%include`
directives will be replaced prior to expansion.
Parameters
----------
tmpl_path: ``str``
A Python string specifying the template file to be expanded.
Keywords
--------
in_dict: ``Dict``, optional
A Python dictionary containing key and value pairs to be used
to parse the respective Jinja-formatted template prior to
expansion.
Returns
-------
output_tmpl: ``str``
A Python string specifying the path to the virtual file path
containing the expanded template.
"""
# Expand the Jinja-formatted template file.
output_tmpl = _concat_tmpl(tmpl_path=tmpl_path, in_dict=in_dict)
return output_tmpl
# ----
[docs]def write_from_template(
tmpl_path: str,
output_file: str,
in_dict: Dict,
fail_missing: bool = False,
rpl_tmpl_mrks: bool = False,
f90_bool: bool = False,
skip_missing: bool = False,
warn: bool = True,
concat_tmpl: bool = False,
**kwargs: Dict,
) -> None:
"""
Description
-----------
This function writes a Jinja2-formatted file established from a
templated Jinja2-formatted file.
Parameters
----------
tmpl_path: ``str``
A Python string defining the path to the Jinja2-formatted
template file.
output_file: ``str``
A Python string containing the full-path to the
Jinja2-formatted file to be written.
in_dict: ``Dict``
A Python dictionary containing the template variable key and
value pairs.
Keywords
--------
fail_missing: ``bool``, optional
A Python boolean valued variable specifying whether to fail if
variables within the Jinja2-formatted template file have not
been specified within the Python dictionary containing the
Jinja2-formatted file template variable key and value pairs
(`in_dict`).
rpl_tmpl_mrks: ``bool``, optional
A Python boolean valued variable specifying whether to replace
any pre-defined template markers (see
`confs/template_interface.py`, prior to populating the
Jinja2-formatted template.
f90_bool: ``bool``, optional
A Python boolean valued variable specifying whether to
transform boolean variables to a FORTRAN 90 format.
skip_missing: ``bool``, optional
A Python boolean valued variable specifying whether to skip
(i.e., exclude from output) template variables that are not
specified within Python dictionary containing the
Jinja2-formatted file template variable key and value pairs
(`in_dict`).
warn: ``bool``, optional
A Python boolean valued variable specifying whether to create
`Logger` warning messages for missing template variables.
Other Parameters
----------------
kwargs: ``Dict``
A Python dictionary containing additional key and value pairs
to be passed to the function.
Raises
------
Jinja2InterfaceError:
- raised if an exception is encountered while writing the
Jinja2-formatted file.
"""
# Format the template and attribute values accordingly.
if rpl_tmpl_mrks:
tmpl_path = _replace_tmplmarkers(tmpl_path=tmpl_path)
if f90_bool:
for key, value in in_dict.items():
in_dict[key] = parser_interface.f90_bool(value)
# Determine what, if any, variables have not been specified with
# corresponding Python dictionary `in_dict` key and value pairs.
missing_vars_list = _find_missing_vars(
tmpl_path=tmpl_path, in_dict=in_dict, warn=warn, fail_missing=fail_missing
)
# Read the original template and remove any strings containing
# matches to those in `missing_vars_list`; proceed accordingly.
if skip_missing:
virtfile = fileio_interface.virtual_file().name
with open(tmpl_path, "r", encoding="utf-8") as file:
tmpl_in_list = file.read().split("\n")
with open(virtfile, "w", encoding="utf-8") as file:
for tmpl_var in tmpl_in_list:
if not any(
missing_var
for missing_var in missing_vars_list
if missing_var in tmpl_var
):
file.write(f"{tmpl_var}\n")
tmpl_path = virtfile
# Open the Jinja2-formatted template file, update the Jinja2
# template variable(s), and write the results to the output file
# path.
try:
tmpl = _get_template(tmpl_path=tmpl_path)
with open(output_file, "w", encoding="utf-8") as file:
file.write(tmpl.render(in_dict, env=os.environ))
except Exception as errmsg:
msg = (
f"Rendering Jinja2-formatted file {output_file} failed with "
f"error {errmsg}. Aborting!!!"
)
raise Jinja2InterfaceError(msg=msg)
if skip_missing or rpl_tmpl_mrks:
os.unlink(path=virtfile)
# ----
[docs]def write_jinja2(jinja2_file: str, in_dict: Dict) -> None:
"""
Description
-----------
This function writes a Jinja2-formatted file using the specified
Python dictionary.
Parameters
----------
jinja2_file: ``str``
A Python string containing the full-path to the
Jinja2-formatted file to be written.
in_dict: ``Dict``
A Python dictionary containing the attributes to be written to
the Jinja2 file.
Raises
------
Jinja2InterfaceError:
- raised if an exception is encountered while writing the
Jinja2-formatted file.
"""
# Open and write the dictionary contents to the specified
# Jinja2-formatted file path; proceed accordingly.
msg = f"Writing Jinja2 formatted file {jinja2_file}."
logger.info(msg=msg)
try:
with open(jinja2_file, "w", encoding="utf-8") as file:
file.write("#!Jinja2\n")
for key in in_dict.keys():
value = in_dict[key]
if isinstance(value, str):
string = f'set {key} = "{value}"'
else:
string = f"set {key} = {value}"
file.write("{%% %s %%}\n" % string)
except Exception as errmsg:
msg = f"Writing Jinja2-formatted file {jinja2_file} failed with error {errmsg}. Aborting!!!"
raise Jinja2InterfaceError(msg=msg)