Source code for ssg.jinja

"""
Common functions for processing Jinja2 in SSG
"""

from __future__ import absolute_import

import os.path
import sys
import jinja2

from urllib.parse import quote
from shlex import quote as shell_quote

from .constants import JINJA_MACROS_DIRECTORY
from .utils import (required_key,
                    product_to_name,
                    name_to_platform,
                    product_to_platform,
                    banner_regexify,
                    banner_anchor_wrap,
                    escape_id,
                    escape_regex,
                    escape_yaml_key,
                    sha256
                    )


[docs] class MacroError(RuntimeError): pass
[docs] class AbsolutePathFileSystemLoader(jinja2.BaseLoader): """ AbsolutePathFileSystemLoader is a custom Jinja2 template loader that loads templates from the file system using absolute paths. Attributes: encoding (str): The encoding used to read the template files. Defaults to 'utf-8'. """ def __init__(self, encoding='utf-8'): self.encoding = encoding
[docs] def get_source(self, environment, template): """ Retrieves the source code of a Jinja2 template from the file system. Args: environment (jinja2.Environment): The Jinja2 environment. template (str): The absolute path to the template file. Returns: tuple: A tuple containing the template contents as a string, the template path, and a function to check if the template file has been updated. Raises: jinja2.TemplateNotFound: If the template file does not exist or the path is not absolute. RuntimeError: If there is an error reading the template file. """ if not os.path.isabs(template): raise jinja2.TemplateNotFound(template) template_file = jinja2.utils.open_if_exists(template) if template_file is None: raise jinja2.TemplateNotFound(template) try: contents = template_file.read().decode(self.encoding) except Exception as exc: msg = ("Error reading file {template}: {exc}" .format(template=template, exc=str(exc))) raise RuntimeError(msg) from exc finally: template_file.close() mtime = os.path.getmtime(template) def uptodate(): try: return os.path.getmtime(template) == mtime except OSError: return False return contents, template, uptodate
def _preload_macros_from_file(env, macros_file): template = env.get_template(macros_file) module = template.module for name in dir(module): item = getattr(module, name) if callable(item) and not name.startswith("_"): env.globals[name] = item
[docs] def preload_macros(env): if "site-packages" in JINJA_MACROS_DIRECTORY and env.globals.get('product_dir'): # use product_dir to find macros directory jinja_macros_directory = os.path.join( os.path.dirname(os.path.dirname(env.globals['product_dir'])), "shared", "macros" ) else: jinja_macros_directory = JINJA_MACROS_DIRECTORY for filename in sorted(os.listdir(jinja_macros_directory)): if not filename.endswith(".jinja"): continue macros_file = os.path.join(jinja_macros_directory, filename) _preload_macros_from_file(env, macros_file)
[docs] class JinjaEnvironment(jinja2.Environment): def __init__(self, bytecode_cache=None): super(JinjaEnvironment, self).__init__( block_start_string="{{%", block_end_string="%}}", variable_start_string="{{{", variable_end_string="}}}", comment_start_string="{{#", comment_end_string="#}}", loader=AbsolutePathFileSystemLoader(), bytecode_cache=bytecode_cache )
# Module-level cached environment for jinja environment _jinja_env = None def _get_jinja_environment(substitutions_dict): """ Initializes and returns a Jinja2 Environment with custom settings and filters. This function sets up a Jinja2 Environment with custom block, variable, and comment delimiters. It also configures a bytecode cache if specified in the substitutions_dict. Additionally, it adds several custom filters to the environment. Args: substitutions_dict (dict): A dictionary containing configuration options. Expected keys include: - "jinja2_cache_enabled": A string ("true" or "false") indicating whether bytecode caching is enabled. - "jinja2_cache_dir": The directory path for storing the bytecode cache (required if caching is enabled). Returns: jinja2.Environment: The configured Jinja2 Environment instance. """ global _jinja_env if _jinja_env is None: bytecode_cache = None if substitutions_dict.get("jinja2_cache_enabled") == "true": bytecode_cache = jinja2.FileSystemBytecodeCache( required_key(substitutions_dict, "jinja2_cache_dir") ) # TODO: Choose better syntax? _jinja_env = JinjaEnvironment(bytecode_cache=bytecode_cache) add_python_functions(substitutions_dict) _jinja_env.filters['banner_anchor_wrap'] = banner_anchor_wrap _jinja_env.filters['banner_regexify'] = banner_regexify _jinja_env.filters['escape_id'] = escape_id _jinja_env.filters['escape_regex'] = escape_regex _jinja_env.filters['escape_yaml_key'] = escape_yaml_key _jinja_env.filters['quote'] = shell_quote _jinja_env.filters['sha256'] = sha256 _jinja_env.globals.update(substitutions_dict) preload_macros(_jinja_env) return _jinja_env
[docs] def initialize(substitutions_dict): _get_jinja_environment(substitutions_dict)
[docs] def raise_exception(message): raise MacroError(message)
[docs] def update_substitutions_dict(filename, substitutions_dict): """ Update the substitutions dictionary with macro definitions from a Jinja2 file. This function treats the given filename as a Jinja2 file containing macro definitions. It exports definitions that do not start with an underscore (_) into the substitutions_dict, which is a dictionary mapping names to macro objects. During the macro compilation process, symbols that already exist in substitutions_dict may be used by the new definitions. Args: filename (str): The path to the Jinja2 file containing macro definitions. substitutions_dict (dict): The dictionary to update with new macro definitions. Returns: None """ template = _get_jinja_environment(substitutions_dict).get_template(filename) all_symbols = template.make_module(substitutions_dict).__dict__ for name, symbol in all_symbols.items(): if name.startswith("_"): continue substitutions_dict[name] = symbol
[docs] def process_file(filepath, substitutions_dict): """ Process the Jinja file at the given path with the specified substitutions. Args: filepath (str): The path to the Jinja file to be processed. substitutions_dict (dict): A dictionary containing the substitutions to be applied to the template. Returns: str: The rendered template as a string. """ filepath = os.path.abspath(filepath) template = _get_jinja_environment(substitutions_dict).get_template(filepath) return template.render(substitutions_dict)
[docs] def add_python_functions(substitutions_dict): """ Adds predefined Python functions to the provided substitutions dictionary. The following functions are added: - 'product_to_name': Maps a product identifier to its name. - 'name_to_platform': Maps a name to its platform. - 'product_to_platform': Maps a product identifier to its platform. - 'url_encode': Encodes a URL. - 'raise': Raises an exception. - 'expand_yaml_path': Expands a YAML path. Args: substitutions_dict (dict): The dictionary to which the functions will be added. """ substitutions_dict['product_to_name'] = product_to_name substitutions_dict['name_to_platform'] = name_to_platform substitutions_dict['product_to_platform'] = product_to_platform substitutions_dict['url_encode'] = url_encode substitutions_dict['raise'] = raise_exception substitutions_dict['expand_yaml_path'] = expand_yaml_path
def _load_macros_from_directory(macros_directory, substitutions_dict): """ Helper function to load and update macros from the specified directory. Args: macros_directory (str): The path to the directory containing macro files. substitutions_dict (dict): A dictionary to be augmented with Jinja macros. Raises: RuntimeError: If there is an error while reading or processing the macro files. """ try: for filename in sorted(os.listdir(macros_directory)): if filename.endswith(".jinja"): macros_file = os.path.join(macros_directory, filename) update_substitutions_dict(macros_file, substitutions_dict) except Exception as exc: msg = ("Error extracting macro definitions from '{1}': {0}" .format(str(exc), filename)) raise RuntimeError(msg) from exc def _load_macros(macros_directory, substitutions_dict=None): """ Load macros from a specified directory and add them to a substitutions dictionary. This function checks if the given macros directory exists, adds Python functions to the substitutions dictionary, and then loads macros from the directory into the dictionary. Args: macros_directory (str): The path to the directory containing macro files. substitutions_dict (dict, optional): A dictionary to store the loaded macros. If None, a new dictionary is created. Returns: dict: The updated substitutions dictionary containing the loaded macros. Raises: RuntimeError: If the specified macros directory does not exist. """ if substitutions_dict is None: substitutions_dict = dict() add_python_functions(substitutions_dict) if not os.path.isdir(macros_directory): msg = ("The directory '{}' does not exist.").format(macros_directory) raise RuntimeError(msg) _load_macros_from_directory(macros_directory, substitutions_dict) return substitutions_dict
[docs] def load_macros(substitutions_dict=None): """ Augments the provided substitutions_dict with project Jinja macros found in the in JINJA_MACROS_DIRECTORY from constants.py. Args: substitutions_dict (dict, optional): A dictionary to be augmented with Jinja macros. Defaults to None. Returns: dict: The updated substitutions_dict containing the Jinja macros. """ return _load_macros(JINJA_MACROS_DIRECTORY, substitutions_dict)
[docs] def load_macros_from_content_dir(content_dir, substitutions_dict=None): """ Augments the provided substitutions_dict with project Jinja macros found in a specified content directory. Args: content_dir (str): The base directory containing the 'shared/macros' subdirectory. substitutions_dict (dict, optional): A dictionary to be augmented with Jinja macros. Defaults to None. Returns: dict: The updated substitutions_dict containing the Jinja macros. """ jinja_macros_directory = os.path.join(content_dir, 'shared', 'macros') return _load_macros(jinja_macros_directory, substitutions_dict)
[docs] def url_encode(source): """ Encodes a given string into a URL-safe format. Args: source (str): The string to be URL-encoded. Returns: str: The URL-encoded string. """ return quote(source)
[docs] def expand_yaml_path(path, parameter): """ Expands a dot-separated YAML path into a formatted YAML string. Args: path (str): The dot-separated path to be expanded. parameter (str): An additional parameter to be appended at the end of the path. Returns: str: A formatted YAML string representing the expanded path. """ out = "" i = 0 for x in path.split("."): i += 1 if i != len(path.split(".")): out += i * " " + x + ":\n" elif parameter != "": out += i * " " + x + ":\n" i += 1 out += i * " " + parameter else: out += i * " " + x return out
[docs] def render_template(data, template_path, output_path, loader): """ Renders a template with the given data and writes the output to a file. Args: data (dict): The data to be used in the template rendering. template_path (str): The path to the template file. output_path (str): The path where the rendered output will be written. loader (jinja2.BaseLoader): The Jinja2 loader to use for loading templates. Returns: None """ env = _get_jinja_environment(dict()) env.loader = loader result = process_file(template_path, data) with open(output_path, "wb") as f: f.write(result.encode('utf8', 'replace'))