import textwrap from typing import Any, Iterable, TypeAlias, TypeGuard TupleList: TypeAlias = Iterable[tuple[str, str]] ListDict: TypeAlias = dict[str, list[str]] StrDict: TypeAlias = dict[str, str] RenderValues: TypeAlias = ListDict | StrDict | TupleList def is_str_dict(values: RenderValues) -> TypeGuard[StrDict]: if not isinstance(values, dict): return False for key, val in values.items(): if not isinstance(key, str): return False if not isinstance(val, str): return False return True def is_list_dict(values: RenderValues) -> TypeGuard[ListDict]: if not isinstance(values, dict): return False for key, val in values.items(): if not isinstance(key, str): return False if isinstance(val, str): continue if not isinstance(val, list): return False for entry in val: if not isinstance(entry, str): return False return True def is_tuple_list(values: RenderValues) -> TypeGuard[TupleList]: if not isinstance(values, list): return False for item in values: if not isinstance(item, tuple): return False if not len(item) == 2: return False if not isinstance(item[0], str): return False if not isinstance(item[1], str): return False return True def render_values_to_list_dict(values: RenderValues) -> ListDict: def __tuple_list_to_dict(src: TupleList) -> ListDict: ret: ListDict = {} for key, val in src: entry = ret.setdefault(key, []) entry.append(val) return ret if is_list_dict(values): return values if is_tuple_list(values): return __tuple_list_to_dict(values) raise Exception('Unsupported template value layout') def merge_values(*values: RenderValues) -> ListDict: ret: ListDict = {} for rhs in values: rhs_dict = render_values_to_list_dict(rhs) for key, val in rhs_dict.items(): entry = ret.setdefault(key, []) entry += val return ret def format_list_dict( template: str, values: ListDict | dict[str, str], li_quote: bool, li_delimiter: str ) -> str: def __format_value(val: Any) -> str: if not li_quote: return str(val) return f'"{val}"' fmt_dict: dict[str, str] = {} for key, value in values.items(): if isinstance(value, (list, tuple)): fmt_dict[key] = li_delimiter.join(map(__format_value, value)) elif isinstance(value, str): fmt_dict[key] = value ret: list[str] = [] parts = template.splitlines(keepends = True) for line in parts: for key, value in fmt_dict.items(): marker = '{' + key + '}' if marker in line: indent = line[:line.index(marker)] value = str(value).replace('\n', '\n' + indent) line = line.replace(marker, value) ret.append(line) return ''.join(ret) def format_lines( template: str, values: list[RenderValues], li_quote: bool, li_delimiter: str ) -> str: return format_list_dict(template, merge_values(*values), li_quote, li_delimiter) _templates = { 'pkg-config': """\ prefix = {prefix} exec_prefix = {{prefix}} includedir = {{prefix}}/include libdir = {{exec_prefix}}/lib Name: {name} Description: {description} Version: {version} """, 'pyrightconfig.json': """\ { "extends": {base}, "include": [ {include} ], "exclude": [ "**/__pycache__", "**/.pytest_cache", "**/.mypy_cache", "**/.ruff_cache", "**/.venv", "**/build", "**/dist" ], "extraPaths": [ {extra_paths} ], "typeCheckingMode": "basic", "pythonPlatform": "Linux" } """, } def tmpl_render( template_name: str, values: list[RenderValues], li_quote: bool = False, li_delimiter: str = '\n', search_path: list[str] | None = None, ) -> str: def __format(template: str) -> str: return format_lines( template, values, li_quote = li_quote, li_delimiter = li_delimiter, ) for d in search_path if search_path else []: path = d + '/' + template_name try: with open(path, 'r') as f: template = f.read() return __format(template) except FileNotFoundError: pass raw = _templates.get(template_name, None) if raw is None: raise Exception(f'Failed to find template "{template_name}"') return __format(textwrap.dedent(raw))