Source code for ibek.utils
"""
A class containing utility functions for passing into the Jinja context.
This allows us to provide simple functions that can be called inside
Jinja templates with {{ _global.function_name() }}. It also allows
us to maintain state between calls to the Jinja templates because
we pass a single instance of this class into all Jinja contexts.
"""
import os
from pathlib import Path
from typing import Any, Dict, Mapping
from jinja2 import StrictUndefined, Template
[docs]
class Utils:
"""
A Utility class for adding functions to the Jinja context
"""
def __init__(self: "Utils"):
self.file_name: str = ""
self.ioc_name: str = ""
self.__reset__()
def __reset__(self: "Utils"):
"""
Reset all saved state. For use in testing where more than one
IOC is rendered in a single session
"""
self.variables: Dict[str, Any] = {}
[docs]
def set_file_name(self: "Utils", file: Path):
"""
Set the ioc name based on the file name of the instance definition
"""
self.file_name = file.stem
[docs]
def set_ioc_name(self: "Utils", name: str):
"""
Set the ioc name based on the file name of the instance definition
"""
self.ioc_name = name
[docs]
def get_env(self, key: str) -> str:
"""
Get an environment variable
"""
return os.environ.get(key, "")
[docs]
def set(self, key: str, value: Any) -> Any:
"""create a global variable for our jinja context"""
s_key = str(key)
self.variables[s_key] = value
return value
[docs]
def get(self, key: str, default="") -> Any:
"""get the value a global variable for our jinja context"""
# default is used to set an initial value if the variable is not set
s_key = str(key)
return self.variables.get(s_key, default)
[docs]
def incrementor(
self, name: str, start: int = 0, increment: int = 1, stop: int | None = None
) -> int:
"""
get a named counter that increments by inc each time it is called
creates a new counter if it does not yet exist
"""
index = str(name)
counter = self.variables.get(index)
if counter is None:
self.variables[index] = start
else:
if not isinstance(counter, int):
raise ValueError(f"Variable {index} is not an integer")
self.variables[index] += increment
if stop is not None and self.variables[index] > stop:
raise ValueError(f"Counter {index} exceeded maximum value of {stop}")
return self.variables[index]
[docs]
def render(self, context: Any, template_text: Any) -> str:
"""
Render a Jinja template with the global _global object in the context
"""
if not isinstance(template_text, str):
# because this function is used to template arguments, it may
# be passed a non string which will always render to itself
return template_text
try:
jinja_template = Template(template_text, undefined=StrictUndefined)
return jinja_template.render(
context,
# global context for all jinja renders
_global=self,
# put variables created with set/get directly in the context
**self.variables,
ioc_yaml_file_name=self.file_name,
ioc_name=self.ioc_name,
)
except Exception:
print(f"ERROR RENDERING TEMPLATE:\n{template_text}")
raise
[docs]
def render_map(self, context: Any, map: Mapping[str, str | None]) -> dict[str, str]:
"""
Render a map of jinja templates with values from the given context.
If given a key with a value of `None`, the key itself will be used as a template
value, so ``{"P": None}`` is equivalent to ``{"P": "{{ P }}"}``.
Args:
context: Context to extract template variables from
map: Map of macro to jinja template to render
"""
return {
key: self.render(
context, template if template is not None else "{{ %s }}" % key
)
for key, template in map.items()
}
# a singleton Utility object for sharing state across all Entity renders
UTILS: Utils = Utils()