Source code for conda_workspaces.template
"""Jinja2 template rendering for task commands and paths."""
from __future__ import annotations
from functools import lru_cache
from typing import TYPE_CHECKING
from .context import build_template_context
from .exceptions import CondaWorkspacesError
if TYPE_CHECKING:
from pathlib import Path
from jinja2 import Environment as JinjaEnvironment
@lru_cache(maxsize=1)
def _get_jinja_env() -> JinjaEnvironment:
"""Create and cache a sandboxed Jinja2 Environment (singleton)."""
from jinja2 import StrictUndefined
from jinja2.sandbox import SandboxedEnvironment
return SandboxedEnvironment(undefined=StrictUndefined)
[docs]
def render(
template_str: str,
manifest_path: Path | None = None,
task_args: dict[str, str] | None = None,
extra_context: dict[str, object] | None = None,
target_prefix: Path | None = None,
) -> str:
"""Render a Jinja2 template string with the conda-workspaces template context.
If *template_str* contains no template markers it is returned as-is
(fast path that avoids Jinja2 import entirely).
"""
if "{{" not in template_str and "{%" not in template_str:
return template_str
env = _get_jinja_env()
ctx = build_template_context(
manifest_path=manifest_path,
task_args=task_args,
target_prefix=target_prefix,
)
if extra_context:
ctx.update(extra_context)
tpl = env.from_string(template_str)
return tpl.render(ctx)
[docs]
def render_command(
template_str: str,
manifest_path: Path | None = None,
task_args: dict[str, str] | None = None,
target_prefix: Path | None = None,
) -> str:
"""Render shell source without inserting named task argument data.
Named arguments belong in argv-list commands or task environment values.
Quoting before template rendering cannot protect every shell syntax context.
"""
if task_args and ("{{" in template_str or "{%" in template_str):
from jinja2 import meta
argument_names = task_args.keys() - {"conda", "pixi"}
env = _get_jinja_env().overlay()
# Jinja excludes globals from undeclared names, but arguments can shadow them.
env.globals = {
key: value
for key, value in env.globals.items()
if key not in argument_names
}
referenced_names = meta.find_undeclared_variables(env.parse(template_str))
if argument_names.intersection(referenced_names):
raise CondaWorkspacesError(
"Named task arguments cannot be referenced in shell command templates.",
hints=[
'Use an array of strings for cmd: ["pytest", "{{ path }}"].',
"For shell tasks, pass data through env for the program to read.",
],
)
return render(
template_str,
manifest_path=manifest_path,
target_prefix=target_prefix,
)
[docs]
def render_list(
items: list[str],
manifest_path: Path | None = None,
task_args: dict[str, str] | None = None,
target_prefix: Path | None = None,
) -> list[str]:
"""Render each string in *items* through the template engine."""
return [
render(
item,
manifest_path=manifest_path,
task_args=task_args,
target_prefix=target_prefix,
)
for item in items
]