from __future__ import annotations
import logging
import os
import shutil
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Iterable
from jinja2 import FileSystemLoader
from jinja2.sandbox import SandboxedEnvironment
from semantic_release.helpers import dynamic_import
if TYPE_CHECKING:
from typing import Literal
from jinja2 import Environment
log = logging.getLogger(__name__)
# pylint: disable=too-many-arguments,too-many-locals
[docs]def environment(
template_dir: Path | str = ".",
block_start_string: str = "{%",
block_end_string: str = "%}",
variable_start_string: str = "{{",
variable_end_string: str = "}}",
comment_start_string: str = "{#",
comment_end_string: str = "#}",
line_statement_prefix: str | None = None,
line_comment_prefix: str | None = None,
trim_blocks: bool = False,
lstrip_blocks: bool = False,
newline_sequence: Literal["\n", "\r", "\r\n"] = "\n",
keep_trailing_newline: bool = False,
extensions: Iterable[str] = (),
autoescape: bool | str = True,
) -> SandboxedEnvironment:
"""
Create a jinja2.sandbox.SandboxedEnvironment with certain parameter resrictions.
For example the Loader is fixed to FileSystemLoader, although the searchpath
is configurable.
``autoescape`` can be a string in which case it should follow the convention
``module:attr``, in this instance it will be dynamically imported.
See https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment for full
parameter descriptions
"""
autoescape_value: bool | Callable[[str | None], bool]
if isinstance(autoescape, str):
autoescape_value = dynamic_import(autoescape)
else:
autoescape_value = autoescape
log.debug("%s", locals())
return SandboxedEnvironment(
block_start_string=block_start_string,
block_end_string=block_end_string,
variable_start_string=variable_start_string,
variable_end_string=variable_end_string,
comment_start_string=comment_start_string,
comment_end_string=comment_end_string,
line_statement_prefix=line_statement_prefix,
line_comment_prefix=line_comment_prefix,
trim_blocks=trim_blocks,
lstrip_blocks=lstrip_blocks,
newline_sequence=newline_sequence,
keep_trailing_newline=keep_trailing_newline,
extensions=extensions,
autoescape=autoescape_value,
loader=FileSystemLoader(template_dir, encoding="utf-8"),
)
# pylint: disable=redefined-outer-name
[docs]def recursive_render(
template_dir: Path,
environment: Environment,
_root_dir: str | os.PathLike[str] = ".",
) -> list[str]:
rendered_paths: list[str] = []
for root, file in (
(Path(root), file)
for root, _, files in os.walk(template_dir)
for file in files
# we slice relpath.parts[1:] to allow the top-level
# template folder to have a dot prefix.
# e.g. to permit ".github/psr-templates" to contain the templates,
# rather than enforcing a top-level, non-hidden directory
if not any(
elem.startswith(".")
for elem in Path(root).relative_to(template_dir).parts[1:]
)
and not file.startswith(".")
):
output_path = (_root_dir / root.relative_to(template_dir)).resolve()
log.info("Rendering templates from %s to %s", root, output_path)
output_path.mkdir(parents=True, exist_ok=True)
if file.endswith(".j2"):
# We know the file ends with .j2 by the filter in the for-loop
output_filename = file[:-3]
# Strip off the template directory from the front of the root path -
# that's the output location relative to the repo root
src_file_path = str((root / file).relative_to(template_dir))
output_file_path = str((output_path / output_filename).resolve())
log.debug("rendering %s to %s", src_file_path, output_file_path)
stream = environment.get_template(src_file_path).stream()
with open(output_file_path, "wb+") as output_file:
stream.dump(output_file, encoding="utf-8")
rendered_paths.append(output_file_path)
else:
src_file = str((root / file).resolve())
target_file = str((output_path / file).resolve())
log.debug(
"source file %s is not a template, copying to %s", src_file, target_file
)
shutil.copyfile(src_file, target_file)
rendered_paths.append(target_file)
return rendered_paths