aboutsummaryrefslogtreecommitdiff
path: root/tools/cru-py/cru/template.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/cru-py/cru/template.py')
-rw-r--r--tools/cru-py/cru/template.py153
1 files changed, 153 insertions, 0 deletions
diff --git a/tools/cru-py/cru/template.py b/tools/cru-py/cru/template.py
new file mode 100644
index 0000000..6749cab
--- /dev/null
+++ b/tools/cru-py/cru/template.py
@@ -0,0 +1,153 @@
+from collections.abc import Mapping
+import os
+import os.path
+from pathlib import Path
+from string import Template
+
+from ._iter import CruIterator
+from ._error import CruException
+
+
+class CruTemplateError(CruException):
+ pass
+
+
+class CruTemplate:
+ def __init__(self, prefix: str, text: str):
+ self._prefix = prefix
+ self._template = Template(text)
+ self._variables = (
+ CruIterator(self._template.get_identifiers())
+ .filter(lambda i: i.startswith(self._prefix))
+ .to_set()
+ )
+ self._all_variables = set(self._template.get_identifiers())
+
+ @property
+ def prefix(self) -> str:
+ return self._prefix
+
+ @property
+ def raw_text(self) -> str:
+ return self._template.template
+
+ @property
+ def py_template(self) -> Template:
+ return self._template
+
+ @property
+ def variables(self) -> set[str]:
+ return self._variables
+
+ @property
+ def all_variables(self) -> set[str]:
+ return self._all_variables
+
+ @property
+ def has_variables(self) -> bool:
+ """
+ If the template does not has any variables that starts with the given prefix,
+ it returns False. This usually indicates that the template is not a real
+ template and should be copied as is. Otherwise, it returns True.
+
+ This can be used as a guard to prevent invalid templates created accidentally
+ without notice.
+ """
+ return len(self.variables) > 0
+
+ def generate(self, mapping: Mapping[str, str], allow_extra: bool = True) -> str:
+ values = dict(mapping)
+ if not self.variables <= set(values.keys()):
+ raise CruTemplateError("Missing variables.")
+ if not allow_extra and not set(values.keys()) <= self.variables:
+ raise CruTemplateError("Extra variables.")
+ return self._template.safe_substitute(values)
+
+
+class TemplateTree:
+ def __init__(
+ self,
+ prefix: str,
+ source: str,
+ template_file_suffix: str | None = ".template",
+ ):
+ """
+ If template_file_suffix is not None, the files will be checked according to the
+ suffix of the file name. If the suffix matches, the file will be regarded as a
+ template file. Otherwise, it will be regarded as a non-template file.
+ Content of template file must contain variables that need to be replaced, while
+ content of non-template file may not contain any variables.
+ If either case is false, it generally means whether the file is a template is
+ wrongly handled.
+ """
+ self._prefix = prefix
+ self._files: list[tuple[Path, CruTemplate]] = []
+ self._source = source
+ self._template_file_suffix = template_file_suffix
+ self._load()
+
+ @property
+ def prefix(self) -> str:
+ return self._prefix
+
+ @property
+ def templates(self) -> list[tuple[Path, CruTemplate]]:
+ return self._files
+
+ @property
+ def source(self) -> str:
+ return self._source
+
+ @property
+ def template_file_suffix(self) -> str | None:
+ return self._template_file_suffix
+
+ @staticmethod
+ def _scan_files(root_path: str) -> list[Path]:
+ result: list[Path] = []
+ for root, _dirs, files in os.walk(root_path):
+ for file in files:
+ path = Path(root, file)
+ path = path.relative_to(root_path)
+ result.append(Path(path))
+ return result
+
+ def _load(self) -> None:
+ files = self._scan_files(self.source)
+ for file_path in files:
+ template_file = Path(self.source) / file_path
+ with open(template_file, "r") as f:
+ content = f.read()
+ template = CruTemplate(self.prefix, content)
+ if self.template_file_suffix is not None:
+ should_be_template = file_path.name.endswith(self.template_file_suffix)
+ if should_be_template and not template.has_variables:
+ raise CruTemplateError(
+ f"Template file {file_path} has no variables."
+ )
+ elif not should_be_template and template.has_variables:
+ raise CruTemplateError(f"Non-template {file_path} has variables.")
+ self._files.append((file_path, template))
+
+ @property
+ def variables(self) -> set[str]:
+ s = set()
+ for _, template in self.templates:
+ s.update(template.variables)
+ return s
+
+ def generate_to(
+ self, destination: str, variables: Mapping[str, str], dry_run: bool
+ ) -> None:
+ for file, template in self.templates:
+ des = Path(destination) / file
+ if self.template_file_suffix is not None and des.name.endswith(
+ self.template_file_suffix
+ ):
+ des = des.parent / (des.name[: -len(self.template_file_suffix)])
+
+ text = template.generate(variables)
+ if not dry_run:
+ des.parent.mkdir(parents=True, exist_ok=True)
+ with open(des, "w") as f:
+ f.write(text)